mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-09 08:54:57 -04:00
feat: add --no-runtime to skip installing runtime entries (#11557)
Adds a `--no-runtime` flag (config: `runtime: boolean`, default `true`) that suppresses install of runtime entries declared via `devEngines.runtime` (the `runtime:` protocol) **without modifying the lockfile**.
The lockfile keeps the runtime entry, so frozen-lockfile validation still passes; only the runtime fetch and `.bin` linking are skipped. Useful in CI matrices where the runtime is provisioned externally (e.g. via `pnpm runtime -g set node <version>`) before `pnpm install` runs.
The existing `--runtime-on-fail=ignore` is unsuitable for this case: it mutates the manifest and regenerates the lockfile to drop the runtime entry, which trips frozen-lockfile validation. The two flags are orthogonal and serve different purposes.
### Implementation
The hook lives in the lockfile filter stage:
- `lockfile/filtering/src/filterImporter.ts` — strips `runtime:` refs from the importer's deps maps when `skipRuntimes` is set.
- `lockfile/filtering/src/filterLockfileByImportersAndEngine.ts` — new `skipRuntimes?: boolean` option; runtime-protocol direct deps are dropped before they enter `pickedPackages`, so they never reach the dep graph or bin-linker. Applies to all runtimes (`node`, `deno`, `bun`) since they share the `runtime:` protocol prefix.
The option is plumbed through `installing/deps-restorer`, `installing/deps-installer`, and `installing/commands` to the user-facing `pnpm install --no-runtime` flag.
### Example
```json
// package.json
{
"devEngines": {
"runtime": {
"name": "node",
"version": "22.13.0",
"onFail": "download"
}
}
}
```
Local dev: `pnpm install` — installs node 22.13.0 as before.
CI matrix entry:
```yaml
- run: pn runtime -g set node ${{ matrix.node }}
- run: pn install --no-runtime
```
The lockfile is unchanged; the matrix's externally-provisioned node is used.
This commit is contained in:
11
.changeset/no-runtime-flag.md
Normal file
11
.changeset/no-runtime-flag.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"@pnpm/lockfile.filtering": minor
|
||||
"@pnpm/installing.linking.modules-cleaner": minor
|
||||
"@pnpm/installing.deps-restorer": minor
|
||||
"@pnpm/installing.deps-installer": minor
|
||||
"@pnpm/installing.commands": minor
|
||||
"@pnpm/config.reader": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Add `--no-runtime` flag (config: `runtime=false`) to skip installing runtime entries (e.g. Node.js downloaded via `devEngines.runtime`) without modifying the lockfile. The lockfile keeps the runtime entry so frozen-lockfile validation still passes; only the runtime fetch and `.bin` linking are skipped. Useful in CI matrices where the runtime is provisioned externally (e.g. via `pnpm runtime -g set node <version>`) before `pnpm install` runs.
|
||||
@@ -249,6 +249,7 @@ export interface Config extends OptionsFromRootManifest {
|
||||
dedupeInjectedDeps?: boolean
|
||||
nodeOptions?: string
|
||||
pmOnFail?: 'download' | 'error' | 'warn' | 'ignore'
|
||||
runtime?: boolean
|
||||
runtimeOnFail?: 'download' | 'error' | 'warn' | 'ignore'
|
||||
virtualStoreDirMaxLength: number
|
||||
peersSuffixMaxLength?: number
|
||||
|
||||
@@ -129,6 +129,7 @@ export const excludedPnpmKeys = [
|
||||
'publish-branch',
|
||||
'recursive-install',
|
||||
'resolve-peers-from-workspace-root',
|
||||
'runtime',
|
||||
'runtime-on-fail',
|
||||
'aggregate-output',
|
||||
'reporter-hide-prefix',
|
||||
|
||||
@@ -98,6 +98,7 @@ export const pnpmTypes = {
|
||||
reporter: String,
|
||||
'resolution-mode': ['highest', 'time-based', 'lowest-direct'],
|
||||
'resolve-peers-from-workspace-root': Boolean,
|
||||
runtime: Boolean,
|
||||
'runtime-on-fail': ['ignore', 'warn', 'error', 'download'],
|
||||
'aggregate-output': Boolean,
|
||||
'reporter-hide-prefix': Boolean,
|
||||
|
||||
@@ -56,6 +56,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
|
||||
'public-hoist-pattern',
|
||||
'registry',
|
||||
'reporter',
|
||||
'runtime',
|
||||
'save-workspace-protocol',
|
||||
'scripts-prepend-node-path',
|
||||
'shamefully-hoist',
|
||||
@@ -132,6 +133,10 @@ For options that may be used with `-r`, see "pnpm help recursive"',
|
||||
description: '`optionalDependencies` are not installed',
|
||||
name: '--no-optional',
|
||||
},
|
||||
{
|
||||
description: 'Skip installing runtime entries (e.g. Node.js downloaded via `devEngines.runtime`). The lockfile is left untouched, so frozen installs still validate; only the runtime fetch and bin-linking are skipped. Useful in CI matrices where the runtime is provisioned externally.',
|
||||
name: '--no-runtime',
|
||||
},
|
||||
{
|
||||
description: `Don't read or generate a \`${WANTED_LOCKFILE}\` file`,
|
||||
name: '--no-lockfile',
|
||||
|
||||
@@ -84,6 +84,7 @@ export type InstallDepsOptions = Pick<Config,
|
||||
| 'production'
|
||||
| 'preferWorkspacePackages'
|
||||
| 'registries'
|
||||
| 'runtime'
|
||||
| 'runtimeOnFail'
|
||||
| 'save'
|
||||
| 'saveDev'
|
||||
@@ -275,6 +276,7 @@ export async function installDeps (
|
||||
linkWorkspacePackagesDepth: opts.linkWorkspacePackages === 'deep' ? Infinity : opts.linkWorkspacePackages ? 0 : -1,
|
||||
sideEffectsCacheRead: opts.sideEffectsCache ?? opts.sideEffectsCacheReadonly,
|
||||
sideEffectsCacheWrite: opts.sideEffectsCache,
|
||||
skipRuntimes: opts.runtime === false,
|
||||
storeController: store.ctrl,
|
||||
storeDir: store.dir,
|
||||
workspacePackages,
|
||||
|
||||
@@ -73,6 +73,7 @@ export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
|
||||
| 'agent'
|
||||
| 'allowBuilds'
|
||||
| 'registries'
|
||||
| 'runtime'
|
||||
| 'save'
|
||||
| 'saveCatalogName'
|
||||
| 'saveDev'
|
||||
@@ -160,6 +161,7 @@ export async function recursive (
|
||||
(((opts.ignoredPackages == null) || opts.ignoredPackages.size === 0) &&
|
||||
pkgs.length === allProjects.length),
|
||||
saveCatalogName: opts.saveCatalogName,
|
||||
skipRuntimes: opts.runtime === false,
|
||||
storeController: store.ctrl,
|
||||
storeDir: store.dir,
|
||||
targetDependenciesField,
|
||||
|
||||
@@ -165,6 +165,7 @@ export interface StrictInstallOptions {
|
||||
*/
|
||||
disableRelinkLocalDirDeps: boolean
|
||||
|
||||
skipRuntimes: boolean
|
||||
supportedArchitectures?: SupportedArchitectures
|
||||
hoistWorkspacePackages?: boolean
|
||||
virtualStoreDirMaxLength: number
|
||||
@@ -279,6 +280,7 @@ const defaults = (opts: InstallOptions): StrictInstallOptions => {
|
||||
ignoreWorkspaceCycles: false,
|
||||
disallowWorkspaceCycles: false,
|
||||
excludeLinksFromLockfile: false,
|
||||
skipRuntimes: false,
|
||||
virtualStoreDirMaxLength: 120,
|
||||
peersSuffixMaxLength: 1000,
|
||||
blockExoticSubdeps: false,
|
||||
|
||||
@@ -1371,6 +1371,20 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (opts.skipRuntimes) {
|
||||
// The lockfile filter (filterImporter) handles wantedLockfile-driven linking,
|
||||
// but the direct bin-linking path at the end of _installInContext iterates
|
||||
// dependenciesByProjectId and only filters by ctx.skipped. Add runtime
|
||||
// depPaths there so that path skips them too.
|
||||
for (const id of Object.keys(dependenciesByProjectId) as ProjectId[]) {
|
||||
for (const [alias, depPath] of dependenciesByProjectId[id].entries()) {
|
||||
if (depPath.includes('@runtime:')) {
|
||||
ctx.skipped.add(depPath)
|
||||
dependenciesByProjectId[id].delete(alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stageLogger.debug({
|
||||
prefix: ctx.lockfileDir,
|
||||
@@ -1423,6 +1437,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
sideEffectsCacheRead: opts.sideEffectsCacheRead,
|
||||
symlink: opts.symlink,
|
||||
skipped: ctx.skipped,
|
||||
skipRuntimes: opts.skipRuntimes,
|
||||
storeController: opts.storeController,
|
||||
virtualStoreDir: ctx.virtualStoreDir,
|
||||
virtualStoreDirMaxLength: ctx.virtualStoreDirMaxLength,
|
||||
|
||||
@@ -68,6 +68,7 @@ export interface LinkPackagesOptions {
|
||||
sideEffectsCacheRead: boolean
|
||||
symlink: boolean
|
||||
skipped: Set<DepPath>
|
||||
skipRuntimes?: boolean
|
||||
storeController: StoreController
|
||||
virtualStoreDir: string
|
||||
virtualStoreDirMaxLength: number
|
||||
@@ -120,6 +121,7 @@ export async function linkPackages (projects: ImporterToUpdate[], depGraph: Depe
|
||||
pruneVirtualStore: opts.pruneVirtualStore,
|
||||
publicHoistedModulesDir: (opts.publicHoistPattern != null) ? opts.rootModulesDir : undefined,
|
||||
skipped: opts.skipped,
|
||||
skipRuntimes: opts.skipRuntimes,
|
||||
storeController: opts.storeController,
|
||||
virtualStoreDir: opts.virtualStoreDir,
|
||||
virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength,
|
||||
@@ -136,6 +138,7 @@ export async function linkPackages (projects: ImporterToUpdate[], depGraph: Depe
|
||||
include: opts.include,
|
||||
registries: opts.registries,
|
||||
skipped: opts.skipped,
|
||||
skipRuntimes: opts.skipRuntimes,
|
||||
}
|
||||
const newCurrentLockfile = filterLockfileByImporters(opts.wantedLockfile, projectIds, {
|
||||
...filterOpts,
|
||||
|
||||
@@ -176,6 +176,7 @@ export interface HeadlessOptions {
|
||||
pendingBuilds: string[]
|
||||
resolveSymlinksInInjectedDirs?: boolean
|
||||
skipped: Set<DepPath>
|
||||
skipRuntimes?: boolean
|
||||
enableModulesDir?: boolean
|
||||
virtualStoreOnly?: boolean
|
||||
nodeLinker?: 'isolated' | 'hoisted' | 'pnp'
|
||||
@@ -259,6 +260,7 @@ export async function headlessInstall (opts: HeadlessOptions): Promise<Installat
|
||||
include: opts.include,
|
||||
registries: opts.registries,
|
||||
skipped,
|
||||
skipRuntimes: opts.skipRuntimes,
|
||||
currentEngine: opts.currentEngine,
|
||||
engineStrict: opts.engineStrict,
|
||||
failOnMissingDependencies: true,
|
||||
|
||||
@@ -50,6 +50,7 @@ export async function prune (
|
||||
pruneStore?: boolean
|
||||
pruneVirtualStore?: boolean
|
||||
skipped: Set<DepPath>
|
||||
skipRuntimes?: boolean
|
||||
virtualStoreDir: string
|
||||
virtualStoreDirMaxLength: number
|
||||
lockfileDir: string
|
||||
@@ -59,6 +60,7 @@ export async function prune (
|
||||
const wantedLockfile = filterLockfile(opts.wantedLockfile, {
|
||||
include: opts.include,
|
||||
skipped: opts.skipped,
|
||||
skipRuntimes: opts.skipRuntimes,
|
||||
})
|
||||
const rootImporter = wantedLockfile.importers['.' as ProjectId] ?? {} as ProjectSnapshot
|
||||
const wantedRootPkgs = mergeDependencies(rootImporter)
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import type { ProjectSnapshot } from '@pnpm/lockfile.types'
|
||||
import type { ProjectSnapshot, ResolvedDependencies } from '@pnpm/lockfile.types'
|
||||
import type { DependenciesField } from '@pnpm/types'
|
||||
|
||||
export function filterImporter (
|
||||
importer: ProjectSnapshot,
|
||||
include: { [dependenciesField in DependenciesField]: boolean }
|
||||
include: { [dependenciesField in DependenciesField]: boolean },
|
||||
opts?: { skipRuntimes?: boolean }
|
||||
): ProjectSnapshot {
|
||||
const skipRuntimes = opts?.skipRuntimes === true
|
||||
return {
|
||||
dependencies: !include.dependencies ? {} : importer.dependencies ?? {},
|
||||
devDependencies: !include.devDependencies ? {} : importer.devDependencies ?? {},
|
||||
optionalDependencies: !include.optionalDependencies ? {} : importer.optionalDependencies ?? {},
|
||||
specifiers: importer.specifiers,
|
||||
dependencies: !include.dependencies ? {} : pickNonRuntime(importer.dependencies, skipRuntimes),
|
||||
devDependencies: !include.devDependencies ? {} : pickNonRuntime(importer.devDependencies, skipRuntimes),
|
||||
optionalDependencies: !include.optionalDependencies ? {} : pickNonRuntime(importer.optionalDependencies, skipRuntimes),
|
||||
specifiers: pickNonRuntime(importer.specifiers, skipRuntimes),
|
||||
}
|
||||
}
|
||||
|
||||
function pickNonRuntime (deps: ResolvedDependencies | undefined, skipRuntimes: boolean): ResolvedDependencies {
|
||||
if (!deps) return {}
|
||||
if (!skipRuntimes) return deps
|
||||
const result: ResolvedDependencies = {}
|
||||
for (const [name, ref] of Object.entries(deps)) {
|
||||
if (!ref.startsWith('runtime:')) {
|
||||
result[name] = ref
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export function filterLockfile (
|
||||
opts: {
|
||||
include: { [dependenciesField in DependenciesField]: boolean }
|
||||
skipped: Set<DepPath>
|
||||
skipRuntimes?: boolean
|
||||
}
|
||||
): LockfileObject {
|
||||
return filterLockfileByImporters(lockfile, Object.keys(lockfile.importers) as ProjectId[], {
|
||||
|
||||
@@ -18,14 +18,20 @@ export function filterLockfileByImporters (
|
||||
opts: {
|
||||
include: { [dependenciesField in DependenciesField]: boolean }
|
||||
skipped: Set<DepPath>
|
||||
skipRuntimes?: boolean
|
||||
failOnMissingDependencies: boolean
|
||||
}
|
||||
): LockfileObject {
|
||||
const importers = { ...lockfile.importers }
|
||||
for (const importerId of importerIds) {
|
||||
importers[importerId] = filterImporter(lockfile.importers[importerId], opts.include, { skipRuntimes: opts.skipRuntimes })
|
||||
}
|
||||
|
||||
const packages = {} as PackageSnapshots
|
||||
if (lockfile.packages != null) {
|
||||
pkgAllDeps(
|
||||
lockfileWalker(
|
||||
lockfile,
|
||||
{ ...lockfile, importers },
|
||||
importerIds,
|
||||
{ include: opts.include, skipped: opts.skipped }
|
||||
).step,
|
||||
@@ -36,11 +42,6 @@ export function filterLockfileByImporters (
|
||||
)
|
||||
}
|
||||
|
||||
const importers = { ...lockfile.importers }
|
||||
for (const importerId of importerIds) {
|
||||
importers[importerId] = filterImporter(lockfile.importers[importerId], opts.include)
|
||||
}
|
||||
|
||||
return {
|
||||
...lockfile,
|
||||
importers,
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface FilterLockfileOptions {
|
||||
failOnMissingDependencies: boolean
|
||||
lockfileDir: string
|
||||
skipped: Set<string>
|
||||
skipRuntimes?: boolean
|
||||
supportedArchitectures?: SupportedArchitectures
|
||||
}
|
||||
|
||||
@@ -52,6 +53,7 @@ export function filterLockfileByImportersAndEngine (
|
||||
const directDepPaths = toImporterDepPaths(lockfile, importerIds, {
|
||||
include: opts.include,
|
||||
importerIdSet,
|
||||
skipRuntimes: opts.skipRuntimes,
|
||||
})
|
||||
|
||||
const packages =
|
||||
@@ -65,12 +67,13 @@ export function filterLockfileByImportersAndEngine (
|
||||
opts.includeIncompatiblePackages === true,
|
||||
lockfileDir: opts.lockfileDir,
|
||||
skipped: opts.skipped,
|
||||
skipRuntimes: opts.skipRuntimes,
|
||||
supportedArchitectures: opts.supportedArchitectures,
|
||||
})
|
||||
: {}
|
||||
|
||||
const importers = mapValues((importer) => {
|
||||
const newImporter = filterImporter(importer, opts.include)
|
||||
const newImporter = filterImporter(importer, opts.include, { skipRuntimes: opts.skipRuntimes })
|
||||
if (newImporter.optionalDependencies != null) {
|
||||
newImporter.optionalDependencies = pickBy((ref, depName) => {
|
||||
const depPath = dp.refToRelative(ref, depName)
|
||||
@@ -105,6 +108,7 @@ function pickPkgsWithAllDeps (
|
||||
includeIncompatiblePackages: boolean
|
||||
lockfileDir: string
|
||||
skipped: Set<string>
|
||||
skipRuntimes?: boolean
|
||||
supportedArchitectures?: SupportedArchitectures
|
||||
}
|
||||
): PackageSnapshots {
|
||||
@@ -132,6 +136,7 @@ function pkgAllDeps (
|
||||
includeIncompatiblePackages: boolean
|
||||
lockfileDir: string
|
||||
skipped: Set<string>
|
||||
skipRuntimes?: boolean
|
||||
supportedArchitectures?: SupportedArchitectures
|
||||
}
|
||||
) {
|
||||
@@ -189,6 +194,7 @@ function pkgAllDeps (
|
||||
...toImporterDepPaths(ctx.lockfile, additionalImporterIds, {
|
||||
include: opts.include,
|
||||
importerIdSet: ctx.importerIdSet,
|
||||
skipRuntimes: opts.skipRuntimes,
|
||||
})
|
||||
)
|
||||
pkgAllDeps(ctx, nextRelDepPaths, installable, opts)
|
||||
@@ -201,6 +207,7 @@ function toImporterDepPaths (
|
||||
opts: {
|
||||
include: { [dependenciesField in DependenciesField]: boolean }
|
||||
importerIdSet: Set<ProjectId>
|
||||
skipRuntimes?: boolean
|
||||
}
|
||||
): DepPath[] {
|
||||
const importerDeps = importerIds
|
||||
@@ -213,6 +220,7 @@ function toImporterDepPaths (
|
||||
: {}),
|
||||
}))
|
||||
.map(Object.entries)
|
||||
.map(entries => opts.skipRuntimes ? entries.filter(([, ref]) => !ref.startsWith('runtime:')) : entries)
|
||||
|
||||
let { depPaths, importerIds: nextImporterIds } = parseDepRefs(unnest(importerDeps), lockfile)
|
||||
|
||||
|
||||
@@ -672,3 +672,80 @@ test('filterByImportersAndEngine(): includes linked packages', () => {
|
||||
'project-3',
|
||||
])
|
||||
})
|
||||
|
||||
test('filterByImportersAndEngine(): skipRuntimes drops runtime: entries from importers and packages', () => {
|
||||
const filteredLockfile = filterLockfileByImportersAndEngine(
|
||||
{
|
||||
importers: {
|
||||
['.' as ProjectId]: {
|
||||
dependencies: {
|
||||
'regular-dep': '1.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
node: 'runtime:22.13.0',
|
||||
'dev-dep': '1.0.0',
|
||||
},
|
||||
optionalDependencies: {
|
||||
bun: 'runtime:1.1.0',
|
||||
},
|
||||
specifiers: {
|
||||
'regular-dep': '^1.0.0',
|
||||
node: 'runtime:22.13.0',
|
||||
'dev-dep': '^1.0.0',
|
||||
bun: 'runtime:1.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
packages: {
|
||||
['regular-dep@1.0.0' as DepPath]: {
|
||||
resolution: { integrity: '' },
|
||||
},
|
||||
['dev-dep@1.0.0' as DepPath]: {
|
||||
resolution: { integrity: '' },
|
||||
},
|
||||
['node@runtime:22.13.0' as DepPath]: {
|
||||
resolution: { integrity: '' },
|
||||
},
|
||||
['bun@runtime:1.1.0' as DepPath]: {
|
||||
resolution: { integrity: '' },
|
||||
},
|
||||
},
|
||||
},
|
||||
['.' as ProjectId],
|
||||
{
|
||||
currentEngine: {
|
||||
nodeVersion: '22.0.0',
|
||||
pnpmVersion: '11.0.0',
|
||||
},
|
||||
engineStrict: false,
|
||||
failOnMissingDependencies: true,
|
||||
include: {
|
||||
dependencies: true,
|
||||
devDependencies: true,
|
||||
optionalDependencies: true,
|
||||
},
|
||||
lockfileDir: process.cwd(),
|
||||
skipped: new Set<string>(),
|
||||
skipRuntimes: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(filteredLockfile.lockfile.importers['.' as ProjectId]).toStrictEqual({
|
||||
dependencies: {
|
||||
'regular-dep': '1.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
'dev-dep': '1.0.0',
|
||||
},
|
||||
optionalDependencies: {},
|
||||
specifiers: {
|
||||
'regular-dep': '^1.0.0',
|
||||
'dev-dep': '^1.0.0',
|
||||
},
|
||||
})
|
||||
expect(Object.keys(filteredLockfile.lockfile.packages ?? {}).sort()).toStrictEqual([
|
||||
'dev-dep@1.0.0',
|
||||
'regular-dep@1.0.0',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { expect, test } from '@jest/globals'
|
||||
import { prepare } from '@pnpm/prepare'
|
||||
import { writeYamlFileSync } from 'write-yaml-file'
|
||||
|
||||
import { execPnpm } from '../utils/index.js'
|
||||
|
||||
@@ -45,3 +47,91 @@ test('runtimeOnFail=ignore prevents Node.js download even when manifest sets onF
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile.importers['.'].devDependencies).toBeUndefined()
|
||||
})
|
||||
|
||||
test('--no-runtime keeps the runtime entry in the lockfile but skips installing the binary', async () => {
|
||||
const project = prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: '24.0.0',
|
||||
onFail: 'download',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await execPnpm(['install'])
|
||||
project.isExecutable('.bin/node')
|
||||
const lockfileBefore = project.readLockfile()
|
||||
expect(lockfileBefore.importers['.'].devDependencies).toStrictEqual({
|
||||
node: { specifier: 'runtime:24.0.0', version: 'runtime:24.0.0' },
|
||||
})
|
||||
|
||||
fs.rmSync('node_modules', { recursive: true, force: true })
|
||||
await execPnpm(['install', '--frozen-lockfile', '--no-runtime'])
|
||||
|
||||
const lockfileAfter = project.readLockfile()
|
||||
expect(lockfileAfter.importers['.'].devDependencies).toStrictEqual({
|
||||
node: { specifier: 'runtime:24.0.0', version: 'runtime:24.0.0' },
|
||||
})
|
||||
expectNoNodeBin()
|
||||
})
|
||||
|
||||
test('--no-runtime works on a fresh checkout with no lockfile (non-frozen path)', async () => {
|
||||
const project = prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: '24.0.0',
|
||||
onFail: 'download',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await execPnpm(['install', '--no-runtime'])
|
||||
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile.importers['.'].devDependencies).toStrictEqual({
|
||||
node: { specifier: 'runtime:24.0.0', version: 'runtime:24.0.0' },
|
||||
})
|
||||
expectNoNodeBin()
|
||||
})
|
||||
|
||||
test('--no-runtime works with enableGlobalVirtualStore=true', async () => {
|
||||
const project = prepare({
|
||||
devEngines: {
|
||||
runtime: {
|
||||
name: 'node',
|
||||
version: '24.0.0',
|
||||
onFail: 'download',
|
||||
},
|
||||
},
|
||||
})
|
||||
writeYamlFileSync(path.resolve('pnpm-workspace.yaml'), {
|
||||
enableGlobalVirtualStore: true,
|
||||
storeDir: path.resolve('store'),
|
||||
})
|
||||
|
||||
await execPnpm(['install', '--no-runtime'])
|
||||
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile.importers['.'].devDependencies).toStrictEqual({
|
||||
node: { specifier: 'runtime:24.0.0', version: 'runtime:24.0.0' },
|
||||
})
|
||||
expectNoNodeBin()
|
||||
})
|
||||
|
||||
function expectNoNodeBin (): void {
|
||||
const binDir = path.join('node_modules', '.bin')
|
||||
for (const name of ['node', 'node.exe', 'node.cmd', 'node.ps1']) {
|
||||
const p = path.join(binDir, name)
|
||||
// lstatSync (vs existsSync) catches dangling symlinks too — existsSync
|
||||
// follows symlinks and would return false for a symlink whose target was
|
||||
// never created, hiding a real bug.
|
||||
let exists = false
|
||||
try {
|
||||
fs.lstatSync(p)
|
||||
exists = true
|
||||
} catch {}
|
||||
expect(exists).toBe(false)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user