Compose sourcemaps in two-stage register hooks - #100
Conversation
The tsc and civet register hooks compile .hera files with an inline map (hera -> TS/Civet) and then transpile to JS without regenerating the map. Emit shifts line numbers (-85 lines for hera.hera through tsc), so the stale map misaligns stack traces and c8 coverage attribution for .hera sources. transpileTsToJs and compileCivetToJs now strip the stale inline map, run the second stage with sourceMap enabled, and attach a composed hera -> JS map via @jridgewell/remapping (first runtime dependency; the dist register files are unbundled). Inputs without an inline map behave as before. Regression tests assert that probe handler lines in the emitted JS map back to their source/hera.hera lines on both paths, and that emit actually shifted the lines so a stale map cannot pass accidentally. With the fixed hook, hera.hera coverage under 0.9.8 codegen is a genuine 100/100 (previously ~94.7% lines / 30% branches of noise), unblocking the @danielx/hera-previous pin bump after release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #100 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 9 9
Lines 1783 1783
Branches 300 300
=========================================
Hits 1783 1783 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Greptile SummaryThis PR fixes stale inline sourcemaps in the two-stage register hooks (
Confidence Score: 3/5The sourcemap composition logic is correct and well-tested for the happy path, but the new Two issues hold the score down:
Important Files Changed
Sequence DiagramsequenceDiagram
participant Loader as Register Hook (CJS/ESM)
participant T as transpileTsToJs / compileCivetToJs
participant E as extractInlineMap
participant C as tsc / civet.compile
participant R as @jridgewell/remapping
participant Out as Output JS
Loader->>T: tsSource / civetSrc (with inline hera→TS/Civet map)
T->>E: source string
E-->>T: "{ code (stripped), map: heraToIntermediateMap }"
T->>C: stripped code + sourceMap:true
C-->>T: "{ outputText/code, sourceMapText/sourceMap }"
T->>R: "remapping([intermediateToJsMap, heraToIntermediateMap], ()=>null)"
R-->>T: composed heraToJsMap
T-->>Out: js + composed inline sourcemap comment
Reviews (1): Last reviewed commit: "Compose sourcemaps in two-stage register..." | Re-trigger Greptile |
A malformed inline map comment now degrades to the no-map pass-through instead of throwing at file-load time, and the tsc map-comment strip tolerates CRLF emit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I thought we added inline source map detection and automatic composition in Civet. Am I misremembering? Or possibly a Civet version issue? Obviously this won't work with TypeScript though. |
The civet register hook reimplemented two-stage map composition with
@jridgewell/remapping, but @danielx/civet's compile already auto-strips a
trailing inline map from its input, adopts it as upstreamSourceMap, and
composes it with its own Civet->JS map when an output map is requested
(`options.upstreamSourceMap ??= sm`).
Detect whether the input carries a map (to leave map-free inputs
untouched), pass the raw source through with { inlineMap: true }, and let
Civet compose. Drops the manual strip/compose and the @jridgewell/remapping
use on this path; remapping is still required for the tsc path, which has
no native composition. Adds a hasInlineMap presence check to sourcemap.civet.
Verified: compiling source/hera.hera through the hook emits a single
composed inline map whose probe handler lines map back to source/hera.hera;
pnpm build && pnpm test green at 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
You're right — Civet's done this since ≥0.11.6 ( |
Problem
The
register/tscandregister/civethooks compile.herafiles with{ inlineMap: true }— a map from generated TS/Civet back to the hera source — then transpile that output to JS without regenerating the map. Emit shifts line numbers (−85 lines forhera.herathrough tsc with 0.9.8 codegen; +3 through Civet),so the stale inline map is misaligned against the JS that actually executes.breaking source attribution for the JS that actually executes. The two stages fail differently:ts.transpileModuleregenerates nothing, so the original hera→TS inline map survives unchanged and is misaligned against the shifted JS — a stale map.the identical flaw — the stale map comment was passed through verbatim.@danielx/civet'scompilealready auto-strips a trailing inline map from its input (options.upstreamSourceMap ??= sm), so the original hook — which requested no output map — emitted JS with no sourcemap at all. Missing attribution, not stale.Consequences:
.herafiles point at the wrong lines (or nowhere, on the civet path).source/hera.herais garbage in every version — it accidentally reported 100% under the 0.9.0 pin and drops to ~94.7% lines / 30% branches under 0.9.8, which is what blocks bumping the@danielx/hera-previouspin (see Full ECMA-262 regex literal grammar sample #99).Reproduced before the fix: the
HandlingExpressionBodyhandler line$1.join("").trimEnd()(source/hera.hera:147) sits at line 661 of the generated TS but line 576 of the emitted JS, and the stale map returnssource: nullfor that JS position.Fix
source/register/sourcemap.civet: strips/decodes a trailing inline-map comment and composes two maps with@jridgewell/remapping, re-encoding inline. Also exports ahasInlineMappresence check.transpileTsToJs: when the input carries an inline map, strip it, runts.transpileModulewithsourceMap: true, drop tsc's dangling//# sourceMappingURL=module.js.mapcomment, and attach the composed hera → JS map. Covers both the cjs and esm paths.compileCivetToJs:same composition using Civet'srelies on Civet's native composition. Civet'ssourceMap: trueoutput (the civet hook had the identical flaw — it passed the stale map comment through verbatim).compileauto-strips a trailing inline map from its input and adopts it as the upstream map (options.upstreamSourceMap ??= sm); requesting{ inlineMap: true }then composes that upstream map with Civet's own Civet→JS map and emits a single hera→JS inline map. The hook only checks whether the input has a map (so map-free inputs stay untouched) and passes the raw source through — no manual@jridgewell/remappingstep on this path.@jridgewell/remappingbecomes the package's first runtime dependency — required by the tsc path, which (unlike Civet) has no native map composition. Already in the tree transitively, but the dist register files are unbundled so it must be declared.@jridgewell/trace-mappingadded as a devDependency for tests.Verification
source/hera.hera:147/:160on both the tsc and civet paths.test/register/{tsc,civet}/transpile.civetcompilesource/hera.hera, assert exactly one inline map survives (the stale one is stripped), assert emit actually shifted the probe lines (so a stale map cannot pass accidentally), and assert two handler lines map back to their.heralines.dist/register/tsc, the suite passes self-bootstrapped on 0.9.8 codegen andhera.herareports a genuine 100% statements/branches/lines.pnpm build && pnpm testgreen at the existing 100% thresholds with the pin still at 0.9.0.Follow-up
Once this ships in a release, bump
@danielx/hera-previousto it — that restoressamples/regex.herato the benchmark table and may surface genuinely uncovered handler branches inhera.heraneeding real tests.🤖 Generated with Claude Code