Skip to content
Merged
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
21 changes: 18 additions & 3 deletions scripts/demo.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { mkdtempSync, rmSync } from 'node:fs';
import { mkdtempSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { isAbsolute, join } from 'node:path';

const root = fileURLToPath(new URL('..', import.meta.url));
const mode = process.argv[2] ?? 'dev';
let temporary: string | undefined;

try {
const npmEntry = process.env.npm_execpath;
if (
!npmEntry ||
!isAbsolute(npmEntry) ||
!statSync(npmEntry, { throwIfNoEntry: false })?.isFile()
) {
throw new Error(
'The demo launcher requires the npm entry supplied by npm. Run "npm run dev" or "npm run build:demo" from the repository root.',
);
}
const environment = { ...process.env };
if (
environment.CSO_GALLERY_DIRECTORY === undefined &&
Expand All @@ -20,8 +30,9 @@ try {
).prepareDemoExamples({ directory: temporary });
}
const result = spawnSync(
'npm',
process.execPath,
[
npmEntry,
'run',
mode,
'--workspace',
Expand All @@ -33,11 +44,15 @@ try {
cwd: root,
stdio: 'inherit',
env: environment,
shell: false,
},
);
if (result.error) {
throw result.error;
}
if (result.signal) {
throw new Error(`Demo npm process terminated by ${result.signal}`);
}
process.exitCode = result.status ?? 1;
} finally {
if (temporary) {
Expand Down
131 changes: 131 additions & 0 deletions tests/integration/demo-launch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, expect, test } from 'vitest';

const root = fileURLToPath(new URL('../..', import.meta.url));
const launcher = join(root, 'scripts/demo.ts');
const directories: string[] = [];

afterEach(() => {
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});

function fixture() {
const directory = mkdtempSync(join(tmpdir(), 'cso npm café '));
directories.push(directory);
const npmEntry = join(directory, 'npm entry.mjs');
const env = { ...process.env };
for (const key of Object.keys(env)) {
if (
[
'path',
'npm_execpath',
'cso_examples_directory',
'cso_gallery_directory',
].includes(key.toLowerCase())
) {
Reflect.deleteProperty(env, key);
}
}
env.PATH = '';
env.npm_execpath = npmEntry;
env.PYTHON = join(directory, 'missing-python');
return { directory, npmEntry, env };
}

test.each([
['dev', 'CSO_EXAMPLES_DIRECTORY'],
['build', 'CSO_GALLERY_DIRECTORY'],
])(
'launches %s through the supplied npm entry with %s',
(mode, dataVariable) => {
const { directory, npmEntry, env } = fixture();
env[dataVariable] = directory;
writeFileSync(
npmEntry,
'process.stdout.write(JSON.stringify({ args: process.argv.slice(2), cwd: process.cwd(), examples: process.env.CSO_EXAMPLES_DIRECTORY, gallery: process.env.CSO_GALLERY_DIRECTORY }));',
);
const forwarded = [
'--port',
'4123',
'a path with spaces',
'"quoted" & $HOME; %PATH%',
'',
];
const result = spawnSync(process.execPath, [launcher, mode, ...forwarded], {
cwd: directory,
env,
encoding: 'utf8',
timeout: 30_000,
});
expect(result.error).toBeUndefined();
expect(result.status, result.stderr).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({
args: ['run', mode, '--workspace', '@cs-object/demo', '--', ...forwarded],
cwd: root.replace(/[\\/]$/, ''),
...(dataVariable === 'CSO_EXAMPLES_DIRECTORY'
? { examples: directory }
: { gallery: directory }),
});
},
);

test('preserves child stderr and nonzero exit status', () => {
const { directory, npmEntry, env } = fixture();
env.CSO_EXAMPLES_DIRECTORY = directory;
writeFileSync(
npmEntry,
'process.stderr.write("npm child failed\\n"); process.exitCode = 23;',
);
const result = spawnSync(process.execPath, [launcher, 'build'], {
env,
encoding: 'utf8',
timeout: 30_000,
});
expect(result.status).toBe(23);
expect(result.stderr).toBe('npm child failed\n');
});

test.skipIf(process.platform === 'win32')(
'reports a child terminated by a signal',
() => {
const { directory, npmEntry, env } = fixture();
env.CSO_EXAMPLES_DIRECTORY = directory;
writeFileSync(npmEntry, 'process.kill(process.pid, "SIGTERM");');
const result = spawnSync(process.execPath, [launcher, 'dev'], {
env,
encoding: 'utf8',
timeout: 30_000,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain('Demo npm process terminated by SIGTERM');
},
);

test.each(['missing', 'empty', 'absent-file', 'directory', 'relative'])(
'explains how to launch when the npm entry is %s before preparing examples',
(kind) => {
const { directory, npmEntry, env } = fixture();
if (kind === 'missing') Reflect.deleteProperty(env, 'npm_execpath');
if (kind === 'empty') env.npm_execpath = '';
if (kind === 'directory') env.npm_execpath = directory;
if (kind === 'relative') env.npm_execpath = 'npm entry.mjs';
if (kind !== 'absent-file')
writeFileSync(npmEntry, 'process.exitCode = 0;');
const result = spawnSync(process.execPath, [launcher], {
cwd: directory,
env,
encoding: 'utf8',
timeout: 30_000,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain('npm run dev');
expect(result.stderr).toContain('npm run build:demo');
expect(result.stdout).toBe('');
},
);
Loading