fix(vite-plugin-angular): preserve TS sourcemaps in test pipeline#2333
Conversation
✅ Deploy Preview for analog-blog ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for analog-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for analog-app ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughThis PR adds source map preservation to the Angular Vite plugin and vitest integration. The plugin now captures TypeScript-emitted source maps during both the JIT transform and full compilation paths, stores them in Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This PR touches multiple package scopes: Please confirm the changes are closely related. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts (1)
1257-1314:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winCapture the actual emitted map filename from TypeScript's emit callback.
The regex
id.replace(/\.ts$/, '.js.map')only matches.tsextensions, but the plugin supports.ctsand.mts(perTS_EXT_REGEXand plugin-config tests). For.cts/.mtssources, this produces.cts.js.map/.mts.js.map, which won't match line 1272's/\.[cm]?js\.map$/pattern and will fail to attach the source map. Use the actualfilenameTypeScript provides in the emit callback instead—this stays consistent with the emit patterns and handles all supported extensions correctly.let content = ''; let map: string | undefined; + let mapFilename: string | undefined; builder.emit( sourceFile, (filename, data) => { if (/\.[cm]?js$/.test(filename)) { content = data; } if (/\.[cm]?js\.map$/.test(filename)) { map = data; + mapFilename = filename; } if ( !watchMode && !isTest && /\.d\.ts/.test(filename) && !filename.includes('.ngtypecheck.') ) { // output to library root instead /src const declarationPath = resolve( config.root, config.build.outDir, relative(config.root, filename), ).replace('/src/', '/'); const declarationFileDir = declarationPath .replace(basename(filename), '') .replace('/src/', '/'); declarationFiles.push({ declarationFileDir, declarationPath, data, }); } }, undefined /* cancellationToken */, undefined /* emitOnlyDtsFiles */, transformers, ); writeFileCallback(id, content, false, undefined, [sourceFile]); - if (map !== undefined) { + if (map !== undefined && mapFilename) { writeFileCallback( - id.replace(/\.ts$/, '.js.map'), + mapFilename, map, false, undefined, [sourceFile], ); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts` around lines 1257 - 1314, The writeOutputFile function currently replaces the source id extension to build the map filename (id.replace(/\.ts$/, '.js.map')), which breaks for .cts/.mts; modify the builder.emit callback (inside writeOutputFile) to capture the actual emitted map filename provided by the emit callback (e.g., store the filename when /\.[cm]?js\.map$/ matches into a variable like emittedMapFilename alongside the map data) and then pass that captured emittedMapFilename to writeFileCallback instead of the id-based replacement; ensure you still set map = data and use the same sourceFile in the writeFileCallback call so emitted maps for .ts/.cts/.mts are written with the exact filenames TypeScript emitted.
🧹 Nitpick comments (3)
tests/vitest-angular/src/sourcemap/vitest.project.ts (1)
10-10: 💤 Low valueDouble cast hides that
angular()returnsPlugin[], notPlugin.
angular(options)is typed asPlugin[]. Wrapping it in[...as unknown as Plugin]produces aPlugin[]whose first element is itself an array — Vite flattens nested plugin arrays at runtime so this works, but the cast obscures it and disables type checking. Spreading is clearer and keeps the type system honest:- plugins: [angular({ jit: false }) as unknown as Plugin], + plugins: [...angular({ jit: false })] as Plugin[],Skip if the sibling projects already use the same
as unknown as Pluginshape — repo consistency wins.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/vitest-angular/src/sourcemap/vitest.project.ts` at line 10, The plugins array currently wraps angular({ jit: false }) with a double-cast to Plugin, hiding that angular(...) actually returns Plugin[] and producing a nested array; replace the casted single entry with a spread of the returned array (use plugins: [...angular({ jit: false })]) and remove the unnecessary as unknown as Plugin cast so TypeScript preserves the correct Plugin[] type for angular(), unless you intentionally must match sibling projects' existing cast convention for consistency.tests/vitest-angular/src/sourcemap/sourcemap.spec.ts (2)
31-44: 💤 Low valueTest regression-bound to native
Error.stackformat — confirm the cross-runner story.
Error.stackline/column formatting differs between V8 (Chromium) and JavaScriptCore/Gecko. The Vitest browser config invitest.config.tsonly registerschromium, so today this is fine, but if anyone ever addswebkit/firefoxinstances the stack frame won't matchsourcemap.spec.ts<...>:LINEand the test will start assertingnot.toBeNull()againstnull. Worth a brief comment near the regex pinning the assumption to V8 stacks, or a fallback parser.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/vitest-angular/src/sourcemap/sourcemap.spec.ts` around lines 31 - 44, The test "error stack maps back to the original TypeScript line" and the helper parseSpecLine assume V8-style Error.stack formatting; update the code so this assumption is explicit and robust: either add a brief comment next to parseSpecLine explaining it pins to V8/Chromium stacks (so future contributors know the limitation) and reference THROW_LINE/MAX_OFFSET in that comment, or enhance parseSpecLine to detect and fall back to parsing WebKit/Gecko stack-frame formats (accepting alternate regexes and returning the same line number semantics) so the expect(...) checks remain valid across chromium/firefox/webkit runners; modify parseSpecLine (and the test description if needed) to reflect the chosen approach.
23-23: ⚡ Quick winHard-coded
THROW_LINE = 34is a maintenance trip-wire.Any future reformatting (Prettier rule change, added import, edited block comment) of lines 1–33 silently shifts the throw line and either masks a regression or fails for the wrong reason. Since the goal is "stack reports a line close to the actual throw," capture the expected line at runtime instead — e.g., throw a probe error one line above and add
1. Keeps the test self-contained and resilient.♻️ Self-anchoring throw line
-const THROW_LINE = 34; -const MAX_OFFSET = 6; +const MAX_OFFSET = 6; + +function currentLine(): number { + const m = /:(\d+)(?::\d+)?\)?$/.exec(new Error().stack?.split('\n')[2] ?? ''); + return m ? Number(m[1]) : NaN; +} @@ test('error stack maps back to the original TypeScript line', () => { let stack = ''; + const expectedLine = currentLine() + 2; try { throw new Error('sourcemap probe'); } catch (e) { stack = (e as Error).stack ?? ''; } const reportedLine = parseSpecLine(stack); expect(reportedLine, `no spec frame in stack:\n${stack}`).not.toBeNull(); expect( - Math.abs((reportedLine as number) - THROW_LINE), - `reported line ${reportedLine}, expected within ${MAX_OFFSET} of ${THROW_LINE}\n${stack}`, + Math.abs((reportedLine as number) - expectedLine), + `reported line ${reportedLine}, expected within ${MAX_OFFSET} of ${expectedLine}\n${stack}`, ).toBeLessThanOrEqual(MAX_OFFSET); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/vitest-angular/src/sourcemap/sourcemap.spec.ts` at line 23, Replace the hard-coded THROW_LINE constant with a runtime-computed value: create a probe Error thrown exactly one line above the real throw, parse its stack to extract the probe's line number, and set the expected throw line to (probeLine + 1); update any references to THROW_LINE to use this computed value so the test remains stable against edits/reformatting (target the existing THROW_LINE constant and the test block that performs the actual throw).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts`:
- Around line 1230-1241: The current check uses _filename.endsWith('.map') and
thus can match declaration maps like foo.d.ts.map and overwrite the JS source
map in outputFiles; change the predicate in the block that upserts into
outputFiles (the branch handling map attachment for _filename) to only accept JS
source maps (e.g. restrict to /\.([cm]?js)\.map$/ or an equivalent endsWith
check for .js/.cjs/.mjs.map) so it matches the same map pattern used by the
writeOutputFile capture and prevents declaration maps from clobbering .js maps.
---
Outside diff comments:
In `@packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts`:
- Around line 1257-1314: The writeOutputFile function currently replaces the
source id extension to build the map filename (id.replace(/\.ts$/, '.js.map')),
which breaks for .cts/.mts; modify the builder.emit callback (inside
writeOutputFile) to capture the actual emitted map filename provided by the emit
callback (e.g., store the filename when /\.[cm]?js\.map$/ matches into a
variable like emittedMapFilename alongside the map data) and then pass that
captured emittedMapFilename to writeFileCallback instead of the id-based
replacement; ensure you still set map = data and use the same sourceFile in the
writeFileCallback call so emitted maps for .ts/.cts/.mts are written with the
exact filenames TypeScript emitted.
---
Nitpick comments:
In `@tests/vitest-angular/src/sourcemap/sourcemap.spec.ts`:
- Around line 31-44: The test "error stack maps back to the original TypeScript
line" and the helper parseSpecLine assume V8-style Error.stack formatting;
update the code so this assumption is explicit and robust: either add a brief
comment next to parseSpecLine explaining it pins to V8/Chromium stacks (so
future contributors know the limitation) and reference THROW_LINE/MAX_OFFSET in
that comment, or enhance parseSpecLine to detect and fall back to parsing
WebKit/Gecko stack-frame formats (accepting alternate regexes and returning the
same line number semantics) so the expect(...) checks remain valid across
chromium/firefox/webkit runners; modify parseSpecLine (and the test description
if needed) to reflect the chosen approach.
- Line 23: Replace the hard-coded THROW_LINE constant with a runtime-computed
value: create a probe Error thrown exactly one line above the real throw, parse
its stack to extract the probe's line number, and set the expected throw line to
(probeLine + 1); update any references to THROW_LINE to use this computed value
so the test remains stable against edits/reformatting (target the existing
THROW_LINE constant and the test block that performs the actual throw).
In `@tests/vitest-angular/src/sourcemap/vitest.project.ts`:
- Line 10: The plugins array currently wraps angular({ jit: false }) with a
double-cast to Plugin, hiding that angular(...) actually returns Plugin[] and
producing a nested array; replace the casted single entry with a spread of the
returned array (use plugins: [...angular({ jit: false })]) and remove the
unnecessary as unknown as Plugin cast so TypeScript preserves the correct
Plugin[] type for angular(), unless you intentionally must match sibling
projects' existing cast convention for consistency.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b8beb7dd-ecc7-4928-960d-83064b7b7419
📒 Files selected for processing (6)
packages/vite-plugin-angular/src/lib/angular-vite-plugin.tspackages/vite-plugin-angular/src/lib/angular-vitest-plugin.tstests/vitest-angular/src/sourcemap/sourcemap.spec.tstests/vitest-angular/src/sourcemap/test-setup.tstests/vitest-angular/src/sourcemap/vitest.project.tstests/vitest-angular/vitest.config.ts
…ugh the Angular plugin Switch TS emit from inline sourcemaps to separate .map files, capture the map alongside the .js content in outputFiles, and return it from the transform hook. Also strip the stale sourceMappingURL comment emitted by TS so downstream tools don't see two references. The .map and .js arrive via writeFileCallback in either order (TS in test mode emits .map first), so upsert the entry without requiring a pre-existing record. The previous version dropped the map silently when .map arrived before .js. In angularVitestSourcemapPlugin, accept a getInMap callback and skip the post-OXC/esbuild transform when the upstream map is already available. The post-pass exists to convert non-Angular .ts files to JS — for files Angular already compiled, re-running OXC produces a map relative to the TS-emitted JS rather than the original source, breaking the chain Vite otherwise wires up. OXC's `inMap` parameter does not chain through the way esbuild's inline `//# sourceMappingURL=` auto-detection does, so skipping is the only reliable option on rolldown-backed Vite. Adds an integration test under tests/vitest-angular/src/sourcemap/ that asserts Error.stack reports a line close to the original .ts throw site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
80d4910 to
a3a7556
Compare
PR Checklist
Closes #2332
Likely also addresses #2296 (coverage variant of the same root cause — coverage maps to TS-emitted JS instead of original
.ts).Affected scope
Recommended merge strategy for maintainer [optional]
What is the new behavior?
Restores accurate sourcemap line numbers for
.tstest files when running Vitest against@analogjs/vite-plugin-angularon Vite 8 / rolldown. Stack frames inError.stack, breakpoints in the Vitest VS Code extension, and coverage reports now point at the original TypeScript line rather than the post-compile JS line.Three changes in one fix:
Switch TS emit from inline → separate
.map. ConfiguresourceMap: !isProd, inlineSourceMap: falseso the TypeScript compiler emits a sibling.js.mapinstead of baking a base64 data URI into the.js. The angular transform hook captures the map and returns it via{ code, map }, and the stale trailing//# sourceMappingURL=comment is stripped so downstream tools don't see two references.Fix order-of-arrival bug in
writeFileCallback. TS emits the.js.mapbefore the.js. The original handler only stored the map when an entry already existed inoutputFiles, so the map was silently dropped on every emit. The handler now upserts in either order.Skip the post-OXC/esbuild transform when the upstream map exists.
angularVitestSourcemapPluginwas a workaround from the esbuild era — esbuild auto-chains inline sourcemaps in itstransformAPI, so post-processing produced a fresh map back to the original.tsfor free. OXC does not auto-chain (inMapis documented but ignored in practice), so re-running it over already-compiled JS produces a map relative to the TS-emitted JS instead of the original source, breaking the chain Vite is wiring up. The post-pass now runs only for files Angular's compilation skipped (e.g. non-decorator.tsunderuseAngularCompilationAPI), which is what it was originally intended for.Test plan
nx run tests-vitest-angular:test— new sub-project undertests/vitest-angular/src/sourcemap/assertsError.stackreports a line within 6 of the original throw site (Angular's compile-timeɵfac/ɵcmpinsertions account for a small irreducible offset). Pre-fix this offset is ~21 lines; post-fix it's 0–4.npm create analog@latest) with 5 stacked decorators above the throw: pre-fix the stack reports an 11-line offset; post-fix it matches the source line exactly.aot,providers,reset-test-bed-between-tests, andsnapshot-serializersintegration projects continue to pass.nx format:checkpnpm buildpnpm test(subset —tests-vitest-angular)Does this PR introduce a breaking change?
Other information
The
getInMapcallback is plumbed end-to-end so that iftransformWithOxc'sinMapparameter starts honoring chained input maps in a future rolldown release, switching the post-plugin from "skip when map exists" to "chain when map exists" is a single-line change. Filed mentally as a TODO; happy to file an upstream OXC issue separately if useful.🤖 Generated with Claude Code