SPEC.md
Turborepo should support devEngines.packageManager in the root package.json as a package manager declaration source. User-facing documentation, errors, and package-manager metadata tooling should lean into devEngines.packageManager as the preferred ecosystem-aligned declaration, while preserving existing top-level packageManager behavior for compatibility.
Implementation precedence remains:
packageManager if present.devEngines.packageManager if top-level packageManager is absent.This is read-path support plus aligned tooling/docs updates. It must not change package-manager behavior after the declaration resolves to the existing internal PackageManager enum.
npm introduced devEngines to describe development-time tooling expectations in package.json. Supporting devEngines.packageManager lets Turborepo participate in that newer ecosystem standard while continuing to support existing repositories that use the top-level Corepack packageManager field.
The desired posture is:
devEngines.packageManager going forward.packageManager authoritative when it exists.devEngines enforcement.Supported root package.json shape:
{
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "9.12.3"
}
}
}
Supported name values:
npmpnpmyarnbunRules:
devEngines.packageManager must be an object.name is required.name must be a lowercase string matching a supported package manager.version is required.version must be an exact valid semver version.name or version is invalid.onFail is ignored entirely.Examples of valid exact versions:
{ "name": "pnpm", "version": "9.12.3" }
{ "name": "pnpm", "version": "9.12.3-alpha.0" }
{ "name": "yarn", "version": "4.5.0+sha224.abc" }
Examples of invalid versions:
{ "name": "pnpm", "version": "^9.0.0" }
{ "name": "pnpm", "version": "9" }
{ "name": "pnpm", "version": "9.12" }
{ "name": "pnpm", "version": " 9.12.3 " }
{ "name": "pnpm", "version": "https://registry.npmjs.org/pnpm/-/pnpm-9.12.3.tgz" }
{ "name": "pnpm", "version": "9.12.3+sha512.Purxi/Zex==" }
If top-level packageManager exists, Turborepo uses existing top-level behavior. devEngines.packageManager must not override it.
{
"packageManager": "[email protected]",
"devEngines": {
"packageManager": { "name": "npm", "version": "10.5.0" }
}
}
This resolves as pnpm because top-level packageManager is authoritative.
If top-level packageManager is absent and devEngines.packageManager exists, Turborepo parses and validates devEngines.packageManager and resolves it to the existing internal PackageManager enum.
If neither field exists, Turborepo should hard error as it does today for missing top-level packageManager. The user-facing message should recommend adding root devEngines.packageManager as the preferred declaration, with top-level packageManager mentioned only as legacy/backward-compatible support where useful.
Only the root package.json participates in this feature. Turborepo should not add upward parent-directory searching and should not read workspace package devEngines.packageManager fields for detection.
After parsing exact semver, mapping to internal variants must reuse existing behavior:
pnpm versions map through the existing PnpmDetector::detect_pnpm6_or_pnpm logic.yarn versions map through the existing YarnDetector::detect_berry_or_yarn logic.npm maps to PackageManager::Npm.bun maps to PackageManager::Bun.The version is transient. Do not store it in long-lived package graph state or add it to public APIs unless an existing API already exposes it for equivalent behavior.
When devEngines.packageManager is used as the declaration source, Turborepo should validate it against existing implicit lockfile detection signals.
Current implicit detection signals are root lockfiles:
pnpm-lock.yamlpackage-lock.jsonyarn.lockbun.lockbun.lockb, which currently surfaces the existing BunBinaryLockfile errorDo not expand mismatch validation to package-manager config files such as pnpm-workspace.yaml or .yarnrc.yml unless existing implicit detection starts using those files.
If implicit detection finds no lockfile signal, there is no mismatch. Return the declared manager.
If implicit detection finds a conflicting manager, hard error.
{
"devEngines": {
"packageManager": { "name": "pnpm", "version": "9.12.3" }
}
}
With package-lock.json, this is an error because the declaration says pnpm but the lockfile indicates npm.
If multiple lockfile signals exist, use existing implicit detection behavior. That means the existing multiple-package-manager error should still occur.
For mismatch comparison:
Pnpm6, Pnpm, and Pnpm9 as the same pnpm family because current lockfile detection only proves pnpm.yarn.lock parsing distinguishes them.npm and bun distinct.When possible, mismatch diagnostics should include both the declared manager and the lockfile signal that caused the conflict.
If top-level packageManager is absent and devEngines.packageManager exists but is malformed, detection must stop immediately with a hard error. Do not silently fall back to lockfile detection. The user expressed intent, but the intent is not properly expressed.
Hard-error cases include:
devEngines is present but not an object.devEngines.packageManager is present but not an object.devEngines.packageManager is null.devEngines.packageManager is an array.name is missing.name is not a string.name is empty.name is unsupported.version is missing.version is not a string.version is empty.version is not exact valid semver.Unsupported names should fail on name before version validation.
Declaration shape and semver validation should happen before lockfile mismatch validation.
For an empty object, prefer a combined shape error instead of reporting only missing name or only missing version.
{
"devEngines": {
"packageManager": {}
}
}
Diagnostic should communicate the expected shape, such as:
{
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "9.12.3"
}
}
}
dangerouslyDisablePackageManagerCheck should apply to package manager declaration checks for both supported declaration fields.
When enabled:
devEngines.packageManager parsing and mismatch validation.bun.lockb.The option should not introduce a new default package manager. It should not suppress existing implicit detection consistency errors unless current behavior already does.
User-facing docs, comments, and schema descriptions for this option should mention both fields or use declaration-neutral wording such as "package manager declaration checks in root package.json".
Rust PackageJson parsing should add structured support for devEngines rather than reading it ad hoc from unstructured other data.
Recommended shape:
PackageJson.dev_engines: Option<DevEngines>DevEngines.package_manager: Option<DevEnginesPackageManager>DevEngines.other to preserve non-package-manager entries.DevEnginesPackageManager.name with span metadata.DevEnginesPackageManager.version with span metadata.DevEnginesPackageManager.other to preserve ignored properties such as onFail and unknown future keys.Spans are required for actionable diagnostics. Labels should point to the most specific location available:
devEngines.packageManager for wrong type, null, array, or combined shape errors.devEngines.packageManager.name for missing, non-string, empty, unsupported, or mismatch errors.devEngines.packageManager.version for missing, non-string, empty, or invalid semver errors.Round-trip serialization must preserve:
devEngines entries such as runtime, cpu, os, and libc.devEngines keys.devEngines.packageManager.onFail.devEngines.packageManager keys.TypeScript package-manager detection in @turbo/workspaces must support devEngines.packageManager with the same rules as Rust detection.
Requirements:
packageManager, then devEngines.packageManager, then existing implicit detection.onFail and unknown properties.devEngines.packageManager in errors.Rust and TypeScript detection must not disagree on whether a root package manager declaration is valid.
The existing add-package-manager codemod should keep the same transformer name but write devEngines.packageManager instead of top-level packageManager.
New output shape:
{
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "9.12.3"
}
}
}
Rules:
packageManager as already set.devEngines.packageManager as already set.devEngines when adding packageManager.devEngines entries.add-package-manager.devEngines.packageManager.Example before:
{
"devEngines": {
"runtime": { "name": "node", "version": "22.0.0" }
}
}
Example after:
{
"devEngines": {
"runtime": { "name": "node", "version": "22.0.0" },
"packageManager": { "name": "pnpm", "version": "9.12.3" }
}
}
Workspace conversion tooling should use devEngines.packageManager for package-manager metadata when adding or migrating declarations.
Rules:
devEngines.packageManager with shallow merge semantics.packageManager and/or devEngines.packageManager when those fields identify the manager being removed.packageManager first so implementation precedence does not keep resolving to the old manager.User-facing docs, errors, and generated comments should lead with devEngines.packageManager as the preferred declaration.
Guidance:
packageManager" in user-facing text.devEngines.packageManager".packageManager only as legacy/backward-compatible support where relevant.devEngines.packageManager as the recommended declaration.add-package-manager codemod, the codemod must write devEngines.packageManager first.dangerouslyDisablePackageManagerCheck comments/docs to mention both declaration fields or use declaration-neutral wording.No turbo.json schema shape changes are needed beyond descriptive comments for existing options.
Do not update examples, templates, or new project scaffolding to write devEngines.packageManager in the initial implementation.
Reason: examples/templates may be consumed by older stable Turborepo versions that do not yet support devEngines.packageManager.
Add a follow-up note: once the stable release containing detection support is available, examples/templates/new project scaffolding should be updated to prefer devEngines.packageManager.
Add complete coverage across shared detection layers and representative end-to-end command paths.
Rust unit coverage:
PackageJson round-trips devEngines and ignored fields.devEngines.packageManager resolves to each supported manager.pnpm exact versions map through existing pnpm version logic.yarn exact versions map through existing Yarn classic/Berry logic.packageManager takes precedence over devEngines.packageManager.packageManager uses valid devEngines.packageManager.devEngines.packageManager.null is rejected.devEngines is rejected when top-level packageManager is absent.devEngines.packageManager is rejected.name / missing version are rejected.name / version are rejected.dangerouslyDisablePackageManagerCheck bypasses devEngines.packageManager parsing/mismatch and uses existing implicit detection.Rust integration coverage:
turbo run works when only devEngines.packageManager is present.devEngines.packageManager is present.turbo generate uses the correct package-manager command when only devEngines.packageManager is present.turbo prune preserves devEngines.packageManager in the pruned root package.json.package.json watch behavior.packages/turbo-repository/rust are updated if they assert package manager detection behavior.TypeScript coverage:
@turbo/workspaces detects devEngines.packageManager with the same rules as Rust.@turbo/workspaces preserves top-level precedence.devEngines.packageManager in user-facing copy.devEngines.packageManager.devEngines.packageManager exists.devEngines.packageManager exists.New test fixtures should prefer devEngines.packageManager unless the test specifically covers legacy top-level packageManager behavior.
Parsing devEngines.packageManager must be pure and local.
Do not:
Rejecting URL versions and non-semver integrity strings avoids turning package-manager detection into a command execution or network-resolution surface.
This feature should only read root metadata and existing root lockfile signals.
Expected overhead:
package.json.yarn.lock contents when existing Yarn detection would already do so.Do not add:
Large repos should not see package-count-dependent overhead from this feature.
No telemetry should be added.
Optional low-volume debug tracing is acceptable for local diagnosis, such as:
devEngines.packageManager as the declaration source.devEngines.packageManager because dangerouslyDisablePackageManagerCheck is enabled.Avoid logging raw package.json values beyond manager names and safe file paths.
No persisted data migration is required.
No cache migration is required.
No lockfile migration is required.
Resolved package-manager behavior should be identical for equivalent declarations after mapping to the internal PackageManager enum.
Top-level packageManager remains supported and authoritative for backward compatibility.
Examples/templates/new project scaffolding should be deferred until the stable release supports devEngines.packageManager.
This feature does not include:
devEngines enforcement.devEngines.packageManager arrays.version.berry, pnpm9, or yarn@berry.devEngines.packageManager.version.onFail.ARCHITECTURE.md or CONTRIBUTING.md unless later implementation changes touch their documented areas.devEngines.packageManager is supported when top-level packageManager is absent.packageManager remains authoritative when present.devEngines.packageManager hard-errors when it is the active declaration source.dangerouslyDisablePackageManagerCheck bypasses devEngines.packageManager validation and uses existing implicit detection.devEngines.packageManager and preserves existing metadata.devEngines.packageManager as preferred.