The ESM entry point is typed as CommonJS
Complexity: Medium (100 points)
Estimated time: 1 day
Context
packages/core/package.json declares:
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
}
One types entry serves both conditions. The package has no "type": "module", so
a .d.ts file is interpreted by TypeScript as a CommonJS declaration — and it
is being offered as the types for the ESM entry point too.
Under moduleResolution: "node16", "nodenext", or "bundler" — the settings any
modern consumer uses — TypeScript resolves the import condition, finds a CJS
declaration file, and the module's shape is wrong: default-import interop differs,
and named exports may not resolve the way the runtime actually behaves. attw calls
this "Masquerading as CJS" and it is one of the most common ways a dual-format
package ships broken types.
tsup is configured with dts: true and both formats, so the build very likely
already emits a matching index.d.mts that nothing points at. Check dist/ after
a build and confirm before you write the fix.
The exports map is also missing "./package.json", which several tools resolve
directly and which a strict exports map otherwise blocks.
None of this is caught anywhere, because nothing lints the published package.
Why this matters
Type errors from a bad exports map do not appear in this repository. They appear
in consumers' repositories, as confusing errors about a module having no default
export or no callable signature — and the consumer has no way to fix it. The
existing smoke test (packages/core/scripts/smoke-test.js) covers ESM import, CJS
require, and tsc resolution, but it runs tsc with --moduleResolution node, the
legacy algorithm, which does not consult exports at all. It cannot catch this.
Where this lives
packages/core/package.json (exports, and a lint script)
packages/core/tsup.config.ts — only if the .d.mts is not being emitted
packages/core/scripts/smoke-test.js (resolution mode)
.github/workflows/ci.yml
Implementation guidelines
-
Look at dist/ first. Run pnpm build and list what is actually emitted.
Write the fix against that, not against what this issue predicts.
-
Split the types conditions:
"exports": {
".": {
"import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" },
"require": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
},
"./package.json": "./package.json"
}
Condition order matters — types must come first within each branch, and
default last. If tsup is not emitting index.d.mts, configure it to before
changing the map; pointing at a file that does not exist is a worse failure than
the one you started with.
-
Keep the top-level main, module and types fields. They are the fallback
for bundlers and toolchains that ignore exports. Removing them is a breaking
change for older consumers and gains nothing.
-
Add the two tools that check this automatically:
publint — validates the manifest and file layout
@arethetypeswrong/cli (attw --pack .) — resolves the package under every
module mode and reports exactly this class of bug
Wire both into a lint:package script and run it in CI after the build. These
two commands are the durable part of this issue; the manifest edit is the easy
part.
-
Fix the smoke test's resolution mode. It currently runs
tsc --moduleResolution node, which bypasses exports entirely. Add runs under
node16 and bundler so it exercises the map it is supposed to be validating.
-
Do not change what is exported from src/index.ts — this issue is about how the
built artifact is described, not about the public API.
Acceptance criteria
Reference
Important rules — read before you start
- Get assigned first. Do not open a PR before you are assigned. Unassigned PRs
are closed without review.
- Target the
dev branch. Branch from dev and open your PR against dev.
PRs opened against main will not be merged.
- Make sure CI/CD passes. Run
pnpm lint, pnpm typecheck, pnpm test, and
pnpm build locally and confirm green before pushing.
- Pull before you push.
git pull --rebase origin dev right before pushing.
- Do not touch files outside your task. Only the files under "Where this
lives". Do not reformat, rename, or delete unrelated files.
- Follow existing conventions — match the surrounding config.
- Use testnet only in every example and test. Never hardcode a mainnet address.
- Check the references above before writing code. If the README and the source
disagree, the source wins.
- Do not open a draft PR to ask questions — ask in the issue comments.
The ESM entry point is typed as CommonJS
Complexity: Medium (100 points)
Estimated time: 1 day
Context
packages/core/package.jsondeclares:One
typesentry serves both conditions. The package has no"type": "module", soa
.d.tsfile is interpreted by TypeScript as a CommonJS declaration — and itis being offered as the types for the ESM entry point too.
Under
moduleResolution: "node16","nodenext", or"bundler"— the settings anymodern consumer uses — TypeScript resolves the
importcondition, finds a CJSdeclaration file, and the module's shape is wrong: default-import interop differs,
and named exports may not resolve the way the runtime actually behaves.
attwcallsthis "Masquerading as CJS" and it is one of the most common ways a dual-format
package ships broken types.
tsup is configured with
dts: trueand both formats, so the build very likelyalready emits a matching
index.d.mtsthat nothing points at. Checkdist/aftera build and confirm before you write the fix.
The
exportsmap is also missing"./package.json", which several tools resolvedirectly and which a strict
exportsmap otherwise blocks.None of this is caught anywhere, because nothing lints the published package.
Why this matters
Type errors from a bad
exportsmap do not appear in this repository. They appearin consumers' repositories, as confusing errors about a module having no default
export or no callable signature — and the consumer has no way to fix it. The
existing smoke test (
packages/core/scripts/smoke-test.js) covers ESM import, CJSrequire, and
tscresolution, but it runstscwith--moduleResolution node, thelegacy algorithm, which does not consult
exportsat all. It cannot catch this.Where this lives
packages/core/package.json(exports, and a lint script)packages/core/tsup.config.ts— only if the.d.mtsis not being emittedpackages/core/scripts/smoke-test.js(resolution mode).github/workflows/ci.ymlImplementation guidelines
Look at
dist/first. Runpnpm buildand list what is actually emitted.Write the fix against that, not against what this issue predicts.
Split the types conditions:
Condition order matters —
typesmust come first within each branch, anddefaultlast. If tsup is not emittingindex.d.mts, configure it to beforechanging the map; pointing at a file that does not exist is a worse failure than
the one you started with.
Keep the top-level
main,moduleandtypesfields. They are the fallbackfor bundlers and toolchains that ignore
exports. Removing them is a breakingchange for older consumers and gains nothing.
Add the two tools that check this automatically:
publint— validates the manifest and file layout@arethetypeswrong/cli(attw --pack .) — resolves the package under everymodule mode and reports exactly this class of bug
Wire both into a
lint:packagescript and run it in CI after the build. Thesetwo commands are the durable part of this issue; the manifest edit is the easy
part.
Fix the smoke test's resolution mode. It currently runs
tsc --moduleResolution node, which bypassesexportsentirely. Add runs undernode16andbundlerso it exercises the map it is supposed to be validating.Do not change what is exported from
src/index.ts— this issue is about how thebuilt artifact is described, not about the public API.
Acceptance criteria
pnpm buildoutput is inspected and the fix matches what is actually emittedimportandrequireconditions each carry their owntypes"./package.json"is exportedmain,moduleandtypesare still presentattw --pack .reports no problems for CJS, ESM,node16andbundlerpublintpassesnode16andbundlersrc/index.tsis unchangedpnpm test,pnpm lint,pnpm typecheck, andpnpm buildall pass locallyCloses #[issue number]devbranch — work pushed tomain(or anybranch other than
dev) will not be mergedappreciated
then is automatically unassigned so the task can go to someone else
Reference
packages/core/package.jsonpackages/core/tsup.config.tspackages/core/scripts/smoke-test.jsci-08(runs the smoke test at release),pkg-04(build settings)attw: https://github.com/arethetypeswrong/arethetypeswrong.github.iopublint: https://publint.devImportant rules — read before you start
are closed without review.
devbranch. Branch fromdevand open your PR againstdev.PRs opened against
mainwill not be merged.pnpm lint,pnpm typecheck,pnpm test, andpnpm buildlocally and confirm green before pushing.git pull --rebase origin devright before pushing.lives". Do not reformat, rename, or delete unrelated files.
disagree, the source wins.