Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { 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';
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, '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 { 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';
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
11 changes: 7 additions & 4 deletions packages/sv/src/cli/check.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 14 additions & 10 deletions packages/sv/src/cli/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
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';

export const migrate = new Command('migrate')
.description('a CLI for migrating Svelte(Kit) codebases')
.argument('[migration]', 'migration to run')
.option('-C, --cwd <path>', '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);
}
Expand Down
41 changes: 20 additions & 21 deletions packages/sv/src/cli/tests/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if this should be using run instead? im not really sure myself


// cli finished well
expect(
Expand Down Expand Up @@ -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}`
Expand All @@ -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',
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions packages/sv/src/core/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,21 +250,21 @@ 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})`)}`
);
}

// 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
});
Expand Down
23 changes: 14 additions & 9 deletions packages/sv/src/core/formatFiles.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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' };
}
}
Loading