Skip to content

Build: Normalize import extensions in ESM package type declarations - #80606

Draft
i-am-chitti wants to merge 1 commit into
WordPress:trunkfrom
i-am-chitti:fix/esm-declaration-extensions
Draft

Build: Normalize import extensions in ESM package type declarations#80606
i-am-chitti wants to merge 1 commit into
WordPress:trunkfrom
i-am-chitti:fix/esm-declaration-extensions

Conversation

@i-am-chitti

Copy link
Copy Markdown
Contributor

What?

Adds a shared post-tsc build step that appends explicit file extensions to the relative import/export specifiers in the compiled type declarations of "type": "module" packages, so the published .d.ts graph resolves under Node-style ESM (node16/nodenext) and not just bundler.

Why?

tsc emits declaration specifiers verbatim from source, and the shared config authors extensionless relative imports (valid under moduleResolution: "bundler"). Because these packages are "type": "module", Node interprets the emitted .d.ts
as ESM, where relative specifiers require file extensions. As reported in #80206, @arethetypeswrong/cli (attw) flags this as an InternalResolutionError for @wordpress/theme, @wordpress/design-system-mcp, and @wordpress/video-conversion (and @wordpress/vips):

"@wordpress/theme"
node16 (from ESM): 🥴 Internal resolution error
bundler: 🟢

Notably, the runtime half of the pipeline already handles this — esbuild emits ./private-apis.mjs in build-module. The defect is an asymmetry: the JS emit normalizes extensions, the declaration emit does not. This PR closes that gap in
shared tooling rather than per-package.

How?

  • New tools/build-scripts/packages/normalize-declaration-extensions.mjs, run right after tsc --build in build.mjs.
  • For each "type": "module" package with a build-types/ directory, it resolves every relative specifier against the emitted declaration graph and rewrites it to the runtime counterpart of its target:
    • ./foo./foo.js (target foo.d.ts)
    • ./foo./foo.mjs / .cjs (target foo.d.mts / foo.d.cts)
    • ./dir./dir/index.js (directory targets)
  • Handles from, side-effect import, and inline/dynamic import(...) specifiers; skips bare (package) specifiers and specifiers that already carry an extension (idempotent).
  • Source is left untouched — imports stay extensionless, so Jest, Storybook, webpack, and the bundler-based type check are unaffected. The rewrite only targets the generated (gitignored) build-types artifacts, mirroring esbuild's existing .mjs handling.
  • Unit tests in normalize-declaration-extensions.test.js.

Design notes / trade-offs

  • This is one of the tactical options from Establish a shared ESM declaration strategy for WordPress packages #80206. The alternative (explicit .ts/.tsx source extensions via allowImportingTsExtensions) is also viable and would make the change visible in source, but touches imports repo-wide. Both are compatible with — and superseded by — a futuretsdown/tsup dual-build migration.
  • Because the step runs last, a bare tsc --build on its own still emits extensionless declarations locally; the normalization is applied by npm run build (the published path). Only tsc writes build-types, so nothing after the step clobbers it.

Testing Instructions

  1. Build declarations for an affected package: npx tsc --build packages/theme/tsconfig.json --force
  2. Confirm the raw output has extensionless specifiers (head -1 packages/theme/build-types/index.d.ts).
  3. Run the normalizer: node tools/build-scripts/packages/normalize-declaration-extensions.mjs
  4. The specifiers now carry .js (e.g. from './private-apis.js').
  5. Validate with attw — node16 (from ESM) should be green with 0 internal resolution errors: npx -y @arethetypeswrong/cli@0.18.4 --pack packages/theme --profile esm-only
  6. Repeat for design-system-mcp, video-conversion, vips.
  7. Run the unit tests: npm run test:unit -- tools/build-scripts/packages/normalize-declaration-extensions.test.js

Verified: a consumer importing @wordpress/theme type-checks cleanly under bundler, node16, and nodenext after normalization.

Screenshots or screencast

Not applicable.

Use of AI Tools

Claude Code: Opus 4.8
Investigate and

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant