Add TypeScript resolution support to stasis bundle (--typescript, tsconfig paths) - #168
Merged
Merged
Conversation
stasis bundle (--typescript, tsconfig paths)
exo-nikita
force-pushed
the
claude/stasis-typescript-resolution-03wt3s
branch
from
August 21, 2026 00:35
f322380 to
9dd1ce6
Compare
…script tsc never rewrites specifiers, so TS sources import each other by their output names (`./x.js` for the file living on disk as `./x.ts`) -- a mapping Node's resolver refuses. `--typescript` retries a failed resolution with tsc's extension substitution (.js -> .ts, .mjs -> .mts, .cjs -> .cts, plus .tsx under --jsx) and TS extension/index probing for extensionless specifiers (./x -> ./x.ts, ./dir -> ./dir/index.ts), on both the built-in resolver and the legacy-field (--mainFields/--metro) one. Fallback-only: an on-disk .js always wins over its .ts twin, and a successful resolution is never rewritten. Bare package specifiers are left to their manifests, and .d.ts is never a target. Rejected with --metro-resolver (the project's metro-resolver can't substitute) and for non-JS bundles. Also adds src/resolve-fields.js to the published files list: shipped files (src/cmd/bundle.js, and now src/scan.js) import it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011np6aa8nMXi29WPqD6azHh
… both resolvers Adds tsconfig `compilerOptions.paths` alias support to `--typescript`: `"@/*": ["./src/*"]`-style aliases resolve for bare specifiers nothing else resolved, from the project root's tsconfig.json when present or an explicit `--tsconfig=path` (rejected without --typescript, fails closed on a missing file). The config is read as JSONC, `extends` chains are followed (relative and node_modules bases, `paths` replacing wholesale), targets resolve against `baseUrl` (else the declaring config's dir), exact keys beat longest-prefix `*` patterns, and a dependency's own imports never see the app's aliases. The tsc-style rules now live once, in src/resolve-typescript.js, and both resolvers (scan.js's built-in and the --mainFields/--metro field resolver) complete their misses through the same dispatcher, closing the gaps where the flag silently didn't apply: package `exports` targets and `#name` `imports` targets naming compiled files that exist only as TS source, a directory `main` under the built-in resolver, bare subpaths into exports-less packages, and '.', '..' and trailing-'/' specifiers (which resolve as directory imports, so a literal '.ts' dotfile can no longer shadow index.ts). Exports maps are never widened: an unexported subpath stays unresolved. `.d.ts` screening now lives at probe time, once; typescriptSiblings uses extname so dotfile names can't split into a bogus substitution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011np6aa8nMXi29WPqD6azHh
…stripping
Match verbatimModuleSyntax / Node semantics for which import statements
survive type erasure: `import type { A }` / `export type { A } from` /
`export type *` vanish whole (no edge), while a statement that merely
lists inline `type` specifiers (`import { type A }`, `export { type B }
from`) -- or none at all (`import {}`, `export {} from`) -- still loads
its module at runtime, so its edge is recorded. Previously any import
whose named specifiers were all inline-`type` lost its edge (breaking
--bundle=load on a module Node actually evaluates), and `export {} from`
lost its edge entirely.
oxc's module records can't draw that line (`import type { A }` and
`import { type A }` yield identical all-isType entries, and
`export {} from` yields no entry at all), so static import/re-export
edges are now read off the AST, where statement-level importKind /
exportKind carry it. Verified against Node 24: a bundle of the mixed
forms loads the exact set plain `node` evaluates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011np6aa8nMXi29WPqD6azHh
exo-nikita
force-pushed
the
claude/stasis-typescript-resolution-03wt3s
branch
from
August 21, 2026 01:19
9dd1ce6 to
e484a63
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds tsc-style TypeScript resolution to the bundler via a new
--typescriptflag: specifiers naming compiled output (e.g.import "./x.js") resolve to their on-disk TypeScript sources (./x.ts), the waytscresolves imports between TS sources, including through package manifests and tsconfigcompilerOptions.pathsaliases.Key Changes
New
--typescriptflag: retries a failed resolution with tsc's mapping. Fallback-only: an on-disk.jsalways wins over its.tstwin, and a successful resolution is never rewritten.Extension substitution rules:
.js→.ts(or.tsxwhen--jsxis enabled).jsx→.tsx(only when--jsxis enabled).mjs→.mts,.cjs→.cts./x→./x.ts,./dir→./dir/index.ts(also through a directory's package.jsonmain).d.ts) are never resolution targetsManifest coverage: the same substitution applies to package
maintargets,exportstargets,#nameimportstargets, and bare subpaths into exports-less packages — so an unbuilt TS-source dependency resolves whether it declaresmainorexports. Exports maps are never widened: an unexported subpath stays unresolved.tsconfig
pathsaliases (e.g."@/*": ["./src/*"]): consulted for bare specifiers nothing else resolved, from the project root'stsconfig.jsonwhen present or an explicit--tsconfig=path. JSONC accepted,extendschains followed (pathsreplaces wholesale, like tsc), targets resolved againstbaseUrl(else the declaring config's dir), exact keys beat longest-prefix*patterns. A dependency's own imports never see the app's aliases, and--tsconfigwithout--typescriptis rejected.One shared implementation: all tsc-style rules live in the new
src/resolve-typescript.js; both resolvers (the built-in Node resolver inscan.jsand the legacy-field--mainFields/--metroresolver) complete their misses through the same dispatcher, so the flag behaves identically on every path.'.','..'and trailing-/specifiers resolve as directory imports (a literal.tsdotfile can no longer shadowindex.ts).Statement-level type erasure (verbatimModuleSyntax / Node type-stripping parity): only
import type/export type/export type *statements drop their edge;import {},import { type A },export { type A } from, andexport {} fromstill load their modules at runtime, so their edges are recorded (previously all-inline-typeimports andexport {} fromlost their edges). Static import/re-export edges now come off the AST, where statement-levelimportKind/exportKindcarries the distinction oxc's module records cannot.Validation: rejects
--typescriptfor non-JS entries and with--metro-resolver(which cannot substitute).Notable Implementation Details
./dep.js,@/dep.js) while the target is the mapped file —stasis run --bundle=loadreplays the mapping from the import map, so the bundle runs under plain Node with types stripped at load.--lockfileattests the mapped targets (the real bytes on disk), never phantom.jsoutputs.bundle → run --bundle=loadround-trip verified against plain Node's load set.https://claude.ai/code/session_011np6aa8nMXi29WPqD6azHh