fix: node runtime is not moved to dependencies on pnpm add (#10210)

close #10209
This commit is contained in:
Zoltan Kochan
2025-11-20 02:35:46 +01:00
committed by GitHub
parent 8ffb1a7f0c
commit 98a5f1ce33
4 changed files with 76 additions and 20 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/read-project-manifest": patch
"pnpm": patch
---
Node.js runtime is not added to "dependencies" on `pnpm add`, if there's a `engines.runtime` setting declared in `package.json` [#10209](https://github.com/pnpm/pnpm/issues/10209).

View File

@@ -0,0 +1,9 @@
{
"engines": {
"runtime": {
"name": "node",
"version": "24",
"onFail": "download"
}
}
}

View File

@@ -229,41 +229,51 @@ function convertManifestAfterRead (manifest: ProjectManifest): ProjectManifest {
}
function convertManifestBeforeWrite (manifest: ProjectManifest): ProjectManifest {
for (const runtimeName of ['node', 'deno', 'bun']) {
const nodeDep = manifest.devDependencies?.[runtimeName]
if (typeof nodeDep === 'string' && nodeDep.startsWith('runtime:')) {
const version = nodeDep.replace(/^runtime:/, '')
manifest.devEngines ??= {}
convertDependenciesToEnginesRuntime(manifest, 'devDependencies', 'devEngines')
convertDependenciesToEnginesRuntime(manifest, 'dependencies', 'engines')
return manifest
}
const nodeRuntimeEntry: EngineDependency = {
function convertDependenciesToEnginesRuntime (
manifest: ProjectManifest,
dependenciesFieldName: 'dependencies' | 'devDependencies',
enginesFieldName: 'engines' | 'devEngines'
): void {
for (const runtimeName of ['node', 'deno', 'bun']) {
const dep = manifest[dependenciesFieldName]?.[runtimeName]
if (typeof dep === 'string' && dep.startsWith('runtime:')) {
const version = dep.replace(/^runtime:/, '')
manifest[enginesFieldName] ??= {}
const runtimeEntry: EngineDependency = {
name: runtimeName,
version,
onFail: 'download',
}
if (!manifest.devEngines.runtime) {
manifest.devEngines.runtime = nodeRuntimeEntry
} else if (Array.isArray(manifest.devEngines.runtime)) {
const existing = manifest.devEngines.runtime.find(({ name }) => name === runtimeName)
const enginesField = manifest[enginesFieldName]!
if (!enginesField.runtime) {
enginesField.runtime = runtimeEntry
} else if (Array.isArray(enginesField.runtime)) {
const existing = enginesField.runtime.find(({ name }) => name === runtimeName)
if (existing) {
Object.assign(existing, nodeRuntimeEntry)
Object.assign(existing, runtimeEntry)
} else {
manifest.devEngines.runtime.push(nodeRuntimeEntry)
enginesField.runtime.push(runtimeEntry)
}
} else if (manifest.devEngines.runtime.name === runtimeName) {
Object.assign(manifest.devEngines.runtime, nodeRuntimeEntry)
} else if (enginesField.runtime.name === runtimeName) {
Object.assign(enginesField.runtime, runtimeEntry)
} else {
manifest.devEngines.runtime = [
manifest.devEngines.runtime,
nodeRuntimeEntry,
enginesField.runtime = [
enginesField.runtime,
runtimeEntry,
]
}
if (manifest.devDependencies) {
delete manifest.devDependencies[runtimeName]
if (manifest[dependenciesFieldName]) {
delete manifest[dependenciesFieldName][runtimeName]
}
}
}
return manifest
}
const dependencyKeys = new Set([

View File

@@ -63,6 +63,37 @@ test('readProjectManifest() converts devEngines runtime to devDependencies', asy
})
})
test('readProjectManifest() converts engines runtime to dependencies', async () => {
const dir = f.prepare('package-json-with-engines')
const { manifest, writeProjectManifest } = await tryReadProjectManifest(dir)
expect(manifest).toStrictEqual(
{
dependencies: {
node: 'runtime:24',
},
engines: {
runtime: {
name: 'node',
version: '24',
onFail: 'download',
},
},
}
)
await writeProjectManifest(manifest!)
const pkgJson = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
expect(pkgJson).toStrictEqual({
dependencies: {},
engines: {
runtime: {
name: 'node',
version: '24',
onFail: 'download',
},
},
})
})
test.each([
{
name: 'creates devEngines when it is missing',