{"record":{"id":"a2b01704c5bb086e","repo":"vercel/turborepo","slug":"package-manager-unexpected-a2b017","errorCode":"package_manager-unexpected","errorMessage":"Not an npm project","messagePattern":"Not an npm project","errorType":"exception","errorClass":"ConvertError","httpStatus":null,"severity":"error","filePath":"packages/turbo-workspaces/src/managers/npm.ts","lineNumber":57,"sourceCode":" */\n// eslint-disable-next-line @typescript-eslint/require-await -- must match the detect type signature\nasync function detect(args: DetectArgs): Promise<boolean> {\n  const lockFile = path.join(args.workspaceRoot, PACKAGE_MANAGER_DETAILS.lock);\n  const packageManager = getWorkspacePackageManager({\n    workspaceRoot: args.workspaceRoot\n  });\n  return (\n    fs.existsSync(lockFile) || packageManager === PACKAGE_MANAGER_DETAILS.name\n  );\n}\n\n/**\n  Read workspace data from npm workspaces into generic format\n*/\nasync function read(args: ReadArgs): Promise<Project> {\n  const isNpm = await detect(args);\n  if (!isNpm) {\n    throw new ConvertError(\"Not an npm project\", {\n      type: \"package_manager-unexpected\"\n    });\n  }\n\n  const packageJson = getPackageJson(args);\n  const { name, description } = getWorkspaceInfo(args);\n  const workspaceGlobs = parseWorkspacePackages({\n    workspaces: packageJson.workspaces\n  });\n  return {\n    name,\n    description,\n    packageManager: PACKAGE_MANAGER_DETAILS.name,\n    paths: expandPaths({\n      root: args.workspaceRoot,\n      lockFile: PACKAGE_MANAGER_DETAILS.lock\n    }),\n    workspaceData: {","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/vercel/turborepo/blob/9f94a7d215b3881942527dd526afa3fc650869d5/packages/turbo-workspaces/src/managers/npm.ts#L39-L75","documentation":"Thrown by the npm manager handler's read() in turbo-workspaces when it is asked to read an npm workspaces project but its own detect() returns false. Detection means: package-lock.json exists at the workspace root OR the packageManager/devEngines.packageManager field in package.json declares npm. It is a ConvertError with type \"package_manager-unexpected\", i.e. the caller assumed a package manager the project does not actually use.","triggerScenarios":"Calling MANAGERS.npm.read({ workspaceRoot }) directly (MANAGERS is exported) on a root with neither package-lock.json nor a \"packageManager\": \"npm@...\"/devEngines.packageManager declaration; or via getWorkspaceDetails()/convert() when package-lock.json or the packageManager field is deleted between detect() and read() (e.g. a concurrent install, git checkout, or cleanup script running during migration).","commonSituations":"Lockfile removed or gitignored so a fresh clone/CI checkout has no package-lock.json; migrating away from pnpm/yarn by hand (deleted the old lockfile, ran npm nothing yet); passing the wrong root (repo root instead of the directory that holds package.json); assuming npm is the default and calling npm.read without ever generating a lockfile.","solutions":["Ensure package-lock.json exists at the workspace root: run `npm install` (or `npm install --package-lock-only`) to generate it, then retry","Add an explicit declaration to root package.json: \"packageManager\": \"npm@10.x.y\" (or devEngines.packageManager: { name: \"npm\", version: \"10.x.y\" }) so detection does not depend on the lockfile","If using the public API, prefer getWorkspaceDetails({ root }) over calling MANAGERS.npm.read directly - it probes all managers in order (aube, nub, pnpm, yarn, npm, bun) and reads whichever detects","Stop concurrent installs/checkouts that delete the lockfile between detection and read, then re-run the convert/migrate command"],"exampleFix":"// before - assumes npm, throws if no lockfile and no declaration\nconst project = await MANAGERS.npm.read({ workspaceRoot: root });\n\n// after - detect first, or let getWorkspaceDetails pick the right manager\nif (await MANAGERS.npm.detect({ workspaceRoot: root })) {\n  const project = await MANAGERS.npm.read({ workspaceRoot: root });\n} else {\n  const project = await getWorkspaceDetails({ root }); // probes all managers\n}","handlingStrategy":"validation","validationCode":"import { MANAGERS } from \"turbo-workspaces\";\nimport path from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\n// Gate npm.read() on its own detect() before calling it\nconst workspaceRoot = path.resolve(root);\nconst hasLockfile = existsSync(path.join(workspaceRoot, \"package-lock.json\"));\nconst isNpm = hasLockfile || (await MANAGERS.npm.detect({ workspaceRoot }));\nif (!isNpm) {\n  throw new Error(`${workspaceRoot} is not an npm project (no package-lock.json, no npm declaration)`);\n}\nconst project = await MANAGERS.npm.read({ workspaceRoot });","typeGuard":"import { ConvertError } from \"turbo-workspaces\";\n\nfunction isPackageManagerUnexpected(err: unknown): err is ConvertError {\n  return err instanceof ConvertError && err.type === \"package_manager-unexpected\";\n}","tryCatchPattern":"try {\n  const project = await MANAGERS.npm.read({ workspaceRoot });\n} catch (err) {\n  if (err instanceof ConvertError && err.type === \"package_manager-unexpected\") {\n    // fall back to manager-agnostic discovery instead of assuming npm\n    project = await getWorkspaceDetails({ root: workspaceRoot });\n  } else {\n    throw err;\n  }\n}","preventionTips":["Prefer getWorkspaceDetails({ root }) over calling a specific manager's read - it detects first and never mismatches","Keep the lockfile committed so detection has a stable marker on every checkout","Declare \"packageManager\": \"npm@<version>\" in root package.json to make detection independent of lockfile presence","Never run installs, git cleans, or migrations concurrently against the same workspace"],"tags":["npm","workspaces","package-manager-detection","migration","convert"],"backgroundTag":"wrong-package-manager","analyzedSha":"9f94a7d215b3881942527dd526afa3fc650869d5","analyzedAt":"2026-08-16T19:47:29.531Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}