vercel/turborepo · error · ConvertError
package_manager-unexpected
package_manager-unexpected
Error message
Not an npm project
What it means
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.
Source
Thrown at packages/turbo-workspaces/src/managers/npm.ts:57
*/
// eslint-disable-next-line @typescript-eslint/require-await -- must match the detect type signature
async function detect(args: DetectArgs): Promise<boolean> {
const lockFile = path.join(args.workspaceRoot, PACKAGE_MANAGER_DETAILS.lock);
const packageManager = getWorkspacePackageManager({
workspaceRoot: args.workspaceRoot
});
return (
fs.existsSync(lockFile) || packageManager === PACKAGE_MANAGER_DETAILS.name
);
}
/**
Read workspace data from npm workspaces into generic format
*/
async function read(args: ReadArgs): Promise<Project> {
const isNpm = await detect(args);
if (!isNpm) {
throw new ConvertError("Not an npm project", {
type: "package_manager-unexpected"
});
}
const packageJson = getPackageJson(args);
const { name, description } = getWorkspaceInfo(args);
const workspaceGlobs = parseWorkspacePackages({
workspaces: packageJson.workspaces
});
return {
name,
description,
packageManager: PACKAGE_MANAGER_DETAILS.name,
paths: expandPaths({
root: args.workspaceRoot,
lockFile: PACKAGE_MANAGER_DETAILS.lock
}),
workspaceData: {View on GitHub (pinned to 9f94a7d215)
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
Example fix
// before - assumes npm, throws if no lockfile and no declaration
const project = await MANAGERS.npm.read({ workspaceRoot: root });
// after - detect first, or let getWorkspaceDetails pick the right manager
if (await MANAGERS.npm.detect({ workspaceRoot: root })) {
const project = await MANAGERS.npm.read({ workspaceRoot: root });
} else {
const project = await getWorkspaceDetails({ root }); // probes all managers
} Defensive patterns
Strategy: validation
Validate before calling
import { MANAGERS } from "turbo-workspaces";
import path from "node:path";
import { existsSync } from "node:fs";
// Gate npm.read() on its own detect() before calling it
const workspaceRoot = path.resolve(root);
const hasLockfile = existsSync(path.join(workspaceRoot, "package-lock.json"));
const isNpm = hasLockfile || (await MANAGERS.npm.detect({ workspaceRoot }));
if (!isNpm) {
throw new Error(`${workspaceRoot} is not an npm project (no package-lock.json, no npm declaration)`);
}
const project = await MANAGERS.npm.read({ workspaceRoot }); Type guard
import { ConvertError } from "turbo-workspaces";
function isPackageManagerUnexpected(err: unknown): err is ConvertError {
return err instanceof ConvertError && err.type === "package_manager-unexpected";
} Try / catch
try {
const project = await MANAGERS.npm.read({ workspaceRoot });
} catch (err) {
if (err instanceof ConvertError && err.type === "package_manager-unexpected") {
// fall back to manager-agnostic discovery instead of assuming npm
project = await getWorkspaceDetails({ root: workspaceRoot });
} else {
throw err;
}
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- package_manager-unexpected
- package_manager-unexpected
- package_manager-unexpected
- bun-workspace_glob_error
- pnpm-workspace_parse_error
AI-assisted analysis of vercel/turborepo@9f94a7d215 (2026-08-16).
Data as JSON: /api/errors/a2b01704c5bb086e.
Report an issue: GitHub.