@pnpm/exec.lifecycle 1001.0.25

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2016 Rico Sta. Cruz and other contributors
4
+ Copyright (c) 2016-2026 Zoltan Kochan and other contributors
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @pnpm/lifecycle
2
+
3
+ > Package lifecycle hook runner
4
+
5
+ <!--@shields('npm')-->
6
+ [![npm version](https://img.shields.io/npm/v/@pnpm/lifecycle.svg)](https://www.npmjs.com/package/@pnpm/lifecycle)
7
+ <!--/@-->
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ pnpm add @pnpm/logger @pnpm/lifecycle
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import runLifecycleHook, {runPostinstallHooks} from '@pnpm/lifecycle'
19
+
20
+ const targetPkgRoot = path.resolve('node_modules/target-pkg')
21
+ const pkg = require(path.join(targetPkgRoot, 'package.json'))
22
+
23
+ // Run a specific hook
24
+ await runLifecycleHook('preinstall', pkg, {
25
+ pkgId: 'target-pkg/1.0.0',
26
+ pkgRoot: targetPkgRoot,
27
+ rawConfig: {},
28
+ rootModulesDir: path.resolve('node_modules'),
29
+ unsafePerm: true,
30
+ })
31
+
32
+ // Run all install hooks
33
+ await runPostinstallHooks({
34
+ pkgId: 'target-pkg/1.0.0',
35
+ pkgRoot: targetPkgRoot,
36
+ rawConfig: {},
37
+ rootModulesDir: path.resolve('node_modules'),
38
+ unsafePerm: true,
39
+ })
40
+ ```
41
+
42
+ ## API
43
+
44
+ ### `runLifecycleHook(stage, packageManifest, opts): Promise<void>`
45
+
46
+ ### `runPostinstallHooks(opts): Promise<void>`
47
+
48
+ ## License
49
+
50
+ MIT
package/lib/index.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { runLifecycleHook, type RunLifecycleHookOptions } from './runLifecycleHook.js';
2
+ import { runLifecycleHooksConcurrently, type RunLifecycleHooksConcurrentlyOptions } from './runLifecycleHooksConcurrently.js';
3
+ export declare function makeNodeRequireOption(modulePath: string): {
4
+ NODE_OPTIONS: string;
5
+ };
6
+ export { runLifecycleHook, type RunLifecycleHookOptions, runLifecycleHooksConcurrently, type RunLifecycleHooksConcurrentlyOptions, };
7
+ export declare function runPostinstallHooks(opts: RunLifecycleHookOptions): Promise<boolean>;
package/lib/index.js ADDED
@@ -0,0 +1,28 @@
1
+ import { safeReadPackageJsonFromDir } from '@pnpm/pkg-manifest.reader';
2
+ import { runLifecycleHook } from './runLifecycleHook.js';
3
+ import { runLifecycleHooksConcurrently } from './runLifecycleHooksConcurrently.js';
4
+ export function makeNodeRequireOption(modulePath) {
5
+ let { NODE_OPTIONS } = process.env;
6
+ NODE_OPTIONS = `${NODE_OPTIONS ?? ''} --require=${modulePath}`.trim();
7
+ return { NODE_OPTIONS };
8
+ }
9
+ export { runLifecycleHook, runLifecycleHooksConcurrently, };
10
+ export async function runPostinstallHooks(opts) {
11
+ const pkg = await safeReadPackageJsonFromDir(opts.pkgRoot);
12
+ if (pkg == null)
13
+ return false;
14
+ if (pkg.scripts == null) {
15
+ pkg.scripts = {};
16
+ }
17
+ if (pkg.scripts.preinstall) {
18
+ await runLifecycleHook('preinstall', pkg, opts);
19
+ }
20
+ const executedAnInstallScript = await runLifecycleHook('install', pkg, opts);
21
+ if (pkg.scripts.postinstall) {
22
+ await runLifecycleHook('postinstall', pkg, opts);
23
+ }
24
+ return pkg.scripts.preinstall != null ||
25
+ executedAnInstallScript ||
26
+ pkg.scripts.postinstall != null;
27
+ }
28
+ //# sourceMappingURL=index.js.map
@@ -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
+ rawConfig: object;
11
+ rootModulesDir: string;
12
+ scriptShell?: string;
13
+ silent?: boolean;
14
+ scriptsPrependNodePath?: boolean | 'warn-only';
15
+ shellEmulator?: boolean;
16
+ stdio?: string;
17
+ unsafePerm: boolean;
18
+ }
19
+ export declare function runLifecycleHook(stage: string, manifest: ProjectManifest | DependencyManifest, opts: RunLifecycleHookOptions): Promise<boolean>;
@@ -0,0 +1,166 @@
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 isWindows from 'is-windows';
8
+ import { join as shellQuote } from 'shlex';
9
+ function noop() { } // eslint-disable-line:no-empty
10
+ export async function runLifecycleHook(stage, manifest, opts) {
11
+ const optional = opts.optional === true;
12
+ // To remediate CVE_2024_27980, Node.js does not allow .bat or .cmd files to
13
+ // be spawned without the "shell: true" option.
14
+ //
15
+ // https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
16
+ //
17
+ // Unfortunately, setting spawn's shell option also causes arguments to be
18
+ // evaluated before they're passed to the shell, resulting in a surprising
19
+ // behavior difference only with .bat/.cmd files.
20
+ //
21
+ // Instead of showing a "spawn EINVAL" error, let's throw a clearer error that
22
+ // this isn't supported.
23
+ //
24
+ // If this behavior needs to be supported in the future, the arguments would
25
+ // need to be escaped before they're passed to the .bat/.cmd file. For
26
+ // example, scripts such as "echo %PATH%" should be passed verbatim rather
27
+ // than expanded. This is difficult to do correctly. Other open source tools
28
+ // (e.g. Rust) attempted and introduced bugs. The Rust blog has a good
29
+ // high-level explanation of the same security vulnerability Node.js patched.
30
+ //
31
+ // https://blog.rust-lang.org/2024/04/09/cve-2024-24576.html#overview
32
+ //
33
+ // Note that npm (as of version 10.5.0) doesn't support setting script-shell
34
+ // to a .bat or .cmd file either.
35
+ if (opts.scriptShell != null && typeof opts.scriptShell === 'string' && isWindowsBatchFile(opts.scriptShell)) {
36
+ throw new PnpmError('ERR_PNPM_INVALID_SCRIPT_SHELL_WINDOWS', 'Cannot spawn .bat or .cmd as a script shell.', {
37
+ hint: `\
38
+ The pnpm-workspace.yaml scriptShell option was configured to a .bat or .cmd file. These cannot be used as a script shell reliably.
39
+
40
+ Please unset the scriptShell option, or configure it to a .exe instead.
41
+ `,
42
+ });
43
+ }
44
+ const m = { _id: getId(manifest), ...manifest };
45
+ m.scripts = { ...m.scripts };
46
+ switch (stage) {
47
+ case 'start':
48
+ if (!m.scripts.start) {
49
+ if (!existsSync('server.js')) {
50
+ throw new PnpmError('NO_SCRIPT_OR_SERVER', 'Missing script start or file server.js');
51
+ }
52
+ m.scripts.start = 'node server.js';
53
+ }
54
+ break;
55
+ case 'install':
56
+ if (!m.scripts.install && !m.scripts.preinstall) {
57
+ checkBindingGyp(opts.pkgRoot, m.scripts);
58
+ }
59
+ break;
60
+ }
61
+ if (opts.args?.length && m.scripts?.[stage]) {
62
+ // It is impossible to quote a command line argument that contains newline for Windows cmd.
63
+ const escapedArgs = isWindows()
64
+ ? opts.args.map((arg) => JSON.stringify(arg)).join(' ')
65
+ : shellQuote(opts.args);
66
+ m.scripts[stage] = `${m.scripts[stage]} ${escapedArgs}`;
67
+ }
68
+ // This script is used to prevent the usage of npm or Yarn.
69
+ // It does nothing, when pnpm is used, so we may skip its execution.
70
+ if (m.scripts[stage] === 'npx only-allow pnpm' || !m.scripts[stage])
71
+ return false;
72
+ if (opts.stdio !== 'inherit') {
73
+ lifecycleLogger.debug({
74
+ depPath: opts.depPath,
75
+ optional,
76
+ script: m.scripts[stage],
77
+ stage,
78
+ wd: opts.pkgRoot,
79
+ });
80
+ }
81
+ const logLevel = (opts.stdio !== 'inherit' || opts.silent)
82
+ ? 'silent'
83
+ : undefined;
84
+ await lifecycle(m, stage, opts.pkgRoot, {
85
+ config: {
86
+ ...opts.rawConfig,
87
+ 'frozen-lockfile': false,
88
+ },
89
+ dir: opts.rootModulesDir,
90
+ extraBinPaths: opts.extraBinPaths,
91
+ extraEnv: {
92
+ ...opts.extraEnv,
93
+ INIT_CWD: opts.initCwd ?? process.cwd(),
94
+ PNPM_SCRIPT_SRC_DIR: opts.pkgRoot,
95
+ },
96
+ log: {
97
+ clearProgress: noop,
98
+ info: noop,
99
+ level: logLevel,
100
+ pause: noop,
101
+ resume: noop,
102
+ showProgress: noop,
103
+ silly: npmLog,
104
+ verbose: npmLog,
105
+ warn: (...msg) => {
106
+ globalWarn(msg.join(' '));
107
+ },
108
+ },
109
+ runConcurrently: true,
110
+ scriptsPrependNodePath: opts.scriptsPrependNodePath,
111
+ scriptShell: opts.scriptShell,
112
+ shellEmulator: opts.shellEmulator,
113
+ stdio: opts.stdio ?? 'pipe',
114
+ unsafePerm: opts.unsafePerm,
115
+ });
116
+ return true;
117
+ function npmLog(prefix, logId, stdtype, line) {
118
+ switch (stdtype) {
119
+ case 'stdout':
120
+ case 'stderr':
121
+ lifecycleLogger.debug({
122
+ depPath: opts.depPath,
123
+ line: (line ?? 0).toString(),
124
+ stage,
125
+ stdio: stdtype,
126
+ wd: opts.pkgRoot,
127
+ });
128
+ return;
129
+ case 'Returned: code:': {
130
+ if (opts.stdio === 'inherit') {
131
+ // Preventing the pnpm reporter from overriding the project's script output
132
+ return;
133
+ }
134
+ const code = line ?? 1;
135
+ lifecycleLogger.debug({
136
+ depPath: opts.depPath,
137
+ exitCode: code,
138
+ optional,
139
+ stage,
140
+ wd: opts.pkgRoot,
141
+ });
142
+ }
143
+ }
144
+ }
145
+ }
146
+ /**
147
+ * Run node-gyp when binding.gyp is available. Only do this when there are no
148
+ * `install` and `preinstall` scripts (see `npm help scripts`).
149
+ */
150
+ function checkBindingGyp(root, scripts) {
151
+ if (existsSync(path.join(root, 'binding.gyp'))) {
152
+ scripts.install = 'node-gyp rebuild';
153
+ }
154
+ }
155
+ function getId(manifest) {
156
+ return `${manifest.name ?? ''}@${manifest.version ?? ''}`;
157
+ }
158
+ function isWindowsBatchFile(scriptShell) {
159
+ // Node.js performs a similar check to determine whether it should throw
160
+ // EINVAL when spawning a .cmd/.bat file.
161
+ //
162
+ // https://github.com/nodejs/node/commit/6627222409#diff-1e725bfa950eda4d4b5c0c00a2bb6be3e5b83d819872a1adf2ef87c658273903
163
+ const scriptShellLower = scriptShell.toLowerCase();
164
+ return isWindows() && (scriptShellLower.endsWith('.cmd') || scriptShellLower.endsWith('.bat'));
165
+ }
166
+ //# 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,84 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { linkBins } from '@pnpm/bins.linker';
4
+ import { fetchFromDir } from '@pnpm/fetching.directory-fetcher';
5
+ import { logger } from '@pnpm/logger';
6
+ import { runGroups } from 'run-groups';
7
+ import { runLifecycleHook } from './runLifecycleHook.js';
8
+ export async function runLifecycleHooksConcurrently(stages, importers, childConcurrency, opts) {
9
+ const importersByBuildIndex = new Map();
10
+ for (const importer of importers) {
11
+ if (!importersByBuildIndex.has(importer.buildIndex)) {
12
+ importersByBuildIndex.set(importer.buildIndex, [importer]);
13
+ }
14
+ else {
15
+ importersByBuildIndex.get(importer.buildIndex).push(importer);
16
+ }
17
+ }
18
+ const sortedBuildIndexes = Array.from(importersByBuildIndex.keys()).sort((a, b) => a - b);
19
+ const groups = sortedBuildIndexes.map((buildIndex) => {
20
+ const importers = importersByBuildIndex.get(buildIndex);
21
+ return importers.map(({ manifest, modulesDir, rootDir, stages: importerStages, targetDirs }) => async () => {
22
+ // We are linking the bin files, in case they were created by lifecycle scripts of other workspace packages.
23
+ await linkBins(modulesDir, path.join(modulesDir, '.bin'), {
24
+ extraNodePaths: opts.extraNodePaths,
25
+ allowExoticManifests: true,
26
+ preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
27
+ projectManifest: manifest,
28
+ warn: (message) => {
29
+ logger.warn({ message, prefix: rootDir });
30
+ },
31
+ });
32
+ const runLifecycleHookOpts = {
33
+ ...opts,
34
+ depPath: rootDir,
35
+ pkgRoot: rootDir,
36
+ rootModulesDir: modulesDir,
37
+ };
38
+ let isBuilt = false;
39
+ for (const stage of (importerStages ?? stages)) {
40
+ if (await runLifecycleHook(stage, manifest, runLifecycleHookOpts)) { // eslint-disable-line no-await-in-loop
41
+ isBuilt = true;
42
+ }
43
+ }
44
+ if (targetDirs == null || targetDirs.length === 0 || !isBuilt)
45
+ return;
46
+ const filesResponse = await fetchFromDir(rootDir, { resolveSymlinks: opts.resolveSymlinksInInjectedDirs });
47
+ await Promise.all(targetDirs.map(async (targetDir) => {
48
+ const targetModulesDir = path.join(targetDir, 'node_modules');
49
+ const newFilesMap = new Map(filesResponse.filesMap);
50
+ if (fs.existsSync(targetModulesDir)) {
51
+ // If the target directory contains a node_modules directory
52
+ // (it may happen when the hoisted node linker is used)
53
+ // then we need to preserve this node_modules.
54
+ // So we scan this node_modules directory and pass it as part of the new package.
55
+ await scanDir('node_modules', targetModulesDir, targetModulesDir, newFilesMap);
56
+ }
57
+ return opts.storeController.importPackage(targetDir, {
58
+ filesResponse: {
59
+ resolvedFrom: 'local-dir',
60
+ ...filesResponse,
61
+ filesMap: newFilesMap,
62
+ },
63
+ force: false,
64
+ });
65
+ }));
66
+ });
67
+ });
68
+ await runGroups(childConcurrency, groups);
69
+ }
70
+ async function scanDir(prefix, rootDir, currentDir, index) {
71
+ const files = await fs.promises.readdir(currentDir);
72
+ await Promise.all(files.map(async (file) => {
73
+ const fullPath = path.join(currentDir, file);
74
+ const stat = await fs.promises.stat(fullPath);
75
+ if (stat.isDirectory()) {
76
+ return scanDir(prefix, rootDir, fullPath, index);
77
+ }
78
+ if (stat.isFile()) {
79
+ const relativePath = path.relative(rootDir, fullPath);
80
+ index.set(path.join(prefix, relativePath), fullPath);
81
+ }
82
+ }));
83
+ }
84
+ //# sourceMappingURL=runLifecycleHooksConcurrently.js.map
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@pnpm/exec.lifecycle",
3
+ "version": "1001.0.25",
4
+ "description": "Package lifecycle hook runner",
5
+ "keywords": [
6
+ "pnpm",
7
+ "pnpm11",
8
+ "lifecycle",
9
+ "scripts"
10
+ ],
11
+ "license": "MIT",
12
+ "funding": "https://opencollective.com/pnpm",
13
+ "repository": "https://github.com/pnpm/pnpm/tree/main/exec/lifecycle",
14
+ "homepage": "https://github.com/pnpm/pnpm/tree/main/exec/lifecycle#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/pnpm/pnpm/issues"
17
+ },
18
+ "type": "module",
19
+ "main": "lib/index.js",
20
+ "types": "lib/index.d.ts",
21
+ "exports": {
22
+ ".": "./lib/index.js"
23
+ },
24
+ "files": [
25
+ "lib",
26
+ "!*.map"
27
+ ],
28
+ "dependencies": {
29
+ "@pnpm/npm-lifecycle": "^1001.0.0",
30
+ "is-windows": "^1.0.2",
31
+ "path-exists": "^5.0.0",
32
+ "run-groups": "^5.0.0",
33
+ "shlex": "^3.0.0",
34
+ "@pnpm/core-loggers": "1001.0.4",
35
+ "@pnpm/bins.linker": "1000.2.6",
36
+ "@pnpm/fetching.directory-fetcher": "1000.1.14",
37
+ "@pnpm/error": "1000.0.5",
38
+ "@pnpm/store.cafs-types": "1000.0.0",
39
+ "@pnpm/store.controller-types": "1004.1.0",
40
+ "@pnpm/types": "1000.9.0",
41
+ "@pnpm/pkg-manifest.reader": "1000.1.2"
42
+ },
43
+ "peerDependencies": {
44
+ "@pnpm/logger": ">=1001.0.0 <1002.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/is-windows": "^1.0.2",
48
+ "@zkochan/rimraf": "^4.0.0",
49
+ "load-json-file": "^7.0.1",
50
+ "@pnpm/prepare": "1000.0.4",
51
+ "@pnpm/exec.lifecycle": "1001.0.25",
52
+ "@pnpm/test-fixtures": "1000.0.0",
53
+ "@pnpm/test-ipc-server": "1000.0.0",
54
+ "@pnpm/logger": "1001.0.1"
55
+ },
56
+ "engines": {
57
+ "node": ">=22.13"
58
+ },
59
+ "jest": {
60
+ "preset": "@pnpm/jest-config"
61
+ },
62
+ "scripts": {
63
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
64
+ "_test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest",
65
+ "test": "pnpm run compile && pnpm run _test",
66
+ "fix": "tslint -c tslint.json src/**/*.ts test/**/*.ts --fix",
67
+ "compile": "tsgo --build && pnpm run lint --fix"
68
+ }
69
+ }