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

  1. Ensure package-lock.json exists at the workspace root: run `npm install` (or `npm install --package-lock-only`) to generate it, then retry
  2. 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
  3. 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
  4. 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

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


AI-assisted analysis of vercel/turborepo@9f94a7d215 (2026-08-16). Data as JSON: /api/errors/a2b01704c5bb086e. Report an issue: GitHub.