Files
TREK/server/scripts/dto-open-shapes.mjs
Maurice a0e1a39acc ci(server): ratchet the open shapes in every schema a DTO binds
contracts:open in shared only counts exports named *RequestSchema, but
the name is a convention: 37 body DTOs wrap a schema called *BodySchema,
*InputSchema or plain *Schema, and tags.dto.ts declares its schemas
locally. pluginInstanceConfigUpdateSchema (a record of unknown behind
PluginConfigDto), createAlbumLinkSchema with five open shapes and a dozen
more were never counted, and a new open body under one of those names
passed both checks.

tests/unit/contracts/dto-open-shapes.test.ts walks the other way round.
It finds every class that extends createZodDto under src/, imports it,
and counts the open shapes in the schema the Zod pipe validates with
shared's walk. Each class is held to scripts/dto-open-shapes-baseline.json,
61 DTOs with 101 open shapes today, and a new DTO may hold none. It fails
closed: a createZodDto call that is not an exported class, two DTO
classes with one name, an empty tree or an unreadable baseline stop it.
It runs with the unit tests; npm run contracts:dto-open runs it on its
own and --update only lowers the baseline.

The five DTO classes declared inside roadtrip controllers are exported so
the walk can reach their schemas. The selector test picks up prettier's
wrapping of the cases added before.
2026-10-08 13:04:39 +02:00

49 lines
2.1 KiB
JavaScript

#!/usr/bin/env node
/*
* contracts:dto-open: runs tests/unit/contracts/dto-open-shapes.test.ts, the
* ratchet over the open shapes in every createZodDto class under src/.
*
* The check has to import the TypeScript DTO files, so it lives in that test
* and runs with the unit tests in CI; this wrapper only gives it a command and
* the lowering mode:
*
* npm run contracts:dto-open check against scripts/dto-open-shapes-baseline.json
* npm run contracts:dto-open -- --update lower the baseline to today's counts; it never
* raises or adds an entry
*
* The shared package must be built first (the DTOs import @trek/shared).
*/
import { spawnSync } from 'node:child_process';
import { existsSync, realpathSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const SERVER_ROOT = fileURLToPath(new URL('..', import.meta.url));
const TEST = 'tests/unit/contracts/dto-open-shapes.test.ts';
function main(argv) {
const unknown = argv.filter((arg) => arg !== '--update');
if (unknown.length) throw new Error(`unknown argument(s): ${unknown.join(' ')}`);
if (!existsSync(join(SERVER_ROOT, TEST))) throw new Error(`${TEST} is missing`);
const vitest = join(dirname(createRequire(import.meta.url).resolve('vitest/package.json')), 'vitest.mjs');
const env = { ...process.env, DTO_OPEN_SHAPES_UPDATE: argv.includes('--update') ? '1' : '0' };
const result = spawnSync(process.execPath, [vitest, 'run', TEST], { cwd: SERVER_ROOT, env, stdio: 'inherit' });
if (result.error) throw result.error;
return result.status ?? 1;
}
// Compared by real path, so the check still runs when the script is started through a symlink.
const isCli =
Boolean(process.argv[1]) &&
existsSync(process.argv[1]) &&
realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
if (isCli) {
try {
process.exitCode = main(process.argv.slice(2));
} catch (err) {
console.error(`FAIL ${err.message}`);
process.exitCode = 1;
}
}