@pnpm/exec.lifecycle 1100.1.7 → 1100.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @pnpm/lifecycle
2
2
 
3
+ ## 1100.1.8
4
+
5
+ ### Patch Changes
6
+
7
+ - Republished every package: the tarballs published by the v11.13.1 through v11.16.0 releases were missing most of their compiled files due to a packing bug [#13164](https://github.com/pnpm/pnpm/issues/13164).
8
+
9
+ - Updated dependencies:
10
+ - @pnpm/bins.linker@1100.0.22
11
+ - @pnpm/core-loggers@1100.2.5
12
+ - @pnpm/error@1100.1.0
13
+ - @pnpm/fetching.directory-fetcher@1100.0.25
14
+ - @pnpm/pkg-manifest.reader@1100.0.12
15
+ - @pnpm/store.cafs-types@1100.0.2
16
+ - @pnpm/store.controller-types@1100.1.10
17
+ - @pnpm/types@1101.6.0
18
+
3
19
  ## 1100.1.7
4
20
 
5
21
  ### Patch Changes
package/lib/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { runLifecycleHook, type RunLifecycleHookOptions } from './runLifecycleHook.js';
2
+ import { runLifecycleHooksConcurrently, type RunLifecycleHooksConcurrentlyOptions } from './runLifecycleHooksConcurrently.js';
3
+ import { killTrackedProcessTrees, type TrackableChildProcess, trackChildProcess } from './trackChildProcess.js';
4
+ export declare function makeNodeRequireOption(modulePath: string, env?: Record<string, string | undefined>): {
5
+ NODE_OPTIONS: string;
6
+ };
7
+ export declare function makeNodePackageMapOption(packageMapPath: string, env?: Record<string, string | undefined>): {
8
+ NODE_OPTIONS: string;
9
+ };
10
+ export { killTrackedProcessTrees, runLifecycleHook, type RunLifecycleHookOptions, runLifecycleHooksConcurrently, type RunLifecycleHooksConcurrentlyOptions, type TrackableChildProcess, trackChildProcess, };
11
+ export declare function runPostinstallHooks(opts: RunLifecycleHookOptions): Promise<boolean>;
@@ -0,0 +1,19 @@
1
+ import type { DependencyManifest, ProjectManifest } from '@pnpm/types';
2
+ export interface RunLifecycleHookOptions {
3
+ args?: string[];
4
+ depPath: string;
5
+ extraBinPaths?: string[];
6
+ extraEnv?: Record<string, string>;
7
+ initCwd?: string;
8
+ optional?: boolean;
9
+ pkgRoot: string;
10
+ rootModulesDir: string;
11
+ scriptShell?: string;
12
+ silent?: boolean;
13
+ scriptsPrependNodePath?: boolean | 'warn-only';
14
+ shellEmulator?: boolean;
15
+ stdio?: string;
16
+ unsafePerm: boolean;
17
+ userAgent?: string;
18
+ }
19
+ export declare function runLifecycleHook(stage: string, manifest: ProjectManifest | DependencyManifest, opts: RunLifecycleHookOptions): Promise<boolean>;
@@ -0,0 +1,168 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { lifecycleLogger } from '@pnpm/core-loggers';
4
+ import { PnpmError } from '@pnpm/error';
5
+ import { globalWarn } from '@pnpm/logger';
6
+ import { lifecycle } from '@pnpm/npm-lifecycle';
7
+ import chalk from 'chalk';
8
+ import isWindows from 'is-windows';
9
+ import { join as shellQuote } from 'shlex';
10
+ function noop() { } // eslint-disable-line:no-empty
11
+ export async function runLifecycleHook(stage, manifest, opts) {
12
+ const optional = opts.optional === true;
13
+ // To remediate CVE_2024_27980, Node.js does not allow .bat or .cmd files to
14
+ // be spawned without the "shell: true" option.
15
+ //
16
+ // https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
17
+ //
18
+ // Unfortunately, setting spawn's shell option also causes arguments to be
19
+ // evaluated before they're passed to the shell, resulting in a surprising
20
+ // behavior difference only with .bat/.cmd files.
21
+ //
22
+ // Instead of showing a "spawn EINVAL" error, let's throw a clearer error that
23
+ // this isn't supported.
24
+ //
25
+ // If this behavior needs to be supported in the future, the arguments would
26
+ // need to be escaped before they're passed to the .bat/.cmd file. For
27
+ // example, scripts such as "echo %PATH%" should be passed verbatim rather
28
+ // than expanded. This is difficult to do correctly. Other open source tools
29
+ // (e.g. Rust) attempted and introduced bugs. The Rust blog has a good
30
+ // high-level explanation of the same security vulnerability Node.js patched.
31
+ //
32
+ // https://blog.rust-lang.org/2024/04/09/cve-2024-24576.html#overview
33
+ //
34
+ // Note that npm (as of version 10.5.0) doesn't support setting script-shell
35
+ // to a .bat or .cmd file either.
36
+ if (opts.scriptShell != null && typeof opts.scriptShell === 'string' && isWindowsBatchFile(opts.scriptShell)) {
37
+ throw new PnpmError('ERR_PNPM_INVALID_SCRIPT_SHELL_WINDOWS', 'Cannot spawn .bat or .cmd as a script shell.', {
38
+ hint: `\
39
+ The pnpm-workspace.yaml scriptShell option was configured to a .bat or .cmd file. These cannot be used as a script shell reliably.
40
+
41
+ Please unset the scriptShell option, or configure it to a .exe instead.
42
+ `,
43
+ });
44
+ }
45
+ const m = { _id: getId(manifest), ...manifest };
46
+ m.scripts = { ...m.scripts };
47
+ switch (stage) {
48
+ case 'start':
49
+ if (!m.scripts.start) {
50
+ if (!existsSync('server.js')) {
51
+ throw new PnpmError('NO_SCRIPT_OR_SERVER', 'Missing script start or file server.js');
52
+ }
53
+ m.scripts.start = 'node server.js';
54
+ }
55
+ break;
56
+ case 'install':
57
+ if (!m.scripts.install && !m.scripts.preinstall) {
58
+ checkBindingGyp(opts.pkgRoot, m.scripts);
59
+ }
60
+ break;
61
+ }
62
+ if (opts.args?.length && m.scripts?.[stage]) {
63
+ // It is impossible to quote a command line argument that contains newline for Windows cmd.
64
+ const escapedArgs = isWindows()
65
+ ? opts.args.map((arg) => JSON.stringify(arg)).join(' ')
66
+ : shellQuote(opts.args);
67
+ m.scripts[stage] = `${m.scripts[stage]} ${escapedArgs}`;
68
+ }
69
+ // This script is used to prevent the usage of npm or Yarn.
70
+ // It does nothing, when pnpm is used, so we may skip its execution.
71
+ if (m.scripts[stage] === 'npx only-allow pnpm' || !m.scripts[stage])
72
+ return false;
73
+ if (opts.stdio !== 'inherit') {
74
+ lifecycleLogger.debug({
75
+ depPath: opts.depPath,
76
+ optional,
77
+ script: m.scripts[stage],
78
+ stage,
79
+ wd: opts.pkgRoot,
80
+ });
81
+ }
82
+ else if (!opts.silent) {
83
+ process.stderr.write(chalk.dim(`$ ${m.scripts[stage]}`) + '\n');
84
+ }
85
+ const logLevel = (opts.stdio !== 'inherit' || opts.silent)
86
+ ? 'silent'
87
+ : undefined;
88
+ await lifecycle(m, stage, opts.pkgRoot, {
89
+ config: {},
90
+ dir: opts.rootModulesDir,
91
+ extraBinPaths: opts.extraBinPaths,
92
+ extraEnv: {
93
+ ...opts.extraEnv,
94
+ INIT_CWD: opts.initCwd ?? process.cwd(),
95
+ PNPM_SCRIPT_SRC_DIR: opts.pkgRoot,
96
+ ...(opts.userAgent ? { npm_config_user_agent: opts.userAgent } : {}),
97
+ },
98
+ log: {
99
+ clearProgress: noop,
100
+ info: noop,
101
+ level: logLevel,
102
+ pause: noop,
103
+ resume: noop,
104
+ showProgress: noop,
105
+ silly: npmLog,
106
+ verbose: npmLog,
107
+ warn: (...msg) => {
108
+ globalWarn(msg.join(' '));
109
+ },
110
+ },
111
+ runConcurrently: true,
112
+ scriptsPrependNodePath: opts.scriptsPrependNodePath,
113
+ scriptShell: opts.scriptShell,
114
+ shellEmulator: opts.shellEmulator,
115
+ stdio: opts.stdio ?? 'pipe',
116
+ unsafePerm: opts.unsafePerm,
117
+ });
118
+ return true;
119
+ function npmLog(prefix, logId, stdtype, line) {
120
+ switch (stdtype) {
121
+ case 'stdout':
122
+ case 'stderr':
123
+ lifecycleLogger.debug({
124
+ depPath: opts.depPath,
125
+ line: (line ?? 0).toString(),
126
+ stage,
127
+ stdio: stdtype,
128
+ wd: opts.pkgRoot,
129
+ });
130
+ return;
131
+ case 'Returned: code:': {
132
+ if (opts.stdio === 'inherit') {
133
+ // Preventing the pnpm reporter from overriding the project's script output
134
+ return;
135
+ }
136
+ const code = line ?? 1;
137
+ lifecycleLogger.debug({
138
+ depPath: opts.depPath,
139
+ exitCode: code,
140
+ optional,
141
+ stage,
142
+ wd: opts.pkgRoot,
143
+ });
144
+ }
145
+ }
146
+ }
147
+ }
148
+ /**
149
+ * Run node-gyp when binding.gyp is available. Only do this when there are no
150
+ * `install` and `preinstall` scripts (see `npm help scripts`).
151
+ */
152
+ function checkBindingGyp(root, scripts) {
153
+ if (existsSync(path.join(root, 'binding.gyp'))) {
154
+ scripts.install = 'node-gyp rebuild';
155
+ }
156
+ }
157
+ function getId(manifest) {
158
+ return `${manifest.name ?? ''}@${manifest.version ?? ''}`;
159
+ }
160
+ function isWindowsBatchFile(scriptShell) {
161
+ // Node.js performs a similar check to determine whether it should throw
162
+ // EINVAL when spawning a .cmd/.bat file.
163
+ //
164
+ // https://github.com/nodejs/node/commit/6627222409#diff-1e725bfa950eda4d4b5c0c00a2bb6be3e5b83d819872a1adf2ef87c658273903
165
+ const scriptShellLower = scriptShell.toLowerCase();
166
+ return isWindows() && (scriptShellLower.endsWith('.cmd') || scriptShellLower.endsWith('.bat'));
167
+ }
168
+ //# sourceMappingURL=runLifecycleHook.js.map
@@ -0,0 +1,18 @@
1
+ import type { StoreController } from '@pnpm/store.controller-types';
2
+ import type { ProjectManifest, ProjectRootDir } from '@pnpm/types';
3
+ import { type RunLifecycleHookOptions } from './runLifecycleHook.js';
4
+ export type RunLifecycleHooksConcurrentlyOptions = Omit<RunLifecycleHookOptions, 'depPath' | 'pkgRoot' | 'rootModulesDir'> & {
5
+ resolveSymlinksInInjectedDirs?: boolean;
6
+ storeController: StoreController;
7
+ extraNodePaths?: string[];
8
+ preferSymlinkedExecutables?: boolean;
9
+ };
10
+ export interface Importer {
11
+ buildIndex: number;
12
+ manifest: ProjectManifest;
13
+ rootDir: ProjectRootDir;
14
+ modulesDir: string;
15
+ stages?: string[];
16
+ targetDirs?: string[];
17
+ }
18
+ export declare function runLifecycleHooksConcurrently(stages: string[], importers: Importer[], childConcurrency: number, opts: RunLifecycleHooksConcurrentlyOptions): Promise<void>;
@@ -0,0 +1,67 @@
1
+ import path from 'node:path';
2
+ import { linkBins } from '@pnpm/bins.linker';
3
+ import { fetchFromDir } from '@pnpm/fetching.directory-fetcher';
4
+ import { logger } from '@pnpm/logger';
5
+ import { runGroups } from 'run-groups';
6
+ import { runLifecycleHook } from './runLifecycleHook.js';
7
+ export async function runLifecycleHooksConcurrently(stages, importers, childConcurrency, opts) {
8
+ const importersByBuildIndex = new Map();
9
+ for (const importer of importers) {
10
+ if (!importersByBuildIndex.has(importer.buildIndex)) {
11
+ importersByBuildIndex.set(importer.buildIndex, [importer]);
12
+ }
13
+ else {
14
+ importersByBuildIndex.get(importer.buildIndex).push(importer);
15
+ }
16
+ }
17
+ const sortedBuildIndexes = Array.from(importersByBuildIndex.keys()).sort((a, b) => a - b);
18
+ const groups = sortedBuildIndexes.map((buildIndex) => {
19
+ const importers = importersByBuildIndex.get(buildIndex);
20
+ return importers.map(({ manifest, modulesDir, rootDir, stages: importerStages, targetDirs }) => async () => {
21
+ // We are linking the bin files, in case they were created by lifecycle scripts of other workspace packages.
22
+ await linkBins(modulesDir, path.join(modulesDir, '.bin'), {
23
+ extraNodePaths: opts.extraNodePaths,
24
+ allowExoticManifests: true,
25
+ preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
26
+ projectManifest: manifest,
27
+ warn: (message) => {
28
+ logger.warn({ message, prefix: rootDir });
29
+ },
30
+ });
31
+ const runLifecycleHookOpts = {
32
+ ...opts,
33
+ depPath: rootDir,
34
+ pkgRoot: rootDir,
35
+ rootModulesDir: modulesDir,
36
+ };
37
+ let isBuilt = false;
38
+ for (const stage of (importerStages ?? stages)) {
39
+ if (await runLifecycleHook(stage, manifest, runLifecycleHookOpts)) { // eslint-disable-line no-await-in-loop
40
+ isBuilt = true;
41
+ }
42
+ }
43
+ if (targetDirs == null || targetDirs.length === 0 || !isBuilt)
44
+ return;
45
+ // Re-import only the freshly-built source — fetchFromDir already
46
+ // excludes the source's node_modules/. `keepModulesDir: true` makes
47
+ // importIndexedDir skip the destructive makeEmptyDir fast path
48
+ // (#11088) and preserve the target's existing node_modules (bin
49
+ // symlinks + transitive deps from the initial install) via its
50
+ // staging/move path. Replaces the old scanDir-into-filesMap
51
+ // workaround (#4299) that the fast path then wiped, causing ENOENT
52
+ // on .bin/<tool>. Stays on storeController.importPackage so source
53
+ // files keep their hardlinks (no copy-loop).
54
+ const filesResponse = await fetchFromDir(rootDir, { resolveSymlinks: opts.resolveSymlinksInInjectedDirs });
55
+ await Promise.all(targetDirs.map(async (targetDir) => opts.storeController.importPackage(targetDir, {
56
+ filesResponse: {
57
+ resolvedFrom: 'local-dir',
58
+ ...filesResponse,
59
+ },
60
+ force: false,
61
+ keepModulesDir: true,
62
+ })));
63
+ });
64
+ });
65
+ await runGroups(childConcurrency, groups);
66
+ }
67
+ //# sourceMappingURL=runLifecycleHooksConcurrently.js.map
@@ -0,0 +1,23 @@
1
+ export interface TrackableChildProcess {
2
+ pid?: number;
3
+ once: (event: 'close' | 'error', listener: () => void) => unknown;
4
+ }
5
+ /**
6
+ * Registers a child process spawned for a user command so that
7
+ * killTrackedProcessTrees() can terminate its process tree if pnpm exits
8
+ * while the command is still running.
9
+ */
10
+ export declare function trackChildProcess(child: TrackableChildProcess): void;
11
+ /**
12
+ * Kills the process trees of the still-running child processes registered
13
+ * with trackChildProcess(). Best-effort: children that exited concurrently
14
+ * are skipped silently.
15
+ *
16
+ * On Windows the tree is killed with `taskkill /T`, which terminates every
17
+ * descendant of a known PID without enumerating the system process list (an
18
+ * enumeration needs `wmic` or PowerShell there and can take tens of seconds).
19
+ * On POSIX only the tracked child itself is signalled; descendants are
20
+ * expected to be handled by the caller (the error handler enumerates them
21
+ * cheaply with one `ps` call).
22
+ */
23
+ export declare function killTrackedProcessTrees(): Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ const trackedChildPids = new Set();
4
+ /**
5
+ * Registers a child process spawned for a user command so that
6
+ * killTrackedProcessTrees() can terminate its process tree if pnpm exits
7
+ * while the command is still running.
8
+ */
9
+ export function trackChildProcess(child) {
10
+ const pid = child.pid;
11
+ if (pid == null)
12
+ return;
13
+ trackedChildPids.add(pid);
14
+ const untrack = () => {
15
+ trackedChildPids.delete(pid);
16
+ };
17
+ child.once('close', untrack);
18
+ child.once('error', untrack);
19
+ }
20
+ /**
21
+ * Kills the process trees of the still-running child processes registered
22
+ * with trackChildProcess(). Best-effort: children that exited concurrently
23
+ * are skipped silently.
24
+ *
25
+ * On Windows the tree is killed with `taskkill /T`, which terminates every
26
+ * descendant of a known PID without enumerating the system process list (an
27
+ * enumeration needs `wmic` or PowerShell there and can take tens of seconds).
28
+ * On POSIX only the tracked child itself is signalled; descendants are
29
+ * expected to be handled by the caller (the error handler enumerates them
30
+ * cheaply with one `ps` call).
31
+ */
32
+ export async function killTrackedProcessTrees() {
33
+ await Promise.all(Array.from(trackedChildPids, killProcessTree));
34
+ }
35
+ async function killProcessTree(pid) {
36
+ if (process.platform === 'win32') {
37
+ // Resolve taskkill to its absolute System32 location so a taskkill.exe
38
+ // planted in the current directory or on PATH can't be run in its place
39
+ // during error cleanup. `process.env` is case-insensitive on Windows, so
40
+ // `SystemRoot` also matches the SYSTEMROOT/systemroot spellings.
41
+ const taskkillPath = path.join(process.env.SystemRoot ?? process.env.windir ?? 'C:\\Windows', 'System32', 'taskkill.exe');
42
+ await new Promise((resolve) => {
43
+ const taskkill = spawn(taskkillPath, ['/pid', pid.toString(), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
44
+ // pnpm is on its error-exit path and must not hang, so the wait is
45
+ // bounded by a timer that resolves even if taskkill never emits 'exit'
46
+ // or 'error' (and reaps a stuck taskkill). The kill is best-effort
47
+ // either way. The timer is unref'd so it can't keep the process alive.
48
+ const timer = setTimeout(() => {
49
+ taskkill.kill();
50
+ resolve();
51
+ }, 10_000);
52
+ timer.unref();
53
+ const done = () => {
54
+ clearTimeout(timer);
55
+ resolve();
56
+ };
57
+ // A non-zero exit code (128 when the process is already gone, 1 when
58
+ // access is denied) is deliberately ignored.
59
+ taskkill.once('error', done);
60
+ taskkill.once('exit', done);
61
+ });
62
+ }
63
+ else {
64
+ try {
65
+ process.kill(pid);
66
+ }
67
+ catch {
68
+ // the process exited before it could be signalled
69
+ }
70
+ }
71
+ }
72
+ //# sourceMappingURL=trackChildProcess.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/exec.lifecycle",
3
- "version": "1100.1.7",
3
+ "version": "1100.1.8",
4
4
  "description": "Package lifecycle hook runner",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -29,15 +29,15 @@
29
29
  "!*.map"
30
30
  ],
31
31
  "dependencies": {
32
- "@pnpm/bins.linker": "1100.0.21",
33
- "@pnpm/core-loggers": "1100.2.4",
34
- "@pnpm/error": "1100.0.1",
35
- "@pnpm/fetching.directory-fetcher": "1100.0.24",
32
+ "@pnpm/bins.linker": "1100.0.22",
33
+ "@pnpm/core-loggers": "1100.2.5",
34
+ "@pnpm/error": "1100.1.0",
35
+ "@pnpm/fetching.directory-fetcher": "1100.0.25",
36
36
  "@pnpm/npm-lifecycle": "^1100.0.0",
37
- "@pnpm/pkg-manifest.reader": "1100.0.11",
38
- "@pnpm/store.cafs-types": "1100.0.1",
39
- "@pnpm/store.controller-types": "1100.1.9",
40
- "@pnpm/types": "1101.5.0",
37
+ "@pnpm/pkg-manifest.reader": "1100.0.12",
38
+ "@pnpm/store.cafs-types": "1100.0.2",
39
+ "@pnpm/store.controller-types": "1100.1.10",
40
+ "@pnpm/types": "1101.6.0",
41
41
  "chalk": "^5.6.2",
42
42
  "is-windows": "^1.0.2",
43
43
  "path-exists": "^5.0.0",
@@ -49,10 +49,10 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "@jest/globals": "30.4.1",
52
- "@pnpm/exec.lifecycle": "1100.1.7",
52
+ "@pnpm/exec.lifecycle": "1100.1.8",
53
53
  "@pnpm/logger": "1100.0.0",
54
- "@pnpm/prepare": "1100.0.21",
55
- "@pnpm/test-fixtures": "1100.0.0",
54
+ "@pnpm/prepare": "1100.0.22",
55
+ "@pnpm/test-fixtures": "1100.0.1",
56
56
  "@pnpm/test-ipc-server": "1100.0.0",
57
57
  "@types/is-windows": "^1.0.2",
58
58
  "@zkochan/rimraf": "^4.0.0",