|
| 1 | +// The tsconfig presets are a published contract, so they get the same guard the ESLint presets do. |
| 2 | +// |
| 3 | +// `strict: true` alone does NOT enable `noUncheckedIndexedAccess` — it is opt-in, and it is the one |
| 4 | +// flag that makes `arr[i]` / `record[key]` honestly `T | undefined`. Without it TypeScript silently |
| 5 | +// types `const [row] = await db.insert(...).returning()` as non-nullable, so `row.id` type-checks |
| 6 | +// and throws at runtime on an empty result. Assert it (and the rest of the floor) so a future edit |
| 7 | +// cannot quietly weaken the bar. |
| 8 | + |
| 9 | +import assert from 'node:assert/strict'; |
| 10 | +import { readFileSync } from 'node:fs'; |
| 11 | +import path from 'node:path'; |
| 12 | +import { test } from 'node:test'; |
| 13 | +import { fileURLToPath } from 'node:url'; |
| 14 | + |
| 15 | +const here = path.dirname(fileURLToPath(import.meta.url)); |
| 16 | +const readConfig = (name) => |
| 17 | + JSON.parse(readFileSync(path.join(here, '..', 'tsconfig', name), 'utf8')); |
| 18 | + |
| 19 | +const PRESETS = ['react.json', 'react-native.json', 'next.json', 'nest.json']; |
| 20 | + |
| 21 | +/** |
| 22 | + * The type-SAFETY floor: what makes the compiler catch bugs. No preset may weaken these — doing so |
| 23 | + * is a breaking change to consumers. |
| 24 | + */ |
| 25 | +const SAFETY_FLOOR = { |
| 26 | + strict: true, |
| 27 | + noUncheckedIndexedAccess: true, |
| 28 | + noImplicitReturns: true, |
| 29 | + noFallthroughCasesInSwitch: true, |
| 30 | + noUnusedLocals: true, |
| 31 | + noUnusedParameters: true, |
| 32 | + forceConsistentCasingInFileNames: true, |
| 33 | +}; |
| 34 | + |
| 35 | +/** |
| 36 | + * Module MECHANICS: the base sets these, but a framework may legitimately override them (NestJS |
| 37 | + * turns `isolatedModules`/`verbatimModuleSyntax` off because `emitDecoratorMetadata` needs the |
| 38 | + * full-program, CommonJS decorator emit). They are not a safety bar, so they are asserted on the |
| 39 | + * base only — never on the presets. |
| 40 | + */ |
| 41 | +const BASE_MECHANICS = { |
| 42 | + isolatedModules: true, |
| 43 | + verbatimModuleSyntax: true, |
| 44 | +}; |
| 45 | + |
| 46 | +test('base: sets the full type-safety floor', () => { |
| 47 | + const { compilerOptions } = readConfig('base.json'); |
| 48 | + for (const [flag, expected] of Object.entries(SAFETY_FLOOR)) { |
| 49 | + assert.equal( |
| 50 | + compilerOptions[flag], |
| 51 | + expected, |
| 52 | + `tsconfig/base.json must set ${flag}: ${expected}` |
| 53 | + ); |
| 54 | + } |
| 55 | +}); |
| 56 | + |
| 57 | +test('base: sets the module mechanics defaults', () => { |
| 58 | + const { compilerOptions } = readConfig('base.json'); |
| 59 | + for (const [flag, expected] of Object.entries(BASE_MECHANICS)) { |
| 60 | + assert.equal( |
| 61 | + compilerOptions[flag], |
| 62 | + expected, |
| 63 | + `tsconfig/base.json must set ${flag}: ${expected}` |
| 64 | + ); |
| 65 | + } |
| 66 | +}); |
| 67 | + |
| 68 | +// The framework presets inherit the floor rather than restating it, so `extends` IS the contract: |
| 69 | +// if one stops extending base, it silently loses noUncheckedIndexedAccess and the rest. |
| 70 | +for (const preset of PRESETS) { |
| 71 | + test(`${preset}: extends base`, () => { |
| 72 | + assert.equal(readConfig(preset).extends, './base.json', `${preset} must extend ./base.json`); |
| 73 | + }); |
| 74 | + |
| 75 | + test(`${preset}: does not weaken the type-safety floor`, () => { |
| 76 | + const { compilerOptions = {} } = readConfig(preset); |
| 77 | + for (const [flag, expected] of Object.entries(SAFETY_FLOOR)) { |
| 78 | + if (flag in compilerOptions) { |
| 79 | + assert.equal( |
| 80 | + compilerOptions[flag], |
| 81 | + expected, |
| 82 | + `${preset} must not weaken ${flag} inherited from base` |
| 83 | + ); |
| 84 | + } |
| 85 | + } |
| 86 | + }); |
| 87 | +} |
0 commit comments