Skip to content

Build with released typescript@7, drop detective-typescript - #52

Merged
lauckhart merged 8 commits into
nacho-iot:mainfrom
matter-js:feat/typescript-7
Aug 5, 2026
Merged

Build with released typescript@7, drop detective-typescript#52
lauckhart merged 8 commits into
nacho-iot:mainfrom
matter-js:feat/typescript-7

Conversation

@Apollon77

Copy link
Copy Markdown
Contributor

Why

typescript@7 is not a version bump of the package we had — it ships the native compiler plus a new unstable/* API, and its root export is lib/version.cjs, a version string. There is no ts.createProgram, no lib.*.d.ts. Three consumers need the classic API:

  • typedoc@0.28 — peer ≤6.0.x
  • detective-typescript@15 — peer ^5.4.4 || ^6.0.2
  • our own context.ts / incremental-compiler.ts / solution-builder.ts

So node_modules/typescript has to stay on 6.x, and the native compiler is aliased as typescript7, replacing the @typescript/native-preview dev preview. Overriding typedoc's peer to an aliased TS6 does not work — npm cannot satisfy a peer named typescript with an alias while the root pins typescript@7:

peer overridden typescript@"npm:@typescript/typescript6@6.0.2" ... from typedoc@0.28.20

The consumer-facing bug this fixes

detective-typescript pulled in ts-api-utils, whose peer range typescript >=4.8.4 is satisfied by 7.x. npm therefore hoisted it against a consumer's own typescript@7, where it read ts.TypeFlags off the version-string export:

node_modules/ts-api-utils/lib/index.cjs:787
TypeError: Cannot read properties of undefined (reading 'Intrinsic')

This hit every nacho-build command, not just cycles, because cli.ts imports cycles.ts eagerly. cycles.ts now extracts import specifiers with ts.createSourceFile instead, dropping that subtree, the overrides block, 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:

  1. BOT_APP_ID and BOT_APP_PRIVATE_KEY are both organization secrets visible to this repo, 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.
  2. ${{ }} is expanded as text before the shell parses the line, so a 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. Inputs now travel through env:. The same value also reached the Commit and Push steps through step outputs, so env: alone would not have closed the path; the applied version is validated before it is written to GITHUB_OUTPUT, which also stops a multi-line value forging extra outputs.

Verification

New fixture tests/cycles was written against the old detective implementation and passing before cycles.ts was 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:

mutation result
type-only imports counted as edges FAIL type-c must not be reported
type-only exports counted as edges FAIL texp-k must not be reported
elements.length > 0 guard removed FAIL expected empty-i
fixture: export * fromexport type * FAIL expected value-a
fixture: import() → static import FAIL lazy-e must not be reported

The type-only export guard initially survived mutation — the fixture did not cover it, so texp-k/texp-l and texp-m/texp-n were added until it failed.

New fixtures tests/consumer-ts6 and tests/consumer-ts7 pin a consumer's own typescript to each major and run build, cycles and docs — the last proving typedoc still resolves its nested TS6 while the consumer root is on TS7. consumer-ts7 was confirmed to fail against the pre-fix code with the Intrinsic TypeError, so it is a test that can actually fail.

Full suite: 10/10 fixtures green, clean npm ciprepare green.

Not verified

The CI Docker image was not built locally — no daemon available. The lockfile carries all 20 platform binaries including typescript-linux-x64 and win32-x64, so npm ci on linux should be fine, but that is inferred rather than observed.

Follow-ups, not in this PR

tsgo / NACHO_TSGO / tsgoBin are now misnomers — the native compiler ships as typescript@7. Renaming touches five files plus a public env var.

🤖 Generated with Claude Code

Apollon77 and others added 3 commits August 3, 2026 17:01
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>
@Apollon77
Apollon77 requested a review from lauckhart as a code owner August 3, 2026 15:06
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>
Comment thread package.json Outdated
"typedoc-github-theme": "^0.4.0",
"typescript": "~6.0.2"
"typescript": "~6.0.3",
"typescript7": "npm:typescript@~7.0.2"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Apollon77 Apollon77 Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --tsc flag and NACHO_TSGO escape 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 is 5.0.x–6.0.x, which was the hard blocker on the rename: with root typescript at 7.x, npm i would ERESOLVE or nest a duplicate ts6 — exactly the redundant install you wanted to avoid). @microsoft/tsdoc and typedoc-github-theme went 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-package openFiles/closeFiles snapshots. 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.

Comment thread src/util/file.ts
* 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should update the places where we do manual regex replacement (e.g. glob.ts but iirc there's other places) for consistency.

@Apollon77 Apollon77 Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Apollon77 and others added 4 commits August 5, 2026 16:56
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>
@Apollon77
Apollon77 requested a review from lauckhart August 5, 2026 15:37
@lauckhart
lauckhart merged commit 19b659f into nacho-iot:main Aug 5, 2026
3 checks passed
lauckhart pushed a commit that referenced this pull request Aug 5, 2026
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>
lauckhart pushed a commit that referenced this pull request Aug 5, 2026
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>
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.

2 participants