Зачем: проект разрабатывается под Windows, а стенд там не стартовал:
spawn('npx') и spawn('node_modules/.bin/tsx') без shell падают с ENOENT,
потому что в Windows это .cmd-файлы (приёмка владельца, TASK-GROK-2 п.1).
Что: scripts/test/node-commands.mjs находит JS-точки входа через
require.resolve (tsx/cli, vitest.mjs, prettier.cjs) и всегда запускает их
текущим node (process.execPath). Сервер запускается одним процессом
`node --import tsx src/server.ts`: CLI tsx порождает дочерний node, и под
Windows kill() родителя оставил бы сервер-сироту на порту. baseEnv()
передаёт дочерним процессам PATH и под win32 системные переменные
(SystemRoot, TEMP и др.), без которых node на Windows не работает с сетью,
но не наследует секреты родителя.
Проверка на Linux: tests/e2e/harness/node-commands.test.ts (входит в
npm test) — команды начинаются с process.execPath, без .cmd/.bin/npx,
файлы существуют; исходники стенда не вызывают npx и node_modules/.bin.
Реальный прогон на Windows 11 — не проверено (нет Windows-раннера).
76 lines
3 KiB
JavaScript
76 lines
3 KiB
JavaScript
// Запуск инструментов из node_modules без .cmd-обёрток и без npx.
|
||
// Под Windows `node_modules/.bin/tsx` и `npx` — это .cmd-файлы: spawn() без shell
|
||
// падает с ENOENT. Поэтому всегда запускаем текущий node (process.execPath) с
|
||
// JS-точкой входа пакета, найденной через require.resolve.
|
||
import { createRequire } from 'node:module';
|
||
import path from 'node:path';
|
||
import { pathToFileURL } from 'node:url';
|
||
|
||
const repoRoot = path.resolve(import.meta.dirname, '../..');
|
||
const require = createRequire(path.join(repoRoot, 'package.json'));
|
||
|
||
function packageBin(name, binName = name) {
|
||
const manifestPath = require.resolve(`${name}/package.json`);
|
||
const manifest = require(manifestPath);
|
||
const bin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.[binName];
|
||
if (!bin) throw new Error(`${name}: в package.json нет bin "${binName}"`);
|
||
return path.join(path.dirname(manifestPath), bin);
|
||
}
|
||
|
||
/** Абсолютный путь к JS-точке входа CLI (не .cmd, не .bin). */
|
||
export function toolEntry(tool) {
|
||
switch (tool) {
|
||
case 'tsx':
|
||
return require.resolve('tsx/cli');
|
||
case 'vitest':
|
||
return packageBin('vitest');
|
||
case 'prettier':
|
||
return packageBin('prettier');
|
||
default:
|
||
throw new Error(`неизвестный инструмент: ${tool}`);
|
||
}
|
||
}
|
||
|
||
/** Команда для spawn(command, args): всегда process.execPath + JS-файл CLI. */
|
||
export function nodeTool(tool, args = []) {
|
||
return { command: process.execPath, args: [toolEntry(tool), ...args] };
|
||
}
|
||
|
||
/**
|
||
* Запуск TypeScript-файла (например, src/server.ts) в ОДНОМ процессе node с
|
||
* загрузчиком tsx (`node --import tsx`). CLI tsx запускает дочерний node, и под
|
||
* Windows kill() родителя (TerminateProcess) оставил бы сервер-сироту на порту.
|
||
*/
|
||
export function nodeWithTsx(args = []) {
|
||
const loader = pathToFileURL(require.resolve('tsx')).href;
|
||
return { command: process.execPath, args: ['--import', loader, ...args] };
|
||
}
|
||
|
||
/**
|
||
* Минимальное окружение дочернего процесса: PATH и (под Windows) системные
|
||
* переменные, без которых node не может работать с сетью/криптографией
|
||
* (SystemRoot, WINDIR, TEMP и т. п.). Секреты родителя не наследуются.
|
||
*/
|
||
export function baseEnv(platform = process.platform, env = process.env) {
|
||
const keys = ['PATH'];
|
||
if (platform === 'win32') {
|
||
keys.push(
|
||
'SystemRoot',
|
||
'WINDIR',
|
||
'TEMP',
|
||
'TMP',
|
||
'USERPROFILE',
|
||
'LOCALAPPDATA',
|
||
'APPDATA',
|
||
'ComSpec',
|
||
'PATHEXT',
|
||
);
|
||
}
|
||
const result = {};
|
||
for (const key of keys) {
|
||
if (env[key] !== undefined) result[key] = env[key];
|
||
}
|
||
return result;
|
||
}
|
||
|
||
export { repoRoot };
|