test: lock in strict base tsconfig for all packages (E00-S01-T10)
CI / Frozen lockfile install (pull_request) Successful in 45s
CI / Frozen lockfile install (pull_request) Successful in 45s
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Strict tsconfig test — locks in the [E00-S01-T10] strict base tsconfig
|
||||
* committed at the workspace root and shared by every workspace package.
|
||||
*
|
||||
* Acceptance criteria covered (each test fails without the committed config):
|
||||
* - "strict base tsconfig is committed" → root `tsconfig.base.json` exists
|
||||
* and carries `strict: true` plus the strict family the workspace relies
|
||||
* on (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`,
|
||||
* `noImplicitOverride`, `useUnknownInCatchVariables`,
|
||||
* `verbatimModuleSyntax`) and NodeNext module resolution; every workspace
|
||||
* package (apps/server, packages/core, extensions/example) commits a
|
||||
* `tsconfig.json` that extends that root base config.
|
||||
* - "all packages compile under the strict base config" → running
|
||||
* `tsc -p <pkg>/tsconfig.json --noEmit` with the workspace-resolved
|
||||
* TypeScript (exact 6.0.3, locked in by E00-S01-T09) exits 0 for every
|
||||
* workspace package.
|
||||
* - (non-vacuousness) a scratch file that violates the strict family fails
|
||||
* to compile under the committed base config with the expected
|
||||
* strict-family error codes, proving the flags are enforced rather than
|
||||
* merely declared.
|
||||
*
|
||||
* Run: `node --test tests/strict-tsconfig.test.mjs`
|
||||
* (node:test — built into Node >= 18; no dependencies, lockfile untouched.)
|
||||
*/
|
||||
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8');
|
||||
|
||||
/** The workspace packages that must compile under the strict base config. */
|
||||
const WORKSPACE_PACKAGES = ['apps/server', 'packages/core', 'extensions/example'];
|
||||
|
||||
/** The strict-family flags the committed base config must set. */
|
||||
const STRICT_FAMILY_FLAGS = [
|
||||
'strict',
|
||||
'noUncheckedIndexedAccess',
|
||||
'exactOptionalPropertyTypes',
|
||||
'noImplicitOverride',
|
||||
'useUnknownInCatchVariables',
|
||||
'verbatimModuleSyntax',
|
||||
];
|
||||
|
||||
/** The TypeScript compiler resolved by the frozen install (exact 6.0.3, E00-S01-T09). */
|
||||
const TSC_BIN = path.join(REPO_ROOT, 'node_modules', 'typescript', 'bin', 'tsc');
|
||||
|
||||
/**
|
||||
* Runs the workspace-resolved TypeScript against a tsconfig (the same binary
|
||||
* the package `typecheck` scripts use; see E00-S01-T05/T09), executed by the
|
||||
* Node runtime running this test so it works from any cwd.
|
||||
*/
|
||||
function runTsc(cwd, args) {
|
||||
return spawnSync(process.execPath, [TSC_BIN, ...args], {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
timeout: 120_000,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('tsconfig.base.json is committed with the strict family enabled', () => {
|
||||
const base = JSON.parse(read('tsconfig.base.json'));
|
||||
assert.ok(
|
||||
base && typeof base === 'object' && base.compilerOptions,
|
||||
'tsconfig.base.json must be a committed JSON object declaring compilerOptions',
|
||||
);
|
||||
for (const flag of STRICT_FAMILY_FLAGS) {
|
||||
assert.equal(
|
||||
base.compilerOptions[flag],
|
||||
true,
|
||||
`tsconfig.base.json must set "${flag}": true (strict family)`,
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
base.compilerOptions.module,
|
||||
'NodeNext',
|
||||
'tsconfig.base.json must set module to NodeNext (ESM boundary source of truth)',
|
||||
);
|
||||
assert.equal(
|
||||
base.compilerOptions.moduleResolution,
|
||||
'NodeNext',
|
||||
'tsconfig.base.json must set moduleResolution to NodeNext',
|
||||
);
|
||||
assert.equal(
|
||||
base.compilerOptions.target,
|
||||
'ES2023',
|
||||
'tsconfig.base.json must set target to ES2023',
|
||||
);
|
||||
});
|
||||
|
||||
test('every workspace package tsconfig.json extends the committed base config', () => {
|
||||
for (const pkg of WORKSPACE_PACKAGES) {
|
||||
const tsconfig = JSON.parse(read(path.join(pkg, 'tsconfig.json')));
|
||||
assert.equal(
|
||||
tsconfig.extends,
|
||||
'../../tsconfig.base.json',
|
||||
`"${pkg}/tsconfig.json" must extend the committed root tsconfig.base.json`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('every workspace package compiles under the strict base config (tsc --noEmit)', () => {
|
||||
for (const pkg of WORKSPACE_PACKAGES) {
|
||||
const result = runTsc(path.join(REPO_ROOT, pkg), ['-p', 'tsconfig.json', '--noEmit']);
|
||||
assert.equal(
|
||||
result.status,
|
||||
0,
|
||||
`"${pkg}" must compile under the strict base config:\n${(result.stderr || result.stdout || '').trim()}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('the committed base config enforces the strict family (non-vacuous probe)', () => {
|
||||
const scratch = mkdtempSync(path.join(tmpdir(), 'strict-base-probe-'));
|
||||
try {
|
||||
writeFileSync(
|
||||
path.join(scratch, 'tsconfig.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
extends: path.join(REPO_ROOT, 'tsconfig.base.json'),
|
||||
compilerOptions: { noEmit: true },
|
||||
include: ['.'],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(scratch, 'probe.ts'),
|
||||
[
|
||||
'// Each line below violates one strict-family flag of tsconfig.base.json.',
|
||||
'function implicitAny(param) { return param; } // TS7006 - noImplicitAny (strict)',
|
||||
'const items: string[] = [];',
|
||||
'const first = items[0].toUpperCase(); // TS2532 - noUncheckedIndexedAccess',
|
||||
'interface Options { port?: number; }',
|
||||
'const opts: Options = { port: undefined }; // TS2375 - exactOptionalPropertyTypes',
|
||||
'class Base { greet(): string { return "hi"; } }',
|
||||
'class Derived extends Base { greet(): string { return "yo"; } } // TS4114 - noImplicitOverride',
|
||||
'try { JSON.parse(""); } catch (err) { const m = err.message; } // TS18046 - useUnknownInCatchVariables',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
const result = runTsc(scratch, ['-p', 'tsconfig.json', '--noEmit']);
|
||||
assert.notEqual(
|
||||
result.status,
|
||||
0,
|
||||
'a file violating the strict family must fail to compile under the committed base config',
|
||||
);
|
||||
const output = (result.stderr || result.stdout || '');
|
||||
for (const code of ['TS7006', 'TS2532', 'TS2375', 'TS4114', 'TS18046']) {
|
||||
assert.ok(
|
||||
output.includes(code),
|
||||
`probe output must contain ${code} (strict family not enforced):\n${output}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
rmSync(scratch, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user