fix: give the published declarations explicit .js extensions - #78
Merged
Merged
Conversation
`@rollup/plugin-typescript` emits one `.d.ts` per source module and keeps the relative specifiers
extensionless, while the package is `"type": "module"`. A consumer on `moduleResolution: node16` or
`nodenext` with `skipLibCheck: false` therefore fails inside this package's own declarations:
dist/index.d.ts(1,39): error TS2834: Relative import paths need explicit file extensions in
ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'.
dist/index.d.ts(2,56): error TS2835: … Did you mean './maxrects-packer.mjs'?
Measured before the fix, compiling the documented imports by package name: `node16` and `nodenext`
each reported 5 errors, `bundler` and `node10` passed, and `nodenext` passed only with
`skipLibCheck: true`. After the fix all of them pass with `skipLibCheck: false`.
- `scripts/fix-declaration-extensions.mjs` (new, from `postbuild`) rewrites the relative specifiers
in `dist/**/*.d.ts` to end in `.js` — the ESM-correct form, since TypeScript maps `./x.js` to
`./x.d.ts` and the runtime artifacts are bundles without relative imports at all. It fails loudly on
a specifier that has no sibling declaration and on anything it could not rewrite, rather than
writing a path that resolves nowhere, and it is idempotent (a second run rewrites nothing).
- `scripts/verify-package.mjs` compiles the type fixture under **both** `bundler` and `nodenext`
now, with the consumer project marked `"type": "module"` so nodenext reads the fixture as ESM.
Verified to have teeth: with one `.js` extension removed from `dist/index.d.ts`, the gate reports
the TS2834 message and exits non-zero.
- `AGENTS.md` documents the step and the measured matrix; the corresponding item is gone from
`DEFERRED_WORK.md`.
No runtime artifact changes: 17 specifiers in 5 declaration files, no `.js`/`.mjs`/`.cjs` bundle
touched. `node10` (and TypeScript 4.6 with it) resolves `./x.js` to `./x.d.ts` as well, so legacy
consumers are unaffected; `npm test`, `cover` (100% on all four metrics) and the rest of the gates
pass.
|
The PR promises `node16` and `nodenext` both compile the published declarations with `skipLibCheck: false`, but only `nodenext` was in the gate's MODES. `node16` joins it, so the promise is enforced rather than measured once. The fixture is compiled in order `bundler`, `node16`, `nodenext`, and the success line lists all three. Verified to have teeth on its own entry, not just through the loop: removing one `.js` extension from `dist/index.d.ts` makes the gate fail under `node16` (the first strict mode it reaches) with `error TS2834 … when '--moduleResolution' is 'node16' or 'nodenext'`. `node10` stays ungated and documented as measured by hand in AGENTS.md, with the reason: the extension only ever had to be `.js` rather than the `.mjs` TS2835 suggests, so a wrong fix there would only be caught by re-measuring it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the
node16/nodenextitem inDEFERRED_WORK.md: the published declarations were written in a form only a tolerant resolver accepts, so the strictest consumers failed inside this package's own files.The defect, measured
Compiling the documented imports by package name against the built package, before the fix:
skipLibCheck: false)bundler(Vite/webpack)node16nodenextnodenextwithskipLibCheck: truenode10+ CommonJSnode10+ CommonJS on TypeScript 4.6The cause is structural:
@rollup/plugin-typescriptemits one declaration per source module and keeps the relative specifiers extensionless, while the package is"type": "module".The fix
scripts/fix-declaration-extensions.mjs, run frompostbuild, rewrites relative specifiers indist/**/*.d.tsto end in.js— the ESM-correct form: TypeScript maps./x.jsto./x.d.ts, and the runtime artifacts are self-contained bundles with no relative imports, so the extension only ever has to resolve as a declaration. 17 specifiers in 5 files; no.js/.mjs/.cjsartifact is touched.The script is defensive in the two ways that matter for a build step: it refuses to write a specifier that has no sibling declaration to resolve to (that would ship a broken path silently), and after rewriting it asserts nothing was left extensionless. Both were exercised on purpose —
export * from "./missing"producesError: …/dist/__probe.d.ts: "./missing" has no sibling declaration to resolve to. A second run is a no-op.The gate
scripts/verify-package.mjsnow compiles the type fixture underbundler,node16andnodenext, withskipLibCheck: false, in a consumer project marked"type": "module"so the two strict modes read the fixture as ESM — so every mode this description claims is a continuous CI gate, not a one-off measurement. Teeth verified per entry by removing one.jsfromdist/index.d.ts: the gate prints the TS2834 message and exits non-zero, and becausenode16is the first strict mode it reaches, the failure is reported asunder node16.node10stays ungated on purpose — it never needed the extension, and a wrong.mjsfix would only show up there (seeAGENTS.md).Compatibility
The extension is not a
node10problem: that resolver maps./x.jsto./x.d.tstoo, which the table above pins with TypeScript 4.6 — the oldest version anyone is realistically still building against. Noexportsfield is involved, so deep imports keep working exactly as before.AGENTS.mddocuments the step, the matrix and the "three gates" wording; the item is removed fromDEFERRED_WORK.md. Local gates:lint0/0,format:check,typecheck,npm test(98 passed / 2 skipped, exercising the postbuild step end to end),cover— still 100% on statements, branches, functions and lines — andverify:package.