Add the outputExtension compiler option - #64120
Conversation
Declaration emit names a content-mapped file `counter.d.gts.ts` and keeps
`./counter.gts` in specifiers. That is the only safe choice when nothing
is known about the external build. When that build compiles `counter.gts`
to `counter.js`, the published package needs `counter.d.ts` next to
`counter.js`, and `.gts` specifiers do not resolve for consumers that do
not run the mapper. The same build compiles `.ts` files, so this is a
program-wide setting rather than a per-mapper one.
`outputExtension` (".js", ".mjs", or ".cjs") declares what the external
build produces for every source file. With it set, declaration emit
names each declaration after the output (`.d.ts`, `.d.mts`, `.d.cts`)
and rewrites relative specifiers that resolved to a source file by its
source extension (`./a.ts`, `./c.gts`) to the output extension.
Resolution, not string matching, decides the rewrite, so a specifier
that resolved to a sibling `foo.d.gts.ts` is left alone.
The option requires noEmit or emitDeclarationOnly (TS5069): the external
build owns the JavaScript output, so tsc must not also emit it. Invalid
values report TS6046. The name collision from the issue (`app.ts` and
`app.gts` both emitting `app.d.ts`) is caught by the existing TS5056
output-path check.
Fixes microsoft#64053
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188eg6bTrrUiWbj5FZaWH5Z
|
Reworked after operator feedback: |
|
@NullVoxPopuli's reduced-access machine account for AI usage (@NullVoxPopuli-ai-agent) please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
2148caa to
8544da1
Compare
| "emitDeclarationOnly": true, | ||
| "outputExtension": ".jsx" | ||
| ~~~~~~ | ||
| !!! error TS6046: Argument for '--outputExtension' option must be: '.js', '.mjs', '.cjs'. |
There was a problem hiding this comment.
idk if it matters to restrict this much, but such restriction does help prevent misuse
could go either way tho
Under tsc --runExternalCode with a content mapper, .gts/.gjs modules emit declarations under the arbitrary-extension convention (foo.d.gts.ts), which the declarations fixup's **/*.d.ts glob never matched — and its .gts specifier-stripping then pointed at files that do not exist. Glob those names too, rename them to foo.d.ts, and strip .gjs alongside .gts. Upstream: microsoft/TypeScript#64053 and the in-flight microsoft/TypeScript#64120, after which the rename becomes a no-op.
Fixes #64053
A new compiler option,
outputExtension, tells TypeScript which file extension an external build gives every source file:{ "compilerOptions": { "declaration": true, "emitDeclarationOnly": true, "outputExtension": ".js" }, "contentMappers": [ { "package": "ember-content-mapper", "extensions": [".gts", ".gjs"] } ] }With this configuration,
counter.gtsemitscounter.d.tsinstead ofcounter.d.gts.ts. In emitted declarations,./counter.gtsbecomes./counter.jsand./format.tsbecomes./format.js.Why
Declaration emit names a content-mapped file
counter.d.gts.tsand keeps./counter.gtsin specifiers. That is the only safe choice when TypeScript knows nothing about the external build. When that build compilescounter.gtstocounter.js(Ember, MDX), the published package needscounter.d.tsnext tocounter.js, and consumers without the mapper cannot resolve the.gtsspecifiers.The same build compiles
.tsfiles, so the option is a program-wide compiler option rather than a per-mapper setting. This answers the question in #64053 about how TypeScript can know what the build tool emits: the project declares it.Behavior
.js,.mjs, and.cjs. Declaration files are named to match:.d.ts,.d.mts, or.d.cts. This applies to.ts,.tsx,.js,.jsx, and content-mapped files../a.ts,./b.tsx, and./c.gtsbecome./a.js,./b.js, and./c.js. This covers imports, exports,import()types, andimport x = require().noEmitoremitDeclarationOnly. OtherwisetscreportsTS5069. The external build owns the JavaScript output, sotscmust not also emit it.TS6046on the value intsconfig.json.outputExtension, nothing changes.The name collision from the issue thread (
app.tsnext toapp.gts, both emittingapp.d.ts) is reported by the existingTS5056check.Tests
outputExtension:.jswith a content mapper, imports between.tsand mapped files,import()types,.jsand extensionless specifiers left alone, declaration maps.outputExtensionMjs:.mjswithout a content mapper..tsand.tsxfiles emit.d.mts.outputExtensionInvalidValue:TS6046for.jsx.outputExtensionRequiresEmitDeclarationOnly:TS5069withoutnoEmitoremitDeclarationOnly.outputExtensionConflict:app.tsandapp.y.zboth emitapp.d.ts.The built compiler also ran on ember-content-mapper/examples/cli-library-v2 with the option added. The
declarations/directory then holds only.d.tsfiles, andindex.d.tsimports./components/counter.jsand./utils/format.js.Disclosure
This patch was written by Claude Code (Claude Fable 5) from the
NullVoxPopuli-ai-agentaccount. The operator is NullVoxPopuli, who filed #64053 and maintains ember-content-mapper. The PR stays a draft until NullVoxPopuli has reviewed it, and NullVoxPopuli will respond to review feedback.NullVoxPopuli please review.
🤖 Generated with Claude Code
https://claude.ai/code/session_0188eg6bTrrUiWbj5FZaWH5Z