Worked review 4: a library's tsconfig and package.json
The first three reviews were programs. This one is config — the tsconfig.json and package.json of a tiny library, wr4-lib, that exports one Logger class. Config bugs don’t show up as a wrong console output; they show up as a build that refuses to run, or a consumer whose require() call fails for a reason nothing in the source code explains. Every failure below is quoted from a real run in this course’s scratch environment — tsc -p, npm pack + @arethetypeswrong/cli, and an actual consumer calling both require() and import().
The source, tsconfig.json, and package.json
Section titled “The source, tsconfig.json, and package.json”export enum LogLevel { Info, Warn, Error }
export class Logger { constructor(public readonly level: LogLevel) {} log(message: string): void { console.log(`[${LogLevel[this.level]}] ${message}`); }}{ "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "bundler", "declaration": true, "outDir": "dist", "strict": true, "skipLibCheck": true }, "include": ["src"]}{ "name": "wr4-lib", "version": "1.0.0", "type": "module", "main": "./src/index.ts", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } }}Find the bugs before reading on
Section titled “Find the bugs before reading on”None of these three files individually looks obviously wrong — this is what a config review actually feels like. Try to name all four bugs before opening the answers.
Bug 1 — tsconfig.json has no rootDir
Symptom: tsc -p tsconfig.json refuses to build at all.
Real output:
tsconfig.json(7,5): error TS5011: The common source directory of 'tsconfig.json' is'./src'. The 'rootDir' setting must be explicitly set to this or another path toadjust your output's file layout. Visit https://aka.ms/ts6 for migration information.Evidence: with outDir set but no rootDir, tsc cannot decide how to mirror src/index.ts into dist/ without guessing — and TypeScript 7 turned that guess into a hard error instead of the silent dist/src/index.js layout older versions produced.
Fix: set rootDir explicitly:
{ "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "bundler", "rootDir": "src", "declaration": true, "outDir": "dist", "strict": true, "skipLibCheck": true }, "include": ["src"]}Bug 2 — "main": "./src/index.ts" points at raw source
Symptom: the package’s main field — the classic entry point every pre-exports tool still reads — points directly at TypeScript source, not a built file.
Evidence: modern Node ignores main entirely once exports is present, so this specific package still resolves correctly for import/require today — but any tool that resolves the classic way (an older bundler, a monorepo task runner that reads main directly, TypeScript’s own moduleResolution: "node10" for a consumer project) would be handed a .ts file no JavaScript runtime can execute directly. Confirmed directly: running that exact file with Node’s own TypeScript support fails immediately (see Bug 4) — precisely the failure a stray main field this pointed can hand to the wrong tool.
Fix: point main at the actual build output:
"main": "./dist/index.js"Bug 3 — exports has no require condition, but main implies one
Symptom: a main field has historically meant “this is how CommonJS consumers load this package” — but this package’s exports map offers no require condition at all, only import.
Real output (an actual consumer package, in scratch, calling both):
$ node -e "require('wr4-lib')"Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in.../node_modules/wr4-lib/package.json
$ node --input-type=module -e "import('wr4-lib').then(m => new m.Logger(m.LogLevel.Warn).log('hi'))"[Warn] hirequire() fails outright; dynamic import() works. @arethetypeswrong/cli --pack confirms the same gap from the package’s own published shape:
⚠️ A require call resolved to an ESM JavaScript file, which is an error in Node andsome bundlers. CommonJS consumers will need to use a dynamic import.
┌───────────────────┬──────────────────────────────┐│ node10 │ 🟢 │├───────────────────┼──────────────────────────────┤│ node16 (from CJS) │ ⚠️ ESM (dynamic import only) │├───────────────────┼──────────────────────────────┤│ node16 (from ESM) │ 🟢 (ESM) │├───────────────────┼──────────────────────────────┤│ bundler │ 🟢 │└───────────────────┴──────────────────────────────┘Fix: this package genuinely is ESM-only. The fix is not to fake CJS support with no build for it — that would need a real second build target (dist/index.cjs plus a .d.cts declaration, referenced from a "require" condition). The actual, honest fix here is to stop implying CJS support the package never had: once main correctly points at dist/index.js (Bug 2’s fix), the package is at least consistent about being ESM-only, and this warning becomes a documented, understood trade-off instead of an accidental side effect of a stray field — write it down in the README rather than leaving a consumer to discover it via a production crash. If real dual CJS/ESM support is required, the standard remedy is exactly the .d.mts/.d.cts split this course’s Build & Emit lesson covers.
Bug 4 — the source uses a numeric enum and a parameter property
Symptom: both compile perfectly under tsc --strict — this is exactly the erasable-syntax gap this course’s AI Bug Catalog covers (patterns 20 and 21).
Real output (node --experimental-strip-types src/index.ts, run directly on the source — exactly what Bug 2’s stray main field could hand a tool):
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only modeFix: an as const union instead of enum, and an explicit field instead of a parameter property — both erasable, no emit required beyond ordinary object/class syntax:
export const LogLevel = { Info: "Info", Warn: "Warn", Error: "Error" } as const;export type LogLevel = typeof LogLevel[keyof typeof LogLevel];
export class Logger { readonly level: LogLevel; constructor(level: LogLevel) { this.level = level; } log(message: string): void { console.log(`[${this.level}] ${message}`); }}The fixed config
Section titled “The fixed config”export const LogLevel = { Info: "Info", Warn: "Warn", Error: "Error" } as const;export type LogLevel = typeof LogLevel[keyof typeof LogLevel];
export class Logger { readonly level: LogLevel; constructor(level: LogLevel) { this.level = level; } log(message: string): void { console.log(`[${this.level}] ${message}`); }}{ "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "bundler", "rootDir": "src", "declaration": true, "outDir": "dist", "strict": true, "skipLibCheck": true }, "include": ["src"]}{ "name": "wr4-lib", "version": "1.0.0", "type": "module", "types": "./dist/index.d.ts", "main": "./dist/index.js", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }}Proof, real output, in order:
$ tsc -p tsconfig.json(no output -- exit 0)
$ node --experimental-strip-types src/index.ts(no output -- parses and runs cleanly, no enum/parameter-property syntax left to reject)
$ node -e "import('wr4-lib').then(m => new m.Logger(m.LogLevel.Warn).log('all good'))"[Warn] all goodtsc -p now builds without a single flag missing; the source itself is plain, erasable syntax that runs the same whether it goes through tsc, Node’s strip-types, or any other transform; and the package resolves and runs exactly as its exports map promises. The node16 (from CJS) warning from attw is unchanged — because this package genuinely, deliberately does not support require() — but it is no longer contradicted by a main field that implied otherwise.
What the tools caught
Section titled “What the tools caught”| Bug | Tool | Real signal |
|---|---|---|
1. Missing rootDir | tsc -p, directly | TS5011, refuses to build at all |
2. main points at source | Nothing automated | Only surfaces when some other tool actually tries to resolve via main |
3. No require condition | @arethetypeswrong/cli, directly; confirmed by a real require() call | CJSResolvesToESM warning; ERR_PACKAGE_PATH_NOT_EXPORTED |
4. enum / parameter property | Nothing via tsc --strict; only running the source | ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX under node --experimental-strip-types |
Only one of these four is a tsc error a build simply cannot ignore. The other three are exactly the kind of thing this module has trained you to check for by hand — or with attw, which exists specifically because nothing else in the ladder looks at a package from a consumer’s point of view.