Skip to content

Commit 0474a9c

Browse files
arcaniszkochan
andauthored
feat: add support for package maps (#12430)
Generate a Node.js package map at `node_modules/.package-map.json` on every isolated or hoisted install, including under the global virtual store, so that third-party tooling can start experimenting with package maps. The file is serialized compactly. Two settings control how the map is consumed: - `node-experimental-package-map` (default: off): inject `--experimental-package-map` into `NODE_OPTIONS` for the Node.js scripts pnpm runs — dependency lifecycle scripts, `pnpm exec`, and `pnpm run` (including recursive runs). - `node-package-map-type` (`standard` | `loose`): choose between a strict map and one that tolerates hoisting-like access. Covered by both the pnpm CLI and the pacquet (Rust) implementation. --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
1 parent c2414e8 commit 0474a9c

53 files changed

Lines changed: 3373 additions & 51 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@pnpm/installing.commands": patch
3+
"pnpm": patch
4+
---
5+
6+
Fixed `pnpm import` for Yarn v2 lockfiles when `js-yaml` v4 is installed.

.changeset/package-maps.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@pnpm/config.reader": minor
3+
"@pnpm/exec.commands": minor
4+
"@pnpm/exec.lifecycle": minor
5+
"@pnpm/installing.commands": minor
6+
"@pnpm/installing.deps-installer": minor
7+
"@pnpm/installing.deps-restorer": minor
8+
"@pnpm/lockfile.to-pnp": minor
9+
"pnpm": minor
10+
---
11+
12+
Added support for generating Node.js package maps at `node_modules/.package-map.json` during isolated and hoisted installs. Added the `node-experimental-package-map` setting to inject the generated map into pnpm-managed Node.js script environments, and the `node-package-map-type` setting to choose between `standard` and `loose` package maps.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/reader/src/Config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ export interface Config extends OptionsFromRootManifest {
265265
globalPkgDir: string
266266
lockfile?: boolean
267267
dedupeInjectedDeps?: boolean
268+
nodeExperimentalPackageMap?: boolean
269+
nodePackageMapType?: 'standard' | 'loose'
268270
nodeOptions?: string
269271
pmOnFail?: 'download' | 'error' | 'warn' | 'ignore'
270272
runtime?: boolean

config/reader/src/configFileKey.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export const pnpmConfigFileKeys = [
4343
'minimum-release-age-ignore-missing-time',
4444
'minimum-release-age-strict',
4545
'network-concurrency',
46+
'node-experimental-package-map',
47+
'node-package-map-type',
4648
'noproxy',
4749
'npm-path',
4850
'npmrc-auth-file',

config/reader/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ export async function getConfig (opts: {
143143
'dedupe-injected-deps': true,
144144
'disallow-workspace-cycles': false,
145145
'enable-modules-dir': true,
146+
'node-experimental-package-map': false,
147+
'node-package-map-type': 'standard',
146148
'enable-pre-post-scripts': true,
147149
'exclude-links-from-lockfile': false,
148150
'extend-node-path': true,

config/reader/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export const pnpmTypes = {
7575
'minimum-release-age-strict': Boolean,
7676
'modules-dir': String,
7777
'network-concurrency': Number,
78+
'node-experimental-package-map': Boolean,
79+
'node-package-map-type': ['standard', 'loose'],
7880
'node-linker': ['pnp', 'isolated', 'hoisted'],
7981
noproxy: String,
8082
'npm-path': String,

exec/commands/src/exec.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { type Config, type ConfigContext, getWorkspaceConcurrency, types } from
66
import { lifecycleLogger, type LifecycleMessage } from '@pnpm/core-loggers'
77
import type { CheckDepsStatusOptions } from '@pnpm/deps.status'
88
import { PnpmError } from '@pnpm/error'
9-
import { makeNodeRequireOption } from '@pnpm/exec.lifecycle'
9+
import { makeNodePackageMapOption, makeNodeRequireOption } from '@pnpm/exec.lifecycle'
1010
import { logger } from '@pnpm/logger'
1111
import { prependDirsToPath } from '@pnpm/shell.path'
1212
import type { Project, ProjectRootDir, ProjectRootDirRealPath, ProjectsGraph } from '@pnpm/types'
@@ -45,6 +45,8 @@ export function rcOptionsTypes (): Record<string, unknown> {
4545
'unsafe-perm',
4646
'workspace-concurrency',
4747
'reporter-hide-prefix',
48+
'node-experimental-package-map',
49+
'node-package-map-type',
4850
], types),
4951
'shell-mode': Boolean,
5052
'resume-from': String,
@@ -154,6 +156,7 @@ export type ExecOpts = Required<Pick<ConfigContext, 'selectedProjectsGraph'>> &
154156
| 'lockfileDir'
155157
| 'modulesDir'
156158
| 'nodeOptions'
159+
| 'nodeExperimentalPackageMap'
157160
| 'pnpmHomeDir'
158161
| 'recursive'
159162
| 'reporter'
@@ -220,6 +223,8 @@ export async function handler (
220223
const result = createEmptyRecursiveSummary(chunks)
221224
const existsPnp = existsInDir.bind(null, '.pnp.cjs')
222225
const workspacePnpPath = opts.workspaceDir && existsPnp(opts.workspaceDir)
226+
const existsPackageMap = existsInDir.bind(null, path.join(opts.modulesDir ?? 'node_modules', '.package-map.json'))
227+
const workspacePackageMapPath = opts.nodeExperimentalPackageMap && opts.workspaceDir && existsPackageMap(opts.workspaceDir)
223228

224229
let exitCode = 0
225230
const prependPaths = [
@@ -235,15 +240,21 @@ export async function handler (
235240
const startTime = process.hrtime()
236241
try {
237242
const pnpPath = workspacePnpPath ?? existsPnp(prefix)
238-
const extraEnv = {
243+
const packageMapPath = workspacePackageMapPath || (opts.nodeExperimentalPackageMap && existsPackageMap(prefix))
244+
const extraEnv: Record<string, string | undefined> = {
239245
...opts.extraEnv,
240-
...(pnpPath ? makeNodeRequireOption(pnpPath) : {}),
246+
...(opts.nodeOptions ? { NODE_OPTIONS: opts.nodeOptions } : {}),
247+
}
248+
if (pnpPath) {
249+
Object.assign(extraEnv, makeNodeRequireOption(pnpPath, extraEnv))
250+
}
251+
if (packageMapPath) {
252+
Object.assign(extraEnv, makeNodePackageMapOption(packageMapPath, extraEnv))
241253
}
242254
const env = makeEnv({
243255
extraEnv: {
244256
...extraEnv,
245257
PNPM_PACKAGE_NAME: opts.selectedProjectsGraph[prefix]?.package.manifest.name,
246-
...(opts.nodeOptions ? { NODE_OPTIONS: opts.nodeOptions } : {}),
247258
},
248259
prependPaths,
249260
userAgent: opts.userAgent,

exec/commands/src/run.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { type Config, type ConfigContext, getWorkspaceConcurrency, types as allT
1111
import type { CheckDepsStatusOptions } from '@pnpm/deps.status'
1212
import { PnpmError } from '@pnpm/error'
1313
import {
14+
makeNodePackageMapOption,
1415
makeNodeRequireOption,
1516
runLifecycleHook,
1617
type RunLifecycleHookOptions,
@@ -88,6 +89,8 @@ export function rcOptionsTypes (): Record<string, unknown> {
8889
return {
8990
...pick([
9091
'npm-path',
92+
'node-experimental-package-map',
93+
'node-package-map-type',
9194
], allTypes),
9295
}
9396
}
@@ -168,6 +171,7 @@ export type RunOpts =
168171
| 'extraBinPaths'
169172
| 'extraEnv'
170173
| 'nodeOptions'
174+
| 'nodeExperimentalPackageMap'
171175
| 'pnpmHomeDir'
172176
| 'reporter'
173177
| 'scriptShell'
@@ -289,7 +293,17 @@ so you may run "pnpm -w run ${scriptName}"`,
289293
if (pnpPath) {
290294
lifecycleOpts.extraEnv = {
291295
...lifecycleOpts.extraEnv,
292-
...makeNodeRequireOption(pnpPath),
296+
...makeNodeRequireOption(pnpPath, lifecycleOpts.extraEnv),
297+
}
298+
}
299+
const existsPackageMap = existsInDir.bind(null, path.join(opts.modulesDir ?? 'node_modules', '.package-map.json'))
300+
const packageMapPath = opts.nodeExperimentalPackageMap
301+
? (opts.workspaceDir && existsPackageMap(opts.workspaceDir)) ?? existsPackageMap(dir)
302+
: undefined
303+
if (packageMapPath) {
304+
lifecycleOpts.extraEnv = {
305+
...lifecycleOpts.extraEnv,
306+
...makeNodePackageMapOption(packageMapPath, lifecycleOpts.extraEnv),
293307
}
294308
}
295309
const limitRun = pLimit(concurrency)

exec/commands/src/runRecursive.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { throwOnCommandFail } from '@pnpm/cli.utils'
66
import { type Config, type ConfigContext, getWorkspaceConcurrency } from '@pnpm/config.reader'
77
import { PnpmError } from '@pnpm/error'
88
import {
9+
makeNodePackageMapOption,
910
makeNodeRequireOption,
1011
type RunLifecycleHookOptions,
1112
} from '@pnpm/exec.lifecycle'
@@ -34,6 +35,8 @@ export type RecursiveRunOpts = Pick<Config,
3435
| 'stream'
3536
| 'syncInjectedDepsAfterScripts'
3637
| 'workspaceDir'
38+
| 'nodeExperimentalPackageMap'
39+
| 'modulesDir'
3740
> & Pick<ConfigContext, 'rootProjectManifest'> & Required<Pick<ConfigContext, 'allProjects' | 'selectedProjectsGraph'> & Pick<Config, 'workspaceDir' | 'dir'>> &
3841
Partial<Pick<Config, 'extraBinPaths' | 'extraEnv' | 'bail' | 'reporter' | 'reverse' | 'sort' | 'workspaceConcurrency'>> &
3942
{
@@ -74,6 +77,8 @@ export async function runRecursive (
7477
: 'pipe'
7578
const existsPnp = existsInDir.bind(null, '.pnp.cjs')
7679
const workspacePnpPath = opts.workspaceDir && existsPnp(opts.workspaceDir)
80+
const existsPackageMap = existsInDir.bind(null, path.join(opts.modulesDir ?? 'node_modules', '.package-map.json'))
81+
const workspacePackageMapPath = opts.nodeExperimentalPackageMap && opts.workspaceDir && existsPackageMap(opts.workspaceDir)
7782

7883
const requiredScripts = opts.requiredScripts ?? []
7984
if (requiredScripts.includes(scriptName)) {
@@ -135,7 +140,14 @@ export async function runRecursive (
135140
if (pnpPath) {
136141
lifecycleOpts.extraEnv = {
137142
...lifecycleOpts.extraEnv,
138-
...makeNodeRequireOption(pnpPath),
143+
...makeNodeRequireOption(pnpPath, lifecycleOpts.extraEnv),
144+
}
145+
}
146+
const packageMapPath = workspacePackageMapPath || (opts.nodeExperimentalPackageMap && existsPackageMap(prefix))
147+
if (packageMapPath) {
148+
lifecycleOpts.extraEnv = {
149+
...lifecycleOpts.extraEnv,
150+
...makeNodePackageMapOption(packageMapPath, lifecycleOpts.extraEnv),
139151
}
140152
}
141153

0 commit comments

Comments
 (0)