fix: add .js extensions to relative imports (#238) - #239
Open
laazyj wants to merge 1 commit into
Open
Conversation
Since 4.0.5 the package is "type": "module", but the emitted .d.ts files still used extensionless relative specifiers. Under moduleResolution node16/nodenext those are invalid, so consumers get TS2834 on dist/types/index.d.ts -- or, with the common skipLibCheck default, no error at all while every graphlib export silently degrades to `any`. Add the .js extension to every relative specifier in lib/** and index.ts. Only index.ts's type-only re-export had one already, so the convention was already half-applied. The change is additive: extensionless specifiers were valid only in some resolution modes, whereas .js resolves in all of them. The runtime bundles are unaffected, as esbuild inlines every relative import. To keep the convention applied, compile tsconfig.build.json with nodenext. It already scopes itself to the shipped surface (lib/** and index.ts, excluding test/**), so the build now rejects a bad specifier at the source line rather than emitting declarations that break consumers. Output is byte-identical to the previous bundler-resolved emit. tsconfig.json stays on "bundler", leaving the extensionless imports under test/** and src/** untouched. Add test/dist-types.test.ts for the half a compile of this repo cannot cover: that a dependent resolving the package through node_modules and the "exports" map gets working types. It builds declarations into a temp package alongside the real package.json, then type-checks a consumer against it under nodenext with skipLibCheck off; a @ts-expect-error on a deliberate misuse catches the degrade-to-any case. Verified: * Reproduced first: a consumer under nodenext fails with TS2834, and with skipLibCheck on loses its types silently. Both clear after. * The new test and the nodenext build config each fail when a single specifier is reverted, so both guards are load-bearing. * Backward compatibility: a consumer type-checks clean under moduleResolution node10 (commonjs and esnext), bundler, node16 and nodenext, on TypeScript 4.0, 4.5, 5.0 and 5.9. Only "classic" fails, identically before and after, as it cannot resolve node_modules. * Runtime unchanged: no relative specifier survives into any bundle, the unminified bundle differs only by the pending version string, and CJS, ESM and both IIFE bundles smoke-test working. * make lint, make build and make test all pass (259 tests). dist/ is left to be regenerated by the maintainer's release build. Closes dagrejs#238
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.
Problem
Since 4.0.5 the package is
"type": "module", but the emitted.d.tsfiles still use extensionless relative specifiers (export { Graph } from './lib/graph'). UndermoduleResolutionnode16/nodenextthose are invalid, so a dependent gets:The bug has a quieter second face that is arguably worse. With
skipLibCheck: true— the common default — the error is silenced, but the declarations still fail to resolve and every graphlib export degrades toany. Type safety is gone at every call site with nothing on screen to say so.Reported in #238. Only
index.ts's type-only re-export of./lib/types.jsalready carried an extension, so the convention was already half-applied in the repo.Reproduction
A consumer project resolving
@dagrejs/graphlibthroughnode_modulesand theexportsmap,moduleResolution: nodenext:skipLibCheck: falsedist/types/index.d.tsskipLibCheck: trueconst n: number = g.nodes()type-checks —GraphisanyFix
1. Add
.jsto every relative specifier inlib/**andindex.ts. Purely mechanical, 54 lines, one shape:2. Compile
tsconfig.build.jsonwithnodenext(2 lines), so this cannot silently come back. That config already scopes itself to exactly the shipped surface (include: ["lib/**/*", "index.ts"],exclude: ["test/**/*"]), so the build now rejects a bad specifier at the source line rather than emitting declarations that break consumers:The emitted declarations are byte-identical to the previous bundler-resolved output — verified with
diff -r.tsconfig.jsonstays on"bundler", so the extensionless imports throughouttest/**andsrc/**are untouched and need no churn.I looked at enforcing this with ESLint instead, but no installed plugin has such a rule (it would mean adding
eslint-plugin-import), and it would be strictly weaker —import/extensionspattern-matches specifiers, whereas tsc actually resolves them.3. Add
test/dist-types.test.tsfor the half that compiling this repo cannot cover: that a dependent resolving the package throughnode_modulesand the"exports"map gets working types. That wiring can regress independently, as #233 showed. It builds declarations into a temp package next to a copy of the realpackage.json, then type-checks a consumer against it undernodenextwithskipLibCheckoff. The@ts-expect-erroron a deliberate misuse is what catches the degrade-to-anycase.The test is hermetic: it builds from current source rather than reading the committed
dist/, so it leaves the working tree alone and cannot pass on a stale artifact.Backward compatibility
The change is additive — extensionless specifiers were valid in only some resolution modes,
.jsresolves in all of them. Verified with a consumer matrix (skipLibCheckoff, plus a@ts-expect-errorproving types genuinely resolve rather than falling back toany):node(node10), commonjs and esnextbundlernode16,nodenextclassicnode_modulesat allAcross TypeScript 4.0.8, 4.5.5, 5.0.4 and 5.9.3 under node10 resolution: all pass.
Runtime is unaffected, since esbuild inlines every relative import — 0 relative specifiers survive into any bundle. The unminified
dist/graphlib.jsrebuilt from this branch differs from the committed one by only the pending4.0.5→4.0.6-preversion string, i.e. no logic change. CJSrequire(), ESMimportand both IIFE bundles smoke-test working.Validation
nodenextbuild.make lint,make build,make testall pass — 259 tests, 21 suites.Note on
dist/dist/is not included here. The history shows it is regenerated by the maintainer in the release build (prepublishOnlyrunsnpm run build), and contributor fixes such as #235 do not touch it; #237 only did because its fix was adistrename. The committeddist/typestherefore still carries the bug until the next release build regenerates it. Happy to commit the rebuiltdist/instead if you would prefer it in the PR — note that rebuilding also picks up the pending4.0.5→4.0.6-preversion string, since the committed bundles predate that bump.Closes #238