Build with released typescript@7, drop detective-typescript - #52
Conversation
typescript@7 ships the native compiler and a new unstable API only; the classic API is gone and its root export is just a version string. typedoc and our own JS-API code still need that classic API, so node_modules/typescript stays on 6.x and the native compiler is aliased as "typescript7", replacing the @typescript/native-preview dev preview. detective-typescript pulled in ts-api-utils, whose peer range of typescript >=4.8.4 is satisfied by 7.x. npm therefore hoisted it against a consumer's typescript@7, where it read ts.TypeFlags off the version-string export and threw on load — for every nacho-build command, since cli.ts imports cycles.ts eagerly. cycles.ts now extracts import specifiers with ts.createSourceFile, which drops that subtree along with the overrides block. Behaviour matches the previous detective options: type-only and dynamic imports are excluded, re-exports and mixed value/type bindings are not. Also drops @types/madge, which nothing references. New fixtures: cycles covers the import forms the parser distinguishes, and consumer-ts6/consumer-ts7 pin a consumer's own typescript to each major so CI proves both resolve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BOT_APP_ID and BOT_APP_PRIVATE_KEY are both organization secrets visible to this repository, but the workflow read vars.BOT_APP_ID. No variable of that name exists at either level, so app-id was empty and token minting failed before checkout — the only Release run to date never got past its first step. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitHub expands ${{ }} as text before the shell parses the line, so a
workflow_dispatch version of `1.0.0"; <command>; "` executed that command
with the App token in scope — a token that pushes directly to main past the
ruleset. The inputs now travel through env: and stay shell variables.
The same value also reached the Commit and Push steps via step outputs, so
env: alone would not have closed the path; the applied version is validated
against the character set a release tag can contain before it is written to
GITHUB_OUTPUT, which also stops a multi-line value forging extra outputs.
app-slug moves to env: too. It comes from actions/create-github-app-token
and is not attacker controlled, but no interpolation inside any run: block
is a rule that can be checked mechanically.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Module paths are assembled with path.join, so on Windows they carry native
separators. Three places then matched them against hardcoded "/":
- addModuleGlobs rewrote /dist/(esm|cjs)/ to /src/, which never fired, so
sourceModules resolved to transpiled output instead of source
- typedoc reads entry points as globs, where "\" escapes the next character
rather than separating segments, so it rejected the path outright
- naiveToRealNamesFor stripped a trailing "/index" to match typedoc's module
names, which are posix on every platform
The first two together produced "The glob ...\dist\esm\index.js escapes a
non-special character" followed by an unmapped-module-name failure. Module
paths now stay posix from the point they are assembled, and the local glob
already normalises its pattern but resolves results natively, so globbed
wildcard paths are normalised too before the prefix arithmetic uses them.
sourceModules has no callers outside docs.ts, so this reaches nothing else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| "typedoc-github-theme": "^0.4.0", | ||
| "typescript": "~6.0.2" | ||
| "typescript": "~6.0.3", | ||
| "typescript7": "npm:typescript@~7.0.2" |
There was a problem hiding this comment.
I think it would be worthwhile to call ts7 "typescript" rather than ossifying a legacy-only pattern that will likely cause redundant package installation in our local npm dir due to conflicts with other package deps... npm should take care of collisions either way.
I suggest we either rip out the older compiler impls (solution builder, etc.) or just rename their import to typescript6. Probably the former as ts6 is slooow and we're unlikely to ever use again. But lmk if you see a reason to keep ts6 around.
If we also rip out typedoc (support for which is incomplete and doesn't have consumers afaik) then we can drop ts6 altogether. That'd be the route I'd suggest. Could leave files commented so it's easy to resurrect if/when we decide to do that... But hopefully by the time we're there either typedoc is updated or there's a better option available.
There was a problem hiding this comment.
Done, and went the full route you suggested: typescript@7 now ships under its real name ("typescript": "~7.0.2"), and ts6 is gone entirely — no typescript6 alias needed.
What made that possible:
- Ripped the old compiler impls — solution-builder, incremental-compiler, the ts6 context, the
--tscflag andNACHO_TSGOescape hatch. tsgo is the only type checker now. - Ripped typedoc/docs rather than commenting it out — it's a clean
git revert/history resurrection when typedoc supports TS7 (its peer range is5.0.x–6.0.x, which was the hard blocker on the rename: with roottypescriptat 7.x,npm iwould ERESOLVE or nest a duplicate ts6 — exactly the redundant install you wanted to avoid).@microsoft/tsdocandtypedoc-github-themewent with it. - cycles no longer needs the classic JS API: it's ported to the experimental API that typescript@7 ships built-in (
typescript/unstable/sync+typescript/unstable/ast) — one shared Go server per run, per-packageopenFiles/closeFilessnapshots. Output verified byte-identical to the ts6 implementation on the same tree, and the cycles fixture pins the semantics.
Porting also surfaced three edge-case bugs in the type-only detection, now fixed and fixture-covered where expressible: import {} from and import type X = require() are erased by esbuild, and import defer evaluates lazily — none is a load-time edge, so none can form a cycle.
Two follow-ups worth your eyes (both pre-existed this PR, fixed in separate commits): build info was persisted before the deferred Phase-2 transpile ran (11e19f8), and tsgo diagnostic→package mapping assumed two-segment package paths (1d8fc65).
One smaller open question: tsgoBin still resolves via the findPackage directory walk while the imports use Node exports resolution; both agree under npm (the two consumer fixtures pin that) but pnpm/yarn layouts are unverified.
| * Rewrite a native path in posix form. Globs read "\" as an escape rather than a separator, and tooling that names | ||
| * modules after their path uses "/" on every platform. | ||
| */ | ||
| export function posixPath(path: string) { |
There was a problem hiding this comment.
Should update the places where we do manual regex replacement (e.g. glob.ts but iirc there's other places) for consistency.
There was a problem hiding this comment.
Done in 79ecae3 — all seven manual .replace(/\\/g, "/") sites (glob.ts, package.ts, project.ts, tsconfig.ts) now go through posixPath. Small semantic bonus: posixPath only rewrites on Windows, so a literal backslash in a minimatch pattern is no longer mangled on posix.
Review feedback on nacho-iot#52: the manual .replace(/\\/g, "/") sites predate the posixPath helper. posixPath also only rewrites on Windows, so a literal backslash in a minimatch pattern is no longer mangled on posix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback on nacho-iot#52: with the native compiler released as typescript@7 there is no reason to keep two compilers around. - typescript dependency is now typescript@7 under its real name; the typescript7 alias and the ts6 pin are gone - docs command and typedoc/tsdoc dependencies removed until typedoc supports typescript@7; resurrect from git history when it does - ts6 solution-builder/incremental-compiler contexts, --tsc flag and NACHO_TSGO env var removed; tsgo is the only type checker - cycles now parses with the typescript@7 API (typescript/unstable/*): one shared Go server per run, per-package openFiles/closeFiles - cycle semantics fixed while porting: import {} from and import type X = require() are erased by the emitter and import defer evaluates lazily, so none of them is a load-time edge; the cycles fixture asserts the corrected behavior Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In the batched tsgo flow #doBuild only enqueues transpile/CJS-copy work, but still wrote build/info.json immediately. When flushWork then failed, the package was already marked built and the next run silently skipped it, leaving broken dist output in place. Build info for deferred work is now persisted by flushWork after all work items complete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The parser assumed every package sits exactly two path segments below the workspace root, so single-segment or nested workspace entries never matched a real package: their errors were dropped from the per-package report and, on exit code 2, the package was not excluded from transpile and shipped stale declarations. Diagnostics now match the longest actual package path prefix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback on #52: the manual .replace(/\\/g, "/") sites predate the posixPath helper. posixPath also only rewrites on Windows, so a literal backslash in a minimatch pattern is no longer mangled on posix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback on #52: with the native compiler released as typescript@7 there is no reason to keep two compilers around. - typescript dependency is now typescript@7 under its real name; the typescript7 alias and the ts6 pin are gone - docs command and typedoc/tsdoc dependencies removed until typedoc supports typescript@7; resurrect from git history when it does - ts6 solution-builder/incremental-compiler contexts, --tsc flag and NACHO_TSGO env var removed; tsgo is the only type checker - cycles now parses with the typescript@7 API (typescript/unstable/*): one shared Go server per run, per-package openFiles/closeFiles - cycle semantics fixed while porting: import {} from and import type X = require() are erased by the emitter and import defer evaluates lazily, so none of them is a load-time edge; the cycles fixture asserts the corrected behavior Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Why
typescript@7is not a version bump of the package we had — it ships the native compiler plus a newunstable/*API, and its root export islib/version.cjs, a version string. There is nots.createProgram, nolib.*.d.ts. Three consumers need the classic API:typedoc@0.28— peer≤6.0.xdetective-typescript@15— peer^5.4.4 || ^6.0.2context.ts/incremental-compiler.ts/solution-builder.tsSo
node_modules/typescripthas to stay on 6.x, and the native compiler is aliased astypescript7, replacing the@typescript/native-previewdev preview. Overriding typedoc's peer to an aliased TS6 does not work — npm cannot satisfy a peer namedtypescriptwith an alias while the root pinstypescript@7:The consumer-facing bug this fixes
detective-typescriptpulled ints-api-utils, whose peer rangetypescript >=4.8.4is satisfied by7.x. npm therefore hoisted it against a consumer's owntypescript@7, where it readts.TypeFlagsoff the version-string export:This hit every
nacho-buildcommand, not justcycles, becausecli.tsimportscycles.tseagerly.cycles.tsnow extracts import specifiers withts.createSourceFileinstead, dropping that subtree, theoverridesblock, and 20 transitive packages. Behaviour matches the previous detective options: type-only and dynamic imports excluded, re-exports and mixed value/type bindings included.Also drops
@types/madge, which nothing referenced.Release workflow
Two separate commits, unrelated to the TypeScript work but in reach:
BOT_APP_IDandBOT_APP_PRIVATE_KEYare both organization secrets visible to this repo, but the workflow readvars.BOT_APP_ID. No variable of that name exists at either level, soapp-idwas empty and token minting failed before checkout — the only Release run to date never got past its first step.${{ }}is expanded as text before the shell parses the line, so a dispatch version of1.0.0"; <command>; "executed that command with the App token in scope — a token that pushes directly tomainpast the ruleset. Inputs now travel throughenv:. The same value also reached the Commit and Push steps through step outputs, soenv:alone would not have closed the path; the applied version is validated before it is written toGITHUB_OUTPUT, which also stops a multi-line value forging extra outputs.Verification
New fixture
tests/cycleswas written against the old detective implementation and passing beforecycles.tswas touched, then re-run against the new parser with byte-identical output. Every guard was mutation-tested — each mutation applied, packed, fixture re-run, reverted:type-c must not be reportedtexp-k must not be reportedelements.length > 0guard removedexpected empty-iexport * from→export type *expected value-aimport()→ static importlazy-e must not be reportedThe type-only export guard initially survived mutation — the fixture did not cover it, so
texp-k/texp-landtexp-m/texp-nwere added until it failed.New fixtures
tests/consumer-ts6andtests/consumer-ts7pin a consumer's own typescript to each major and runbuild,cyclesanddocs— the last proving typedoc still resolves its nested TS6 while the consumer root is on TS7.consumer-ts7was confirmed to fail against the pre-fix code with theIntrinsicTypeError, so it is a test that can actually fail.Full suite: 10/10 fixtures green, clean
npm ci→preparegreen.Not verified
The CI Docker image was not built locally — no daemon available. The lockfile carries all 20 platform binaries including
typescript-linux-x64andwin32-x64, sonpm cion linux should be fine, but that is inferred rather than observed.Follow-ups, not in this PR
tsgo/NACHO_TSGO/tsgoBinare now misnomers — the native compiler ships astypescript@7. Renaming touches five files plus a public env var.🤖 Generated with Claude Code