Skip to content

fix(core): add exports map and dual declarations for ESM consumers - #78

Open
PerryLink wants to merge 1 commit into
shigma:mainfrom
PerryLink:fix/node-next-exports
Open

PerryLink wants to merge 1 commit into
shigma:mainfrom
PerryLink:fix/node-next-exports

Conversation

@PerryLink

Copy link
Copy Markdown

Problem

schemastery@3.18.0 has no exports map, no types field and no type:module.
Its only declaration file ends with export = Schema while the ESM entry
(lib/index.mjs) exposes Schema as its default export, so ESM consumers
cannot describe the runtime they get. Full evidence matrix: see the linked
issue #77.

Approach

Runtime and source stay untouched. Add an exports map that wires each
condition to a declaration matching its runtime shape:

import  -> lib/index.d.mts  (export default Schema; export type { Schema };)
require -> lib/index.d.cts  (verbatim copy of today's index.d.ts)

Both files are derived from the tsc-emitted index.d.ts by the new
scripts/dual-types.mjs, which fails loudly if the expected trailing
export = Schema; is ever absent. The top-level types field points at
index.d.cts, so legacy resolvers see the same shape as today. type:module
is safe: the package ships no bare .js files.

Why not a manifest-only exports map (single index.d.ts)? Measured result:
default imports regress to TS1192 and import = require to TS1471 - an
export = declaration cannot describe an ESM entry, so the dual
declarations are required.

Why not change the source to export default Schema? That would change the
emitted JS: esbuild would wrap the CJS output so require() consumers get
{ default: Schema } instead of Schema. Splitting only the declaration layer
avoids every runtime change.

Evidence (measured, not assumed)

Red, current 3.18.0 tarball, tsc 5.9.3, strict + verbatimModuleSyntax:

NodeNext: named import { Schema } -> TS2595; type-only named -> TS2595
bundler:  named import -> TS2596
node10:   default import -> TS1259; named import -> TS2617

Green, packed build from this branch (tsc 5.9.3 + esbuild 0.28, the repo's
own build settings, then npm pack):

NodeNext: default import OK; Schema<number> generic via default import
          OK; import type { Schema } OK; import = require OK;
          named value import rejected (TS1485/TS1362) - honest, the
          runtime .mjs has no named export
bundler:  default and type imports OK
node10:   identical to today (TS1259 boundary documented in the issue)
runtime:  node ESM `import Schema from 'schemastery'` works;
          node CJS `require('schemastery')` returns Schema directly
          (no .default wrapper)

Checklist

  • yarn build && yarn test (I could not run the yakumo pipeline here:
    yarn is not installed in this environment; I validated with
    npx tsc --build packages/core + esbuild + npm pack instead)
  • npm pack includes lib/index.d.cts and lib/index.d.mts

Note

Named exports from the ESM entry remain unavailable (the .mjs exposes only
a default export). Adding them needs a source export refactor - left out of
this PR by design; see issue #77 for the follow-up.

The runtime build already ships dual entries (lib/index.cjs with module.exports = Schema, lib/index.mjs with a default export), but package.json has no exports map and the only declaration file uses the CJS export assignment shape, so NodeNext/bundler consumers cannot describe the ESM entry.

- package.json (core): add type:module, types, and an exports map wiring index.d.mts to the import condition and index.d.cts to the require condition.

- scripts/dual-types.mjs: derive index.d.cts (verbatim, keeps export = Schema) and index.d.mts (export default Schema + type-only export of the Schema alias) from the tsc-emitted index.d.ts.

- package.json (root): run the dual-types step as part of build.

This branch has not been deployed

No deployments
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