mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-14 18:01:21 +00:00
fix(tools): spawn npm.cmd through a shell on Windows (#3923)
* fix(tools): spawn npm.cmd through a shell on Windows * fix(tools): join the npm command line on Windows to avoid DEP0190 Passing args alongside shell: true warns on Node 24. Test the spawn tuple as data and run a real npm spawn instead of mocking child_process. --------- Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
This commit is contained in:
1 parent
9f69483593
commit
83de67cca9
5 files changed
+63
-7
No files matched your search
@@ -112,6 +112,8 @@ export default defineConfig(({ mode }) => ({
|
||||
// The MCP connector's signed-upload tools call the `/fs` HTTP API
|
||||
// directly, so their tests need a booted backend (`setupPuterTestEnv`).
|
||||
'src/mcp-connector/**/*.test.{js,ts}',
|
||||
// Root-level tools/ scripts are exercised through this suite.
|
||||
'tools/**/*.test.mjs',
|
||||
],
|
||||
// Root is the repo root so that the file transformer (which
|
||||
// applies `lowerDecoratorsPlugin`) sees both src/backend and
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
// the previous bash version.
|
||||
|
||||
import { existsSync, readdirSync, statSync } from 'node:fs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { join } from 'node:path';
|
||||
import { spawnNpm } from './npmSpawn.mjs';
|
||||
|
||||
const EXT_DIR = './extensions';
|
||||
|
||||
@@ -23,13 +23,11 @@ if (dirs.length === 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
|
||||
function install(dir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = existsSync(join(dir, 'package-lock.json')) ? ['ci'] : ['install'];
|
||||
console.log(`[${dir}] starting npm ${args.join(' ')}`);
|
||||
const child = spawn(npmCmd, args, { cwd: dir });
|
||||
const child = spawnNpm(args, { cwd: dir });
|
||||
let out = '';
|
||||
child.stdout.on('data', (d) => (out += d));
|
||||
child.stderr.on('data', (d) => (out += d));
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Shared helper for spawning npm from the repo's tools/ scripts.
|
||||
//
|
||||
// On Windows `npm` resolves to the `npm.cmd` batch shim, which Node refuses
|
||||
// to spawn without a shell (`spawn EINVAL`). Node also warns when args are
|
||||
// passed alongside `shell: true` (DEP0190), so the command line is joined
|
||||
// here; args must be plain words that need no quoting.
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
/** `[command, args, options]` for `spawn`. */
|
||||
export const npmSpawnArgs = (
|
||||
args,
|
||||
options = {},
|
||||
platform = process.platform,
|
||||
) =>
|
||||
platform === 'win32'
|
||||
? [['npm.cmd', ...args].join(' '), [], { ...options, shell: true }]
|
||||
: ['npm', args, options];
|
||||
|
||||
export const spawnNpm = (args, options = {}) =>
|
||||
spawn(...npmSpawnArgs(args, options));
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { npmSpawnArgs, spawnNpm } from './npmSpawn.mjs';
|
||||
|
||||
describe('npmSpawnArgs', () => {
|
||||
it('runs npm.cmd through a shell on Windows, with no separate args', () => {
|
||||
expect(
|
||||
npmSpawnArgs(['run', 'build:ts'], { cwd: '.' }, 'win32'),
|
||||
).toEqual(['npm.cmd run build:ts', [], { cwd: '.', shell: true }]);
|
||||
});
|
||||
|
||||
it('spawns npm directly without a shell elsewhere', () => {
|
||||
for (const platform of ['linux', 'darwin']) {
|
||||
expect(npmSpawnArgs(['ci'], { cwd: '.' }, platform)).toEqual([
|
||||
'npm',
|
||||
['ci'],
|
||||
{ cwd: '.' },
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('spawnNpm', () => {
|
||||
it('runs npm on the current platform', async () => {
|
||||
const child = spawnNpm(['--version']);
|
||||
let out = '';
|
||||
child.stdout.on('data', (d) => (out += d));
|
||||
const code = await new Promise((resolve, reject) => {
|
||||
child.on('error', reject);
|
||||
child.on('close', resolve);
|
||||
});
|
||||
expect(code).toBe(0);
|
||||
expect(out.trim()).toMatch(/^\d+\.\d+\.\d+/);
|
||||
}, 20_000);
|
||||
});
|
||||
+4
-3
@@ -23,6 +23,8 @@ import { spawn } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { npmSpawnArgs } from './npmSpawn.mjs';
|
||||
|
||||
const rootDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||
|
||||
// `npm start --flag=x` arrives as the npm_config_<flag> env var;
|
||||
@@ -51,7 +53,6 @@ const run = (cmd, args, opts = {}) => new Promise((resolve, reject) => {
|
||||
});
|
||||
});
|
||||
|
||||
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
const server = getFlag('server');
|
||||
const extensions = getFlag('extensions');
|
||||
|
||||
@@ -66,8 +67,8 @@ try {
|
||||
if ( extensions ) {
|
||||
console.warn('--extensions only applies to --server (GUI-only) mode; ignoring.');
|
||||
}
|
||||
await run(npm, ['run', 'setupExtensions']);
|
||||
await run(npm, ['run', 'build:ts']);
|
||||
await run(...npmSpawnArgs(['run', 'setupExtensions']));
|
||||
await run(...npmSpawnArgs(['run', 'build:ts']));
|
||||
await run(process.execPath, [
|
||||
'--enable-source-maps',
|
||||
'-r', './dist/src/backend/telemetry.js',
|
||||
|
||||
Reference in new issue
Block a user