krona-staff/scripts/test/node-commands.mjs
grok 0aaa2f4d2d test(e2e): запуск vitest/tsx/prettier без .cmd-обёрток (Windows)
Зачем: проект разрабатывается под 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-раннера).
2026-09-26 15:19:47 +07:00

76 lines
3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Запуск инструментов из 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 };