Skip to content

Commit 532aa3e

Browse files
fix(vite-plugin-angular): compose fastCompile JIT strip map and cover esbuild fallback (#2341)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 95a40e4 commit 532aa3e

2 files changed

Lines changed: 90 additions & 12 deletions

File tree

‎packages/vite-plugin-angular/src/lib/fast-compile-plugin.spec.ts‎

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,16 +167,78 @@ export class App {
167167
);
168168

169169
// OXC strip must have been called with the jitTransform output (which
170-
// still contains `readonly`) so Rolldown receives valid JS.
170+
// still contains `readonly`) so Rolldown receives valid JS. We also
171+
// pass the jitTransform map as `inMap` so OXC composes the strip map
172+
// with it, keeping debug positions accurate.
171173
const oxcStripCall = mockTransformWithOxc.mock.calls.find(
172174
([, callId]) => callId === '/src/app/app.ts',
173175
);
174176
expect(oxcStripCall).toBeDefined();
175177
expect(oxcStripCall![0]).toContain('readonly');
176-
expect(oxcStripCall![2]).toMatchObject({ lang: 'ts' });
178+
expect(oxcStripCall![2]).toMatchObject({ lang: 'ts', sourcemap: true });
179+
expect(oxcStripCall![3]).toBeDefined(); // inMap from jitTransform
177180
expect(result.code).not.toContain('readonly');
178181
});
179182

183+
it('JIT path falls back to esbuild when transformWithOxc is unavailable', async () => {
184+
// Vite 6-8 compatibility: the same regression coverage as the OXC test
185+
// above, but exercising the esbuild branch the plugin takes when
186+
// `vite.transformWithOxc` is undefined (Vite 6 / pre-Rolldown).
187+
mockTransformWithEsbuild.mockResolvedValue({
188+
code: 'export class App { test = ""; }\n',
189+
map: { mappings: '' },
190+
});
191+
192+
const viteMod = await import('vite');
193+
const realOxc = viteMod.transformWithOxc;
194+
Object.defineProperty(viteMod, 'transformWithOxc', {
195+
value: undefined,
196+
configurable: true,
197+
});
198+
try {
199+
const plugin = fastCompilePlugin({
200+
tsconfigGetter: () => 'tsconfig.json',
201+
workspaceRoot: '/workspace',
202+
inlineStylesExtension: 'css',
203+
jit: true,
204+
liveReload: false,
205+
supportedBrowsers: [],
206+
isTest: true,
207+
isAstroIntegration: false,
208+
});
209+
const handler = getTransformHandler(plugin);
210+
211+
const code = `import { Component } from '@angular/core';
212+
@Component({ selector: 'app-root', template: '<div></div>' })
213+
export class App {
214+
readonly test = '';
215+
}
216+
`;
217+
const result = await handler.call(
218+
{ addWatchFile: () => undefined },
219+
code,
220+
'/src/app/app.ts',
221+
);
222+
223+
const esbuildStripCall = mockTransformWithEsbuild.mock.calls.find(
224+
([, callId]) => callId === '/src/app/app.ts',
225+
);
226+
expect(esbuildStripCall).toBeDefined();
227+
expect(esbuildStripCall![0]).toContain('readonly');
228+
expect(esbuildStripCall![2]).toMatchObject({
229+
loader: 'ts',
230+
sourcemap: true,
231+
});
232+
expect(esbuildStripCall![3]).toBeDefined(); // inMap from jitTransform
233+
expect(result.code).not.toContain('readonly');
234+
} finally {
235+
Object.defineProperty(viteMod, 'transformWithOxc', {
236+
value: realOxc,
237+
configurable: true,
238+
});
239+
}
240+
});
241+
180242
it('does not run the bypass strip when the file has @Component', async () => {
181243
const plugin = buildPlugin();
182244
const handler = getTransformHandler(plugin);

‎packages/vite-plugin-angular/src/lib/fast-compile-plugin.ts‎

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -298,17 +298,33 @@ export function fastCompilePlugin(
298298
// StackBlitz / WebContainer — and the production build pipeline
299299
// does not register it either — so the JIT path must self-strip
300300
// to stay safe across environments.
301+
//
302+
// Pass the jitTransform map as `inMap` so OXC/esbuild emit a map
303+
// composed with the original `.ts` source — without this, debug
304+
// stack traces and breakpoints land on the wrong lines once the
305+
// strip removes any TS-only token. `jitTransform` returns
306+
// `map: null` for files with no Angular class (no edits made);
307+
// passing that to OXC/esbuild as `inMap` throws, so coerce to
308+
// `undefined` in that case.
309+
const inMap = result.map ?? undefined;
301310
const stripped = vite.transformWithOxc
302-
? await vite.transformWithOxc(result.code, id, {
303-
lang: 'ts',
304-
sourcemap: false,
305-
decorator: { legacy: false, emitDecoratorMetadata: false },
306-
})
307-
: await vite.transformWithEsbuild(result.code, id, {
308-
loader: 'ts',
309-
sourcemap: false,
310-
});
311-
return { code: stripped.code, map: result.map };
311+
? await vite.transformWithOxc(
312+
result.code,
313+
id,
314+
{
315+
lang: 'ts',
316+
sourcemap: true,
317+
decorator: { legacy: false, emitDecoratorMetadata: false },
318+
},
319+
inMap,
320+
)
321+
: await vite.transformWithEsbuild(
322+
result.code,
323+
id,
324+
{ loader: 'ts', sourcemap: true },
325+
inMap,
326+
);
327+
return { code: stripped.code, map: stripped.map };
312328
}
313329

314330
// Inline external templateUrl/styleUrl(s) into the source before compilation

0 commit comments

Comments
 (0)