Skip to content
Merged
9 changes: 6 additions & 3 deletions packages/sv/src/addons/tests/better-auth/test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { expect } from '@playwright/test';
import { execSync } from 'tinyexec';
import betterAuth from '../../better-auth.ts';
import drizzle from '../../drizzle.ts';
import { setupTest } from '../_setup/suite.ts';
Expand Down Expand Up @@ -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}`);
Expand All @@ -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 });
Expand Down
19 changes: 13 additions & 6 deletions packages/sv/src/addons/tests/drizzle/test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 } });
};
});

Expand Down Expand Up @@ -92,7 +99,7 @@ test.concurrent.for(testCases)(
const pageServerPath = path.resolve(routes, `+page.server.${ts ? 'ts' : 'js'}`);
fs.writeFileSync(pageServerPath, pageServer(ts ? 'ts' : 'js'), '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
Expand Down
17 changes: 13 additions & 4 deletions packages/sv/src/addons/tests/eslint/test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);
});
17 changes: 13 additions & 4 deletions packages/sv/src/addons/tests/prettier/test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { log } from '@clack/prompts';
import { execSync } from 'tinyexec';
import { vi } from 'vitest';
import { ESLINT_VERSION } from '../../common.ts';
import prettier from '../../prettier.ts';
Expand Down Expand Up @@ -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);

Expand Down
17 changes: 9 additions & 8 deletions packages/sv/src/addons/tests/vitest/test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'tinyexec';
Comment thread
sacrosanctic marked this conversation as resolved.
import vitest from '../../vitest-addon.ts';
import { setupTest } from '../_setup/suite.ts';

Expand All @@ -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']
Expand Down
8 changes: 4 additions & 4 deletions packages/sv/src/cli/check.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { execSync } from 'node:child_process';
import process from 'node:process';
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 'tinyexec';
import { forwardExitCode } from '../core/common.ts';
import { detectPackageManager } from '../core/package-manager.ts';

Expand Down Expand Up @@ -39,8 +39,8 @@ 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 {
Expand Down
38 changes: 20 additions & 18 deletions packages/sv/src/cli/tests/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,19 @@ describe('cli', () => {
...args
];

/**
* Same as `exec`. but `cwd` defaults to `testOutputPath`
*/
const run = (...params: Parameters<typeof exec>) => {
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);
Comment thread
AdrianGonz97 marked this conversation as resolved.

// cli finished well
expect(
Expand Down Expand Up @@ -191,24 +201,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}`
Expand Down Expand Up @@ -247,10 +251,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
Expand Down
4 changes: 4 additions & 0 deletions packages/sv/src/core/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,7 @@ export async function runAndValidateVerifications(verifications: Verification[])
}
}
}

export function isNodeError(err: unknown): err is Error & NodeJS.ErrnoException {
return err instanceof Error;
}
11 changes: 6 additions & 5 deletions packages/sv/src/core/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
type Package,
minimizeDiff
} from '@sveltejs/sv-utils';
import { NonZeroExitError, exec } from 'tinyexec';
import { exec } from 'tinyexec';
import { createLoadedAddon } from '../cli/add.ts';
import { filePaths } from './common.ts';
import {
Expand Down Expand Up @@ -416,10 +416,11 @@ export function prepareSvApi(
throwOnError: true
});
} catch (error) {
const typedError = error as NonZeroExitError;
throw new Error(`Failed to execute scripts '${executedCommand}': ${typedError.message}`, {
cause: error
});
let message = `Failed to execute scripts '${executedCommand}'`;
if (error instanceof Error) {
message += `: ${error.message}`;
}
throw new Error(message, { cause: error });
}
},
dependency: (pkg, version) => {
Expand Down
17 changes: 8 additions & 9 deletions packages/sv/src/core/formatFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as p from '@clack/prompts';
import { type AgentName, loadPackageJson, resolveCommand } from '@sveltejs/sv-utils';
import * as resolve from 'empathic/resolve';
import { exec } from 'tinyexec';
import { isNodeError } from './common.ts';
import { detectPackageManager } from './package-manager.ts';
import { findWorkspaceRoot } from './workspace.ts';

Expand Down Expand Up @@ -109,20 +110,18 @@ async function withSpinner(

async function run(command: string, args: string[], cwd: string): Promise<{ error?: string }> {
try {
await exec(command, args, { nodeOptions: { cwd, stdio: 'pipe' }, throwOnError: true });
await exec(command, args, { nodeOptions: { cwd }, throwOnError: true });
return {};
} catch (e) {
// Unix spawn of a missing binary is ENOENT. On Windows, tinyexec often runs via
// cmd.exe which exits 1 with "is not recognized..." instead. We'll treat both as errors
// so we can fall back to the package manager (needed for Yarn PnP).
if (e instanceof Error && 'code' in e) {
if (e.code === 'ENOENT') {
return { error: `${command} not found` };
}

return { error: e.message };
if (!isNodeError(e)) {
return { error: 'unknown error' };
}

return { error: 'unknown error' };
if ('code' in e && e.code === 'ENOENT') {
return { error: `${command} not found` };
}
return { error: e.message };
}
}
17 changes: 3 additions & 14 deletions packages/sv/src/core/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
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 { exec, execSync } from 'tinyexec';
Expand Down Expand Up @@ -69,12 +61,9 @@ export async function installDependencies(
retainLog: true
});

const { command, args } = constructCommand(COMMANDS[agent].install, flags)!;
const { command, args } = resolveCommand(agent, 'install', flags)!;

const proc = exec(command, args, {
nodeOptions: { cwd, stdio: 'pipe' },
throwOnError: false
});
const proc = exec(command, args, { nodeOptions: { cwd }, throwOnError: false });

const output: string[] = [];
try {
Expand Down
Loading