diff --git a/packages/sv/src/addons/tests/better-auth/test.ts b/packages/sv/src/addons/tests/better-auth/test.ts index 4954877ad..d29f06d0e 100644 --- a/packages/sv/src/addons/tests/better-auth/test.ts +++ b/packages/sv/src/addons/tests/better-auth/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; +import { execSync } from 'tinyexec'; import betterAuth from '../../better-auth.ts'; import drizzle from '../../drizzle.ts'; import { setupTest } from '../_setup/suite.ts'; @@ -37,7 +37,7 @@ test.concurrent.for(testCases)('better-auth $variant', async (testCase, { page, fs.writeFileSync(envPath, envContent, 'utf8'); // Generate auth schema using better-auth CLI - execSync('npm run auth:schema', { cwd, stdio: 'pipe' }); + execSync('npm', ['run', 'auth:schema'], { nodeOptions: { cwd }, throwOnError: true }); // Verify schema has auth tables const schemaPath = path.resolve(cwd, `src/lib/server/db/schema.${language}`); @@ -46,7 +46,10 @@ test.concurrent.for(testCases)('better-auth $variant', async (testCase, { page, expect(schemaContent).toContain('./auth.schema'); // Push schema to DB - execSync('npm run db:push -- --force', { cwd, stdio: 'pipe' }); + execSync('npm', ['run', 'db:push', '--', '--force'], { + nodeOptions: { cwd }, + throwOnError: true + }); /** ----- BROWSER SECTION ----- */ const { url, close } = await prepareServer({ cwd, page }); diff --git a/packages/sv/src/addons/tests/drizzle/test.ts b/packages/sv/src/addons/tests/drizzle/test.ts index 4ba45200c..372198c5e 100644 --- a/packages/sv/src/addons/tests/drizzle/test.ts +++ b/packages/sv/src/addons/tests/drizzle/test.ts @@ -1,8 +1,8 @@ -import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; +import { execSync } from 'tinyexec'; import { beforeAll, expect } from 'vitest'; import drizzle from '../../drizzle.ts'; import { setupTest } from '../_setup/suite.ts'; @@ -42,21 +42,28 @@ beforeAll(() => { const cwd = path.dirname(fileURLToPath(import.meta.url)); try { - execSync('docker --version', { cwd, stdio: 'pipe' }); + execSync('docker', ['--version'], { nodeOptions: { cwd }, throwOnError: true }); dockerInstalled = true; } catch { dockerInstalled = false; } - if (dockerInstalled) execSync('docker compose up --detach', { cwd, stdio: 'pipe' }); + if (dockerInstalled) { + execSync('docker', ['compose', 'up', '--detach'], { + nodeOptions: { cwd }, + throwOnError: true + }); + } // cleans up the containers on interrupts (ctrl+c) process.addListener('SIGINT', () => { - if (dockerInstalled) execSync('docker compose down --volumes', { cwd, stdio: 'pipe' }); + if (dockerInstalled) + execSync('docker', ['compose', 'down', '--volumes'], { nodeOptions: { cwd } }); }); return () => { - if (dockerInstalled) execSync('docker compose down --volumes', { cwd, stdio: 'pipe' }); + if (dockerInstalled) + execSync('docker', ['compose', 'down', '--volumes'], { nodeOptions: { cwd } }); }; }); @@ -92,7 +99,7 @@ test.concurrent.for(testCases)( const pageServerPath = path.resolve(routes, `+page.server.${ts ? 'ts' : 'js'}`); fs.writeFileSync(pageServerPath, pageServer, 'utf8'); - execSync('npm run db:push', { cwd, stdio: 'pipe' }); + execSync('npm', ['run', 'db:push'], { nodeOptions: { cwd }, throwOnError: true }); const { close } = await prepareServer({ cwd, page }); // kill server process when we're done diff --git a/packages/sv/src/addons/tests/eslint/test.ts b/packages/sv/src/addons/tests/eslint/test.ts index c458f5b3c..43c92c831 100644 --- a/packages/sv/src/addons/tests/eslint/test.ts +++ b/packages/sv/src/addons/tests/eslint/test.ts @@ -1,6 +1,6 @@ -import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; +import { execSync } from 'tinyexec'; import eslint from '../../eslint.ts'; import { setupTest } from '../_setup/suite.ts'; @@ -15,9 +15,18 @@ test.concurrent.for(testCases)('eslint $variant', (testCase, { expect, ...ctx }) const unlintedFile = 'let foo = "";\nif (Boolean(foo)) {\n//\n}'; fs.writeFileSync(path.resolve(cwd, 'src/lib/foo.js'), unlintedFile, 'utf8'); - expect(() => execSync('pnpm lint', { cwd, stdio: 'pipe' })).toThrow(); + expect( + execSync('pnpm', ['lint'], { nodeOptions: { cwd } }).exitCode, + 'lint should fail on unlinted file' + ).not.toBe(0); - expect(() => execSync('pnpm eslint --fix .', { cwd, stdio: 'pipe' })).not.toThrow(); + expect( + execSync('pnpm', ['eslint', '--fix', '.'], { nodeOptions: { cwd } }).exitCode, + 'eslint --fix should succeed' + ).toBe(0); - expect(() => execSync('pnpm lint', { cwd, stdio: 'pipe' })).not.toThrow(); + expect( + execSync('pnpm', ['lint'], { nodeOptions: { cwd } }).exitCode, + 'lint should pass after fix' + ).toBe(0); }); diff --git a/packages/sv/src/addons/tests/prettier/test.ts b/packages/sv/src/addons/tests/prettier/test.ts index 9af677bcc..ae0379f97 100644 --- a/packages/sv/src/addons/tests/prettier/test.ts +++ b/packages/sv/src/addons/tests/prettier/test.ts @@ -1,7 +1,7 @@ import { log } from '@clack/prompts'; -import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; +import { execSync } from 'tinyexec'; import { vi } from 'vitest'; import { ESLINT_VERSION } from '../../common.ts'; import prettier from '../../prettier.ts'; @@ -48,11 +48,20 @@ test.concurrent.for(testCases)('prettier $kind.type $variant', (testCase, { expe const unformattedFile = 'const foo = "bar"'; fs.writeFileSync(path.resolve(cwd, 'src/lib/foo.js'), unformattedFile, 'utf8'); - expect(() => execSync('pnpm lint', { cwd, stdio: 'pipe' })).toThrow(); + expect( + execSync('pnpm', ['lint'], { nodeOptions: { cwd } }).exitCode, + 'lint should fail on unformatted file' + ).not.toBe(0); - expect(() => execSync('pnpm format', { cwd, stdio: 'pipe' })).not.toThrow(); + expect( + execSync('pnpm', ['format'], { nodeOptions: { cwd } }).exitCode, + 'format should succeed' + ).toBe(0); - expect(() => execSync('pnpm lint', { cwd, stdio: 'pipe' })).not.toThrow(); + expect( + execSync('pnpm', ['lint'], { nodeOptions: { cwd } }).exitCode, + 'lint should pass after format' + ).toBe(0); } else if (testCase.kind.type === 'supported-eslint') { expect(fs.existsSync(path.resolve(cwd, 'eslint.config.js'))).toBe(true); diff --git a/packages/sv/src/addons/tests/vitest/test.ts b/packages/sv/src/addons/tests/vitest/test.ts index b00440972..f299ec4d4 100644 --- a/packages/sv/src/addons/tests/vitest/test.ts +++ b/packages/sv/src/addons/tests/vitest/test.ts @@ -1,6 +1,6 @@ -import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; +import { execSync } from 'tinyexec'; import vitest from '../../vitest-addon.ts'; import { setupTest } from '../_setup/suite.ts'; @@ -13,16 +13,17 @@ test.concurrent.for(testCases)('vitest $variant', (testCase, { expect, ...ctx }) const cwd = ctx.cwd(testCase); expect( - spawnSync('pnpm exec playwright install chromium', { - cwd, - stdio: 'pipe', - shell: true, - timeout: 2 * 60_000 - }).status + execSync('pnpm', ['exec', 'playwright', 'install', 'chromium'], { + nodeOptions: { + cwd, + timeout: 2 * 60_000, + shell: true + } + }).exitCode ).toBe(0); expect( - spawnSync('pnpm test', { cwd, stdio: 'pipe', shell: true, timeout: 2 * 60_000 }).status + execSync('pnpm', ['test'], { nodeOptions: { cwd, shell: true, timeout: 2 * 60_000 } }).exitCode ).toBe(0); const viteFile = ['vite.config.ts', 'vite.config.js'] diff --git a/packages/sv/src/cli/check.ts b/packages/sv/src/cli/check.ts index fd5f2dfa0..ae59243aa 100644 --- a/packages/sv/src/cli/check.ts +++ b/packages/sv/src/cli/check.ts @@ -1,8 +1,8 @@ -import { color, resolveCommandArray } from '@sveltejs/sv-utils'; +import { color, resolveCommand, resolveCommandArray } from '@sveltejs/sv-utils'; import { Command } from 'commander'; import * as resolve from 'empathic/resolve'; -import { execSync } from 'node:child_process'; import process from 'node:process'; +import { execSync } from 'tinyexec'; import { forwardExitCode } from '../core/common.ts'; import { detectPackageManager } from '../core/package-manager.ts'; @@ -39,8 +39,11 @@ async function runCheck(cwd: string, args: string[]) { // avoids printing the stack trace for `sv` when `svelte-check` exits with an error code try { - const cmd = resolveCommandArray(pm, 'execute-local', ['svelte-check', ...args]).join(' '); - execSync(cmd, { stdio: 'inherit', cwd }); + const cmd = resolveCommand(pm, 'execute-local', ['svelte-check', ...args])!; + execSync(cmd.command, cmd.args, { + nodeOptions: { cwd, stdio: 'inherit' }, + throwOnError: true + }); } catch (error) { forwardExitCode(error); } finally { diff --git a/packages/sv/src/cli/migrate.ts b/packages/sv/src/cli/migrate.ts index 22886dc60..c6b37ddae 100644 --- a/packages/sv/src/cli/migrate.ts +++ b/packages/sv/src/cli/migrate.ts @@ -1,7 +1,7 @@ -import { resolveCommandArray } from '@sveltejs/sv-utils'; +import { resolveCommand } from '@sveltejs/sv-utils'; import { Command } from 'commander'; -import { execSync } from 'node:child_process'; import process from 'node:process'; +import { execSync } from 'tinyexec'; import { forwardExitCode } from '../core/common.ts'; import { detectPackageManager } from '../core/package-manager.ts'; @@ -9,21 +9,25 @@ export const migrate = new Command('migrate') .description('a CLI for migrating Svelte(Kit) codebases') .argument('[migration]', 'migration to run') .option('-C, --cwd ', 'path to working directory', process.cwd()) - .action(async (migration, options) => { - await runMigrate(options.cwd, [migration]); - }); + .action((migration, options) => runMigrate(options.cwd, [migration])); async function runMigrate(cwd: string, args: string[]) { const pm = await detectPackageManager(cwd); // avoids printing the stack trace for `sv` when `svelte-migrate` exits with an error code try { - const cmdArgs = ['svelte-migrate@latest', ...args]; + const newArgs = [ + // skips the download confirmation prompt for `npx` + ...(pm === 'npm' ? '--yes' : ''), + 'svelte-migrate@latest', + ...args + ]; - // skips the download confirmation prompt for `npx` - if (pm === 'npm') cmdArgs.unshift('--yes'); - - execSync(resolveCommandArray(pm, 'execute', cmdArgs).join(' '), { stdio: 'inherit', cwd }); + const cmd = resolveCommand(pm, 'execute', newArgs)!; + execSync(cmd.command, cmd.args, { + nodeOptions: { cwd, stdio: 'inherit' }, + throwOnError: true + }); } catch (error) { forwardExitCode(error); } diff --git a/packages/sv/src/cli/tests/cli.ts b/packages/sv/src/cli/tests/cli.ts index 0bac5f647..fef3f6e7e 100644 --- a/packages/sv/src/cli/tests/cli.ts +++ b/packages/sv/src/cli/tests/cli.ts @@ -96,9 +96,19 @@ describe('cli', () => { ...args ]; + /** + * Same as `exec`. but `cwd` defaults to `testOutputPath` + */ + const run = (...params: Parameters) => { + const [command, args, options = {}] = params; + options.nodeOptions ??= {}; + options.nodeOptions.cwd ??= testOutputPath; + return exec(command, args, options); + }; + // useful for debugging // console.log(`command`, `node ${allArgs.join(' ')}`); - const result = await exec('node', allArgs, { nodeOptions: { stdio: 'pipe' } }); + const result = await exec('node', allArgs); // cli finished well expect( @@ -199,24 +209,18 @@ describe('cli', () => { if (projectName === 'create-with-all-addons' && process.platform !== 'win32') { // the generated project lives inside this repo, so it must not join its workspace - const installResult = await exec( - 'pnpm', - ['install', '--no-frozen-lockfile', '--ignore-workspace'], - { nodeOptions: { stdio: 'pipe', cwd: testOutputPath } } - ); + const installResult = await run('pnpm', [ + 'install', + '--no-frozen-lockfile', + '--ignore-workspace' + ]); expect( installResult.exitCode, `pnpm install failed:\n stdout: ${installResult.stdout}\n stderr: ${installResult.stderr}` ).toBe(0); - await exec('pnpm', ['build'], { - nodeOptions: { stdio: 'pipe', cwd: testOutputPath } - }); - await exec('pnpm', ['auth:schema'], { - nodeOptions: { stdio: 'pipe', cwd: testOutputPath } - }); - const check = await exec('pnpm', ['check'], { - nodeOptions: { stdio: 'pipe', cwd: testOutputPath } - }); + await run('pnpm', ['build']); + await run('pnpm', ['auth:schema']); + const check = await run('pnpm', ['check']); expect( check.exitCode, `svelte-check failed:\n stdout: ${check.stdout}\n stderr: ${check.stderr}` @@ -225,9 +229,6 @@ describe('cli', () => { // `kit@next` moves fast - only a real install/build/check catches options it removed if (projectName === 'create-experimental-next' && process.platform !== 'win32') { - const run = (cmd: string, cmdArgs: string[]) => - exec(cmd, cmdArgs, { nodeOptions: { stdio: 'pipe', cwd: testOutputPath } }); - const install = await run('pnpm', [ 'install', '--no-frozen-lockfile', @@ -287,10 +288,8 @@ describe('cli', () => { for (const cmd of cmds) { // use npm here so the install doesn't walk up into the monorepo's // pnpm workspace and try to resolve packages from there - const res = await exec('npm', cmd, { + const res = await run('npm', cmd, { nodeOptions: { - stdio: 'pipe', - cwd: testOutputPath, env: { ...process.env, // allow npm under a repo whose packageManager is pnpm diff --git a/packages/sv/src/core/engine.ts b/packages/sv/src/core/engine.ts index 8540c56ba..35d864990 100644 --- a/packages/sv/src/core/engine.ts +++ b/packages/sv/src/core/engine.ts @@ -250,10 +250,10 @@ async function runAddon({ addon, loaded, multiple, workspace, workspaceOptions } } }, execute: async (commandArgs, stdio) => { - const { command, args } = resolveCommand(workspace.packageManager, 'execute', commandArgs)!; + const cmd = resolveCommand(workspace.packageManager, 'execute', commandArgs)!; const addonPrefix = multiple ? `${addon.id}: ` : ''; - const executedCommand = [command, ...args].join(' '); + const executedCommand = [cmd.command, ...cmd.args].join(' '); if (!TESTING) { p.log.step( `${addonPrefix}Running external command ${color.optional(`(${executedCommand})`)}` @@ -261,18 +261,21 @@ async function runAddon({ addon, loaded, multiple, workspace, workspaceOptions } } // adding --yes as the first parameter helps avoiding the "Need to install the following packages:" message - if (workspace.packageManager === 'npm') args.unshift('--yes'); + if (workspace.packageManager === 'npm') cmd.args.unshift('--yes'); try { - await exec(command, args, { + await exec(cmd.command, cmd.args, { nodeOptions: { cwd: workspace.cwd, stdio: TESTING ? 'pipe' : stdio }, throwOnError: true }); - } catch (error) { - const typedError = error as NonZeroExitError; - throw new Error(`Failed to execute scripts '${executedCommand}': ${typedError.message}`, { - cause: error - }); + } catch (e) { + let msg; + if (e instanceof NonZeroExitError || e instanceof Error) { + msg = `Failed to execute scripts '${executedCommand}': ${e.message}`; + } else { + msg = 'unknown error'; + } + throw new Error(msg, { cause: e }); } }, dependency: (pkg, version) => { diff --git a/packages/sv/src/core/formatFiles.ts b/packages/sv/src/core/formatFiles.ts index 932fe8024..9a9829004 100644 --- a/packages/sv/src/core/formatFiles.ts +++ b/packages/sv/src/core/formatFiles.ts @@ -1,6 +1,6 @@ import * as p from '@clack/prompts'; import { type AgentName, resolveCommand } from '@sveltejs/sv-utils'; -import { exec } from 'tinyexec'; +import { exec, NonZeroExitError } from 'tinyexec'; export async function formatFiles(options: { packageManager: AgentName; @@ -36,15 +36,20 @@ async function run( cwd: string ): Promise<{ error?: string; notFound?: boolean }> { try { - await exec(command, args, { nodeOptions: { cwd, stdio: 'pipe' }, throwOnError: true }); + await exec(command, args, { nodeOptions: { cwd }, throwOnError: true }); return {}; } catch (e) { - // @ts-expect-error tinyexec rethrows the spawn error as-is - if (e?.code === 'ENOENT') return { notFound: true, error: `${command} not found` }; - // @ts-expect-error `output` is only present on tinyexec's `NonZeroExitError` - const output = e?.output as { stderr?: string; stdout?: string } | undefined; - // failures can land on either stream, so report both - const message = [output?.stderr, output?.stdout].filter(Boolean).join('\n').trim(); - return { error: message || (e instanceof Error ? e.message : 'unknown error') }; + // tinyexec rethrows the spawn error as-is + if ((e as NodeJS.ErrnoException | null)?.code === 'ENOENT') { + return { notFound: true, error: `${command} not found` }; + } + if (e instanceof NonZeroExitError) { + // failures can land on either stream, so report both + const { stderr, stdout } = e.output ?? {}; + const message = [stderr, stdout].filter(Boolean).join('\n').trim(); + return { error: message || e.message }; + } + if (e instanceof Error) return { error: e.message }; + return { error: 'unknown error' }; } } diff --git a/packages/sv/src/core/package-manager.ts b/packages/sv/src/core/package-manager.ts index fece9f764..5c1fcc28d 100644 --- a/packages/sv/src/core/package-manager.ts +++ b/packages/sv/src/core/package-manager.ts @@ -1,13 +1,5 @@ import * as p from '@clack/prompts'; -import { - AGENTS, - type AgentName, - COMMANDS, - color, - constructCommand, - detect, - pnpm -} from '@sveltejs/sv-utils'; +import { AGENTS, type AgentName, color, detect, pnpm, resolveCommand } from '@sveltejs/sv-utils'; import { Option } from 'commander'; import * as find from 'empathic/find'; import fs from 'node:fs'; @@ -72,12 +64,9 @@ export async function installDependencies(agent: AgentName, cwd: string): Promis retainLog: true }); - const { command, args } = constructCommand(COMMANDS[agent].install, [])!; + const { command, args } = resolveCommand(agent, 'install', [])!; - const proc = exec(command, args, { - nodeOptions: { cwd, stdio: 'pipe' }, - throwOnError: false - }); + const proc = exec(command, args, { nodeOptions: { cwd }, throwOnError: false }); const output: string[] = []; try { diff --git a/packages/sv/src/core/verifiers.ts b/packages/sv/src/core/verifiers.ts index 884ef20de..465a26566 100644 --- a/packages/sv/src/core/verifiers.ts +++ b/packages/sv/src/core/verifiers.ts @@ -1,5 +1,4 @@ -import { exec } from 'node:child_process'; -import { promisify } from 'node:util'; +import { exec } from 'tinyexec'; import type { AddonDefinition, SetupResult, Verification } from './config.ts'; import { UnsupportedError } from './errors.ts'; @@ -10,26 +9,17 @@ export function verifyCleanWorkingDirectory(cwd: string, gitCheck: boolean) { verifications.push({ name: 'clean working directory', run: async () => { - try { - // If a user has pending git changes the output of the following command will list - // all files that have been added/modified/deleted and thus the output will not be empty. - // In case the output of the command below is an empty text, we can safely assume - // there are no pending changes. If the below command is run outside of a git repository, - // git will exit with a failing exit code, which will trigger the catch statement. - // also see https://remarkablemark.org/blog/2017/10/12/check-git-dirty/#git-status - const asyncExec = promisify(exec); - const { stdout } = await asyncExec('git status --short', { - cwd - }); - - if (stdout) { - return { success: false, message: 'Uncommited changes found' }; - } - - return { success: true, message: undefined }; - } catch { - return { success: true, message: 'Not a git repository' }; - } + // If a user has pending git changes the output of the following command will list + // all files that have been added/modified/deleted and thus the output will not be empty. + // In case the output of the command below is an empty text, we can safely assume + // there are no pending changes. If the below command is run outside of a git repository, + // git will exit with a failing exit code. + // also see https://remarkablemark.org/blog/2017/10/12/check-git-dirty/#git-status + const result = await exec('git', ['status', '--short'], { nodeOptions: { cwd } }); + + if (result.exitCode !== 0) return { success: true, message: 'Not a git repository' }; + if (result.stdout) return { success: false, message: 'Uncommited changes found' }; + return { success: true, message: undefined }; } }); } diff --git a/packages/sv/src/create/tests/check.ts b/packages/sv/src/create/tests/check.ts index 9aa03af48..7c9eac824 100644 --- a/packages/sv/src/create/tests/check.ts +++ b/packages/sv/src/create/tests/check.ts @@ -1,9 +1,7 @@ -import { type PromiseWithChild, exec as nodeExec } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { promisify } from 'node:util'; -import { exec } from 'tinyexec'; +import { exec, type Result } from 'tinyexec'; import { beforeAll, describe, expect, test } from 'vitest'; import { add, officialAddons } from '../../../../sv/src/index.ts'; import { createProject } from '../../cli/create.ts'; @@ -21,11 +19,9 @@ fs.mkdirSync(test_workspace_dir, { recursive: true }); fs.writeFileSync(path.join(test_workspace_dir, 'pnpm-workspace.yaml'), 'packages:\n - ./*\n'); -const exec_async = promisify(nodeExec); - beforeAll(async () => { const install = await exec('pnpm', ['install', '--no-frozen-lockfile'], { - nodeOptions: { cwd: test_workspace_dir, stdio: 'pipe' } + nodeOptions: { cwd: test_workspace_dir } }); if (install.exitCode !== 0) { throw new Error( @@ -38,7 +34,7 @@ beforeAll(async () => { * Tests in different templates can be run concurrently for a nice speedup locally, but tests within a template must be run sequentially. * It'd be better to group tests by template, but vitest doesn't support that yet. */ -const script_test_map = new Map PromiseWithChild]>>(); +const script_test_map = new Map Result]>>(); const templates = fs.readdirSync(resolve_path('../templates/')) as TemplateType[]; @@ -85,7 +81,7 @@ for (const template of templates.filter((t) => t !== 'addon')) { for (const script of scripts_to_test) { const tests = script_test_map.get(script) ?? []; - tests.push([`${template}-${types}`, () => exec_async(`pnpm ${script}`, { cwd })]); + tests.push([`${template}-${types}`, () => exec('pnpm', [script], { nodeOptions: { cwd } })]); script_test_map.set(script, tests); } diff --git a/packages/sv/src/testing.ts b/packages/sv/src/testing.ts index 5301b2516..18a835461 100644 --- a/packages/sv/src/testing.ts +++ b/packages/sv/src/testing.ts @@ -1,10 +1,9 @@ import type { Page } from '@playwright/test'; -import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import pstree, { type PS } from 'ps-tree'; -import { exec, x } from 'tinyexec'; +import { exec, execSync } from 'tinyexec'; import type { TestProject } from 'vitest/node'; import { add, type AddonMap, type OptionMap } from './core/engine.ts'; import { addPnpmAllowBuilds } from './core/package-manager.ts'; @@ -82,11 +81,7 @@ async function startPreview({ command = 'npm run preview' }: PreviewOptions): Promise<{ url: string; close: () => Promise }> { const [cmd, ...args] = command.split(' '); - const proc = exec(cmd, args, { - nodeOptions: { cwd, stdio: 'pipe' }, - throwOnError: true, - timeout: 66_999 - }); + const proc = exec(cmd, args, { nodeOptions: { cwd }, throwOnError: true, timeout: 66_999 }); const close = async () => { if (!proc.pid) return; @@ -129,7 +124,7 @@ async function getProcessTree(pid: number) { async function terminate(pid: number) { if (process.platform === 'win32') { // on windows, use taskkill to terminate the process tree - await x('taskkill', ['/PID', `${pid}`, '/T', '/F']); + await exec('taskkill', ['/PID', `${pid}`, '/T', '/F']); return; } const children = await getProcessTree(pid); @@ -228,7 +223,10 @@ export async function prepareServer({ expect }: PrepareServerOptions): Promise { // build project - if (buildCommand) execSync(buildCommand, { cwd, stdio: 'pipe' }); + if (buildCommand) { + const [cmd, ...args] = buildCommand.split(' '); + execSync(cmd, args, { nodeOptions: { cwd }, throwOnError: true }); + } // start preview server const { url, close } = await startPreview({ cwd, command: previewCommand }); @@ -366,7 +364,7 @@ export function createSetupTest( const installDir = path.resolve(cwd, testName); const install = await exec('pnpm', ['install'], { - nodeOptions: { cwd: installDir, stdio: 'pipe' } + nodeOptions: { cwd: installDir } }); if (install.exitCode !== 0) { throw new Error(