@vltpkg/run 1.0.0-rc.23 → 1.0.0-rc.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.
@@ -0,0 +1,130 @@
1
+ import { PackageJson } from '@vltpkg/package-json';
2
+ import type { PromiseSpawnOptions, SpawnResultNoStdio, SpawnResultStdioStrings } from '@vltpkg/promise-spawn';
3
+ import type { Manifest } from '@vltpkg/types';
4
+ export * from './node-gyp.ts';
5
+ /** options shared by run() and exec() */
6
+ export type SharedOptions = PromiseSpawnOptions & {
7
+ /** additional arguments to pass to the command */
8
+ args?: string[];
9
+ /** the root of the project, which we don't walk up past */
10
+ projectRoot: string;
11
+ /** the directory where the package.json lives and the script runs */
12
+ cwd: string;
13
+ /**
14
+ * environment variables to set. `process.env` is always included,
15
+ * to omit fields, set them explicitly to undefined.
16
+ *
17
+ * vlt will add some of its own, as well:
18
+ * - npm_lifecycle_event: the event name
19
+ * - npm_lifecycle_script: the command in package.json#scripts
20
+ * - npm_package_json: path to the package.json file
21
+ * - VLT_* envs for all vlt configuration values that are set
22
+ */
23
+ env?: NodeJS.ProcessEnv;
24
+ /**
25
+ * the shell to run the script in. If not set, then the default
26
+ * platform-specific shell will be used.
27
+ */
28
+ 'script-shell'?: boolean | string;
29
+ /**
30
+ * If true, then `FORCE_COLOR=1` will be set in the environment so that
31
+ * scripts will typically have colored output enablled, even if the output
32
+ * is not a tty.
33
+ */
34
+ color?: boolean;
35
+ };
36
+ /**
37
+ * Options for run() and runFG()
38
+ */
39
+ export type RunOptions = SharedOptions & {
40
+ /** the name of the thing in package.json#scripts */
41
+ arg0: string;
42
+ /**
43
+ * pass in a @vltpkg/package-json.PackageJson instance, and
44
+ * it'll be used for reading the package.json file. Optional,
45
+ * may improve performance somewhat.
46
+ */
47
+ packageJson?: PackageJson;
48
+ /**
49
+ * Pass in a manifest to avoid having to read it at all
50
+ */
51
+ manifest?: Pick<Manifest, 'scripts' | 'gypfile'>;
52
+ /**
53
+ * if the script is not defined in package.json#scripts, just ignore it and
54
+ * treat as success. Otherwise, treat as an error. Default false.
55
+ */
56
+ ignoreMissing?: boolean;
57
+ /**
58
+ * skip the pre/post commands, just run the one specified.
59
+ * This can be used to run JUST the specified script, without any
60
+ * pre/post commands.
61
+ */
62
+ ignorePrePost?: boolean;
63
+ };
64
+ /**
65
+ * Options for exec() and execFG()
66
+ */
67
+ export type ExecOptions = SharedOptions & {
68
+ /** the command to execute */
69
+ arg0: string;
70
+ };
71
+ /**
72
+ * Options for runExec() and runExecFG()
73
+ */
74
+ export type RunExecOptions = SharedOptions & {
75
+ /**
76
+ * Either the command to be executed, or the event to be run
77
+ */
78
+ arg0: string;
79
+ /**
80
+ * pass in a @vltpkg/package-json.PackageJson instance, and
81
+ * it'll be used for reading the package.json file. Optional,
82
+ * may improve performance somewhat.
83
+ */
84
+ packageJson?: PackageJson;
85
+ };
86
+ /**
87
+ * Run a package.json#scripts event in the background
88
+ */
89
+ export declare const run: (options: RunOptions) => Promise<RunResult>;
90
+ /**
91
+ * Run a package.json#scripts event in the foreground
92
+ */
93
+ export declare const runFG: (options: RunOptions) => Promise<RunFGResult>;
94
+ /** Return type of {@link run} */
95
+ export type RunResult = SpawnResultStdioStrings & {
96
+ pre?: SpawnResultStdioStrings;
97
+ post?: SpawnResultStdioStrings;
98
+ };
99
+ /** Return type of {@link runFG} */
100
+ export type RunFGResult = SpawnResultNoStdio & {
101
+ pre?: SpawnResultNoStdio;
102
+ post?: SpawnResultNoStdio;
103
+ };
104
+ export declare const isRunResult: (v: unknown) => v is RunResult;
105
+ /**
106
+ * Return type of {@link run} or {@link runFG}, as determined by their base
107
+ * type
108
+ * @internal
109
+ */
110
+ export type RunImplResult<R extends SpawnResultNoStdio | SpawnResultStdioStrings> = R & {
111
+ pre?: R;
112
+ post?: R;
113
+ };
114
+ /**
115
+ * Execute an arbitrary command in the background
116
+ */
117
+ export declare const exec: (options: ExecOptions) => Promise<SpawnResultStdioStrings>;
118
+ /**
119
+ * Execute an arbitrary command in the foreground
120
+ */
121
+ export declare const execFG: (options: ExecOptions) => Promise<SpawnResultNoStdio>;
122
+ /**
123
+ * If the arg0 is a defined package.json script, then run(), otherwise exec()
124
+ */
125
+ export declare const runExec: (options: RunExecOptions) => Promise<RunResult | SpawnResultStdioStrings>;
126
+ /**
127
+ * If the arg0 is a defined package.json script, then runFG(), otherwise
128
+ * execFG()
129
+ */
130
+ export declare const runExecFG: (options: RunExecOptions) => Promise<RunFGResult | SpawnResultNoStdio>;
package/dist/index.js ADDED
@@ -0,0 +1,287 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { PackageJson } from '@vltpkg/package-json';
3
+ import { promiseSpawn } from '@vltpkg/promise-spawn';
4
+ import { foregroundChild } from 'foreground-child';
5
+ import { proxySignals } from 'foreground-child/proxy-signals';
6
+ import { statSync } from 'node:fs';
7
+ import { delimiter, resolve, sep } from 'node:path';
8
+ import { walkUp } from 'walk-up-path';
9
+ import { getNodeGypShimDir, hasNodeGypReference, hasNodeGypBinding, } from "./node-gyp.js";
10
+ // Re-export all named exports from node-gyp.ts
11
+ export * from "./node-gyp.js";
12
+ /** map of which node_modules/.bin folders exist */
13
+ const dotBins = new Map();
14
+ /** Check if a directory exists, and cache result */
15
+ const dirExists = (p) => {
16
+ const cached = dotBins.get(p);
17
+ if (cached !== undefined)
18
+ return cached;
19
+ try {
20
+ const isDir = statSync(p).isDirectory();
21
+ dotBins.set(p, isDir);
22
+ return isDir;
23
+ }
24
+ catch {
25
+ dotBins.set(p, false);
26
+ return false;
27
+ }
28
+ };
29
+ const nmBin = `${sep}node_modules${sep}.bin`;
30
+ /**
31
+ * Add all existing `node_modules/.bin` folders to the PATH that
32
+ * exist between the cwd and the projectRoot, so dependency bins
33
+ * are found, with closer paths higher priority.
34
+ *
35
+ * If command contains node-gyp reference, also inject the shim directory
36
+ * as a fallback (after all existing PATH entries).
37
+ */
38
+ const addPaths = async (projectRoot, cwd, env, command) => {
39
+ const { PATH = '' } = env;
40
+ const PATHsplit = PATH.split(delimiter);
41
+ const paths = new Set();
42
+ // anything in the PATH that already has node_modules/.bin is a thing
43
+ // we put there, perhaps for the vlx exec cache usage
44
+ for (const p of PATHsplit) {
45
+ if (p.endsWith(nmBin)) {
46
+ paths.add(p);
47
+ }
48
+ }
49
+ for (const p of walkUp(cwd)) {
50
+ const dotBin = resolve(p, 'node_modules/.bin');
51
+ if (dirExists(dotBin))
52
+ paths.add(dotBin);
53
+ if (p === projectRoot)
54
+ break;
55
+ }
56
+ for (const p of PATH.split(delimiter)) {
57
+ /* c8 ignore next - pretty rare to have an empty entry */
58
+ if (p)
59
+ paths.add(p);
60
+ }
61
+ // If command has node-gyp, inject shim directory as fallback (last)
62
+ if (command && hasNodeGypReference(command)) {
63
+ try {
64
+ const shimDir = await getNodeGypShimDir();
65
+ paths.add(shimDir);
66
+ /* c8 ignore start */
67
+ }
68
+ catch {
69
+ // Ignore shim creation errors, command will fail naturally if node-gyp is needed
70
+ }
71
+ /* c8 ignore stop */
72
+ }
73
+ env.PATH = [...paths].join(delimiter);
74
+ return env;
75
+ };
76
+ /**
77
+ * Run a package.json#scripts event in the background
78
+ */
79
+ export const run = async (options) => runImpl(options, exec, '');
80
+ /**
81
+ * Run a package.json#scripts event in the foreground
82
+ */
83
+ export const runFG = async (options) => runImpl(options, execFG, null);
84
+ export const isRunResult = (v) => !!v &&
85
+ typeof v === 'object' &&
86
+ !Array.isArray(v) &&
87
+ 'stdout' in v &&
88
+ 'stderr' in v &&
89
+ 'status' in v &&
90
+ 'signal' in v;
91
+ /**
92
+ * Internal implementation of run() and runFG(), since they're mostly identical
93
+ */
94
+ const runImpl = async (options, execImpl, empty) => {
95
+ const { arg0, packageJson = new PackageJson(), ignoreMissing = false, manifest, 'script-shell': shell = true, ...execArgs } = options;
96
+ const pjPath = resolve(options.cwd, 'package.json');
97
+ // npm adds a `"install": "node-gyp rebuild"` if a binding.gyp
98
+ // is present at the time of publish, EVEN IF it's not included
99
+ // in the package. So, we need to read the actual package.json
100
+ // in those cases.
101
+ const untrustworthy = !!(manifest?.gypfile &&
102
+ arg0 === 'install' &&
103
+ manifest.scripts?.install === 'node-gyp rebuild');
104
+ const pj = (untrustworthy ? undefined : manifest) ??
105
+ packageJson.read(options.cwd);
106
+ const { scripts } = pj;
107
+ const command = scripts?.[arg0] ??
108
+ // npm's implicit install behavior: "If there is a binding.gyp file in the
109
+ // root of your package and you haven't defined your own install or preinstall
110
+ // scripts, npm will default the install command to compile using node-gyp
111
+ // via node-gyp rebuild"
112
+ ((arg0 === 'install' &&
113
+ !scripts?.install &&
114
+ !scripts?.preinstall &&
115
+ hasNodeGypBinding(options.cwd)) ?
116
+ 'node-gyp rebuild'
117
+ : undefined);
118
+ // Check for pre/post commands even if main command doesn't exist for cases
119
+ // like packages that have only a preinstall or postinstall script.
120
+ const precommand = !options.ignorePrePost && scripts?.[`pre${arg0}`];
121
+ const postcommand = !options.ignorePrePost && scripts?.[`post${arg0}`];
122
+ const emptyResult = () => ({
123
+ command: '',
124
+ args: execArgs.args ?? [],
125
+ cwd: options.cwd,
126
+ status: 0,
127
+ signal: null,
128
+ stdout: empty,
129
+ stderr: empty,
130
+ });
131
+ const execCommand = (command, preOrPost) => execImpl({
132
+ arg0: command,
133
+ ...execArgs,
134
+ 'script-shell': shell,
135
+ // pre/post scripts always have empty args, main script uses execArgs.args
136
+ args: preOrPost ? [] : (execArgs.args ?? []),
137
+ env: {
138
+ ...execArgs.env,
139
+ npm_package_json: pjPath,
140
+ npm_lifecycle_event: `${preOrPost ?? ''}${arg0}`,
141
+ npm_lifecycle_script: command,
142
+ },
143
+ });
144
+ if (!command && !precommand && !postcommand) {
145
+ if (ignoreMissing) {
146
+ return emptyResult();
147
+ }
148
+ throw error('Script not defined in package.json', {
149
+ name: arg0,
150
+ cwd: options.cwd,
151
+ args: options.args,
152
+ path: pjPath,
153
+ manifest: pj,
154
+ });
155
+ }
156
+ const pre = precommand ? await execCommand(precommand, 'pre') : undefined;
157
+ if (pre?.status || pre?.signal) {
158
+ return {
159
+ ...pre,
160
+ };
161
+ }
162
+ const main = command ? await execCommand(command) : emptyResult();
163
+ if (main.signal || main.status) {
164
+ return {
165
+ ...main,
166
+ ...(pre ? { pre } : {}),
167
+ };
168
+ }
169
+ const post = postcommand ? await execCommand(postcommand, 'post') : undefined;
170
+ if (post?.signal || post?.status) {
171
+ return {
172
+ ...main,
173
+ ...(pre ? { pre } : {}),
174
+ post,
175
+ signal: post.signal,
176
+ status: post.status,
177
+ };
178
+ }
179
+ return {
180
+ ...main,
181
+ ...(pre ? { pre } : {}),
182
+ ...(post ? { post } : {}),
183
+ };
184
+ };
185
+ /**
186
+ * Escapes a string for safe inclusion in a POSIX shell command.
187
+ * Wraps in single quotes and escapes any embedded single quotes.
188
+ */
189
+ const shellEscape = (s) => `'${s.replace(/'/g, "'\\''")}'`;
190
+ const isPosixShell = (shell) => {
191
+ if (typeof shell === 'string')
192
+ return !/(^|\\)cmd\.exe$/i.test(shell);
193
+ return process.platform !== 'win32';
194
+ };
195
+ /**
196
+ * When running through a POSIX shell, Node.js joins `spawn(cmd, args)`
197
+ * into a single string without escaping (DEP0190). This means
198
+ * characters like `#` are interpreted as shell comments, silently
199
+ * swallowing the rest of the command line.
200
+ *
201
+ * Fix: append shell-escaped args directly to the command string
202
+ * so the script is still shell-interpreted but args are passed
203
+ * literally.
204
+ *
205
+ * On Windows with cmd.exe, these characters are not special, so
206
+ * we leave args as-is to avoid injecting POSIX quotes that cmd.exe
207
+ * would pass through literally.
208
+ */
209
+ const withShellArgs = (shell, arg0, args) => {
210
+ if (!shell || args.length === 0 || !isPosixShell(shell))
211
+ return [arg0, args];
212
+ return [`${arg0} ${args.map(shellEscape).join(' ')}`, []];
213
+ };
214
+ /**
215
+ * Execute an arbitrary command in the background
216
+ */
217
+ export const exec = async (options) => {
218
+ const { arg0, args = [], cwd, env = {}, projectRoot, 'script-shell': shell = false, color = false, signal, ...spawnOptions } = options;
219
+ const [spawnCmd, spawnArgs] = withShellArgs(shell, arg0, args);
220
+ const p = promiseSpawn(spawnCmd, spawnArgs, {
221
+ ...spawnOptions,
222
+ shell,
223
+ stdio: 'pipe',
224
+ stdioString: true,
225
+ cwd,
226
+ env: await addPaths(projectRoot, cwd, {
227
+ ...process.env,
228
+ ...env,
229
+ FORCE_COLOR: color ? '1' : '0',
230
+ }, arg0),
231
+ windowsHide: true,
232
+ });
233
+ proxySignals(p.process);
234
+ return await p;
235
+ };
236
+ /**
237
+ * Execute an arbitrary command in the foreground
238
+ */
239
+ export const execFG = async (options) => {
240
+ const { arg0, args = [], cwd, projectRoot, env = {}, 'script-shell': shell = false, color = true, signal, ...spawnOptions } = options;
241
+ const [spawnCmd, spawnArgs] = withShellArgs(shell, arg0, args);
242
+ const processEnv = await addPaths(projectRoot, cwd, {
243
+ ...process.env,
244
+ ...env,
245
+ FORCE_COLOR: color ? '1' : '0',
246
+ }, arg0);
247
+ return new Promise(res => {
248
+ foregroundChild(spawnCmd, spawnArgs, {
249
+ ...spawnOptions,
250
+ shell,
251
+ cwd,
252
+ env: processEnv,
253
+ }, (status, signal) => {
254
+ res({
255
+ command: arg0,
256
+ args,
257
+ cwd,
258
+ stdout: null,
259
+ stderr: null,
260
+ status,
261
+ signal,
262
+ });
263
+ return false;
264
+ });
265
+ });
266
+ };
267
+ const runExecImpl = async (options, runImpl, execImpl) => {
268
+ const { arg0, packageJson = new PackageJson(), ...args } = options;
269
+ const pj = packageJson.read(options.cwd);
270
+ const { scripts } = pj;
271
+ const command = scripts?.[arg0];
272
+ if (command) {
273
+ return runImpl({ ...args, packageJson, arg0 });
274
+ }
275
+ else {
276
+ return execImpl({ ...args, arg0 });
277
+ }
278
+ };
279
+ /**
280
+ * If the arg0 is a defined package.json script, then run(), otherwise exec()
281
+ */
282
+ export const runExec = async (options) => runExecImpl(options, run, exec);
283
+ /**
284
+ * If the arg0 is a defined package.json script, then runFG(), otherwise
285
+ * execFG()
286
+ */
287
+ export const runExecFG = async (options) => runExecImpl(options, runFG, execFG);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Get or create the node-gyp shim file path
3
+ * The shim redirects node-gyp calls to vlx node-gyp@latest
4
+ */
5
+ export declare function getNodeGypShim(): Promise<string>;
6
+ /**
7
+ * Get the directory containing the node-gyp shim
8
+ * This can be prepended to PATH to make the shim available
9
+ */
10
+ export declare function getNodeGypShimDir(): Promise<string>;
11
+ /**
12
+ * Check if a command contains node-gyp references
13
+ */
14
+ export declare function hasNodeGypReference(command: string): boolean;
15
+ /**
16
+ * Check if a binding.gyp file exists
17
+ */
18
+ export declare function hasNodeGypBinding(cwd: string): boolean;
@@ -0,0 +1,68 @@
1
+ import { XDG } from '@vltpkg/xdg';
2
+ import { statSync } from 'node:fs';
3
+ import { chmod, mkdir, stat, writeFile } from 'node:fs/promises';
4
+ import { dirname, join } from 'node:path';
5
+ const xdg = new XDG('vlt');
6
+ let shimPath;
7
+ /**
8
+ * Get or create the node-gyp shim file path
9
+ * The shim redirects node-gyp calls to vlx node-gyp@latest
10
+ */
11
+ export async function getNodeGypShim() {
12
+ if (shimPath)
13
+ return shimPath;
14
+ const runtimeDir = xdg.runtime('run');
15
+ const shimFile = join(runtimeDir, 'node-gyp');
16
+ // Check if shim already exists
17
+ try {
18
+ await stat(shimFile);
19
+ /* c8 ignore next 2 - hard to test */
20
+ shimPath = shimFile;
21
+ return shimPath;
22
+ }
23
+ catch {
24
+ // Shim doesn't exist, create it
25
+ }
26
+ // Create runtime directory if needed
27
+ await mkdir(runtimeDir, { recursive: true });
28
+ // Create shim that calls vlx
29
+ /* c8 ignore start - ignore platform-dependent coverage */
30
+ const shimContent = process.platform === 'win32' ?
31
+ `@echo off\nvlx --yes node-gyp@latest %*\n`
32
+ : `#!/bin/sh\nexec vlx --yes node-gyp@latest "$@"\n`;
33
+ /* c8 ignore stop */
34
+ await writeFile(shimFile, shimContent, 'utf8');
35
+ // Make executable on Unix systems
36
+ /* c8 ignore start - unix-only */
37
+ if (process.platform !== 'win32') {
38
+ await chmod(shimFile, 0o755);
39
+ }
40
+ /* c8 ignore stop */
41
+ shimPath = shimFile;
42
+ return shimPath;
43
+ }
44
+ /**
45
+ * Get the directory containing the node-gyp shim
46
+ * This can be prepended to PATH to make the shim available
47
+ */
48
+ export async function getNodeGypShimDir() {
49
+ const shim = await getNodeGypShim();
50
+ return dirname(shim);
51
+ }
52
+ /**
53
+ * Check if a command contains node-gyp references
54
+ */
55
+ export function hasNodeGypReference(command) {
56
+ return command.includes('node-gyp');
57
+ }
58
+ /**
59
+ * Check if a binding.gyp file exists
60
+ */
61
+ export function hasNodeGypBinding(cwd) {
62
+ try {
63
+ return statSync(join(cwd, 'binding.gyp')).isFile();
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/run",
3
3
  "description": "Run package.json scripts and execute commands",
4
- "version": "1.0.0-rc.23",
4
+ "version": "1.0.0-rc.25",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -12,11 +12,11 @@
12
12
  "email": "support@vlt.sh"
13
13
  },
14
14
  "dependencies": {
15
- "@vltpkg/error-cause": "1.0.0-rc.23",
16
- "@vltpkg/package-json": "1.0.0-rc.23",
17
- "@vltpkg/promise-spawn": "1.0.0-rc.23",
18
- "@vltpkg/types": "1.0.0-rc.23",
19
- "@vltpkg/xdg": "1.0.0-rc.23",
15
+ "@vltpkg/error-cause": "1.0.0-rc.25",
16
+ "@vltpkg/package-json": "1.0.0-rc.25",
17
+ "@vltpkg/promise-spawn": "1.0.0-rc.25",
18
+ "@vltpkg/types": "1.0.0-rc.25",
19
+ "@vltpkg/xdg": "1.0.0-rc.25",
20
20
  "foreground-child": "^3.3.1",
21
21
  "walk-up-path": "^4.0.0"
22
22
  },