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
2 changes: 1 addition & 1 deletion .github/workflows/ci-check-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
- name: Test generator routing and ownership
run: |
node docs/node_modules/tsx/dist/cli.mjs scripts/docs-generators/runner.ts --test-routing
node docs/node_modules/tsx/dist/cli.mjs --test scripts/docs-generators/cua-driver.test.ts scripts/docs-generators/cua-driver-platform.test.ts
node docs/node_modules/tsx/dist/cli.mjs --test scripts/docs-generators/runner.test.ts scripts/docs-generators/cua-driver.test.ts scripts/docs-generators/cua-driver-platform.test.ts
- name: Select affected generator
id: scope
env:
Expand Down
129 changes: 129 additions & 0 deletions scripts/docs-generators/runner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import {
copyFileSync,
cpSync,
mkdirSync,
mkdtempSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import test, { type TestContext } from 'node:test';

function runnerFixture(t: TestContext) {
const directory = mkdtempSync(join(tmpdir(), 'cua-docs-runner-'));
t.after(() => rmSync(directory, { recursive: true, force: true }));
const source = join(directory, 'scripts', 'docs-generators');
cpSync(__dirname, source, { recursive: true });
const docs = join(directory, 'docs');
mkdirSync(docs);
symlinkSync(
resolve(__dirname, '../../docs/node_modules'),
join(docs, 'node_modules'),
'junction'
);
copyFileSync(resolve(__dirname, '../../docs/pnpm-lock.yaml'), join(docs, 'pnpm-lock.yaml'));
writeFileSync(
join(directory, 'fixture.mjs'),
'console.log("FIXTURE_GENERATOR_RAN", JSON.stringify(process.argv.slice(2)));\n'
);
writeFileSync(
join(source, 'config.json'),
JSON.stringify({
description: 'Command regression fixture',
generators: {
fixture: {
name: 'Fixture',
language: 'JavaScript',
sourcePath: 'fixture',
docsOutputPath: 'docs',
generatorScript: 'fixture.mjs',
watchPaths: ['fixture/**'],
buildCommand: null,
buildDirectory: '.',
extractionMethod: 'fixture',
outputs: [],
enabled: true,
},
},
})
);
return (args: string[]) => {
const result = spawnSync(
process.execPath,
[...process.execArgv, join(source, 'runner.ts'), ...args],
{
cwd: directory,
env: { ...process.env, NODE_TEST_CONTEXT: undefined },
encoding: 'utf8',
timeout: 60_000,
}
);
assert.ifError(result.error);
return result;
};
}

test('help prints usage without starting a generator', (t) => {
const run = runnerFixture(t);
const ordinary = run([]);
assert.equal(ordinary.status, 0, ordinary.stdout + ordinary.stderr);
assert.match(ordinary.stdout, /FIXTURE_GENERATOR_RAN \[\]/);
const help = run(['--help']);
assert.equal(help.status, 0, help.stdout + help.stderr);
assert.match(help.stdout, /Usage:/);
assert.match(help.stdout, /--library/);
assert.doesNotMatch(help.stdout + help.stderr, /FIXTURE_GENERATOR_RAN/);
});

test('unknown options fail without starting a generator', (t) => {
const run = runnerFixture(t);
const invalid = run(['--not-a-real-option']);
assert.equal(invalid.status, 1, invalid.stdout + invalid.stderr);
assert.match(invalid.stderr, /Unknown option.*--not-a-real-option/);
assert.doesNotMatch(invalid.stdout + invalid.stderr, /FIXTURE_GENERATOR_RAN/);
});

test('an empty library name does not fall back to running all generators', (t) => {
const run = runnerFixture(t);
const invalid = run(['--library', '']);
assert.equal(invalid.status, 1, invalid.stdout + invalid.stderr);
assert.match(invalid.stderr, /Unknown library/);
assert.doesNotMatch(invalid.stdout + invalid.stderr, /FIXTURE_GENERATOR_RAN/);
});

test('an empty changed-files path does not fall back to running generators', (t) => {
const run = runnerFixture(t);
const invalid = run(['--changed-files-file', '']);
assert.equal(invalid.status, 1, invalid.stdout + invalid.stderr);
assert.match(invalid.stderr, /Changed-files input not found/);
assert.doesNotMatch(invalid.stdout + invalid.stderr, /FIXTURE_GENERATOR_RAN/);
});

test('both check spellings still reach the selected generator', (t) => {
const run = runnerFixture(t);
for (const flag of ['--check', '--check-only']) {
const checked = run(['--library', 'fixture', flag]);
assert.equal(checked.status, 0, checked.stdout + checked.stderr);
assert.match(checked.stdout, /FIXTURE_GENERATOR_RAN \["--check"\]/);
}
});

test('list and changed-file selection report without running generators', (t) => {
const run = runnerFixture(t);
const listed = run(['--list']);
assert.equal(listed.status, 0, listed.stdout + listed.stderr);
assert.match(listed.stdout, /Fixture \(JavaScript\)/);
assert.doesNotMatch(listed.stdout + listed.stderr, /FIXTURE_GENERATOR_RAN/);

const directory = mkdtempSync(join(tmpdir(), 'cua-docs-changes-'));
t.after(() => rmSync(directory, { recursive: true, force: true }));
const input = join(directory, 'changed-files.txt');
writeFileSync(input, 'fixture/example.mjs\n');
const selected = run(['--changed-files-file', input]);
assert.equal(selected.status, 0, selected.stdout + selected.stderr);
assert.equal(selected.stdout.trim(), 'fixture');
});
51 changes: 34 additions & 17 deletions scripts/docs-generators/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import { execSync, spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { parseArgs } from 'node:util';

// ============================================================================
// Types
Expand Down Expand Up @@ -68,18 +69,34 @@ const SHARED_GENERATOR_FILES = new Set([
// ============================================================================

async function main() {
const args = process.argv.slice(2);

// Parse arguments
const checkOnly = args.includes('--check') || args.includes('--check-only');
const listOnly = args.includes('--list');
const changedOnly = args.includes('--changed');
const changedFilesFileIndex = args.indexOf('--changed-files-file');
const changedFilesFile =
changedFilesFileIndex !== -1 ? args[changedFilesFileIndex + 1] : undefined;
const testRouting = args.includes('--test-routing');
const libraryIndex = args.indexOf('--library');
const specificLibrary = libraryIndex !== -1 ? args[libraryIndex + 1] : null;
const { values } = parseArgs({
options: {
help: { type: 'boolean' },
list: { type: 'boolean' },
library: { type: 'string' },
check: { type: 'boolean' },
'check-only': { type: 'boolean' },
changed: { type: 'boolean' },
'changed-files-file': { type: 'string' },
'test-routing': { type: 'boolean' },
},
});
if (values.help) {
console.log(`Usage: pnpm --dir docs docs:generate [options]

--help Show this help without running generators
--list List configured generators
--library <name> Run one configured generator
--check, --check-only Check for documentation drift
--changed Select generators from changed files
--changed-files-file <path> Print generators selected by a changed-file list
--test-routing Verify generator routing`);
return;
}

const checkOnly = Boolean(values.check || values['check-only']);
const changedFilesFile = values['changed-files-file'];
const specificLibrary = values.library;

// Load config
if (!fs.existsSync(CONFIG_PATH)) {
Expand All @@ -89,12 +106,12 @@ async function main() {

const config: Config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));

if (testRouting) {
if (values['test-routing']) {
testGeneratorRouting(config);
return;
}

if (changedFilesFile) {
if (changedFilesFile !== undefined) {
if (!fs.existsSync(changedFilesFile)) {
console.error(`Changed-files input not found: ${changedFilesFile}`);
process.exit(1);
Expand All @@ -108,22 +125,22 @@ async function main() {
console.log('==================================\n');

// List mode
if (listOnly) {
if (values.list) {
listGenerators(config);
return;
}

// Determine which generators to run
let generatorsToRun: string[] = [];

if (specificLibrary) {
if (specificLibrary !== undefined) {
if (!config.generators[specificLibrary]) {
console.error(`❌ Unknown library: ${specificLibrary}`);
console.log('\nAvailable libraries:', Object.keys(config.generators).join(', '));
process.exit(1);
}
generatorsToRun = [specificLibrary];
} else if (changedOnly) {
} else if (values.changed) {
generatorsToRun = getChangedGenerators(config);
if (generatorsToRun.length === 0) {
console.log('✅ No documentation-related changes detected.');
Expand Down
Loading