Skip to content

Commit df0fed7

Browse files
committed
fix(@angular/build): strip all vite id prefixes from minified code with external dependencies
The regex used by `createRemoveIdPrefixPlugin` appended a greedy `(?:/.+)?` to each external package name. On minified single-line output the first match on an external with a deep import path (e.g. `@angular/common/http`) consumed the remainder of the line, so subsequent `/@id/` specifiers were never stripped and failed in the browser with an "Unsupported Content-Type" error. The suffix is now bounded so a match cannot extend past the end of an import specifier string literal. Fixes #33524
1 parent 92da0d6 commit df0fed7

2 files changed

Lines changed: 101 additions & 1 deletion

File tree

packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ export function createRemoveIdPrefixPlugin(externals: string[]): Plugin {
2929
return;
3030
}
3131

32-
const escapedExternals = externals.map((e) => escapeRegexSpecialChars(e) + '(?:/.+)?');
32+
// The path suffix is bounded so that a match can never extend past the end of an
33+
// import specifier string literal. With a greedy `.+`, minified (single-line) code
34+
// would let the first match consume the remainder of the line, leaving all later
35+
// `/@id/` occurrences on that line unstripped.
36+
const escapedExternals = externals.map(
37+
(e) => escapeRegexSpecialChars(e) + '(?:/[^\'"`\\s]+)?',
38+
);
3339
const prefixedExternalRegex = new RegExp(
3440
`${resolvedConfig.base}${VITE_ID_PREFIX}(${escapedExternals.join('|')})`,
3541
'g',
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { createRemoveIdPrefixPlugin } from './id-prefix-plugin';
10+
11+
/**
12+
* Runs the plugin's `configResolved` hook against a minimal fake resolved
13+
* configuration and returns the transform function of the plugin it registers.
14+
*/
15+
function getTransform(externals: string[], base: string): (code: string) => string {
16+
const plugin = createRemoveIdPrefixPlugin(externals);
17+
const resolvedConfig = {
18+
base,
19+
plugins: [] as { name: string; transform?: (code: string) => string }[],
20+
};
21+
22+
(plugin.configResolved as (config: unknown) => void)(resolvedConfig);
23+
24+
const pushedPlugin = resolvedConfig.plugins[0];
25+
expect(pushedPlugin?.transform).toBeDefined();
26+
27+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
28+
return pushedPlugin.transform!;
29+
}
30+
31+
describe('createRemoveIdPrefixPlugin', () => {
32+
it('should strip the prefix from every occurrence on a single (minified) line', () => {
33+
const transform = getTransform(
34+
['@angular/common', '@angular/common/http', '@angular/core', '@angular/router'],
35+
'/',
36+
);
37+
38+
const minified =
39+
'import{a}from"/@id/@angular/common/http";' +
40+
'import{b}from"/@id/@angular/router";' +
41+
'import{c}from"/@id/@angular/core";';
42+
43+
expect(transform(minified)).toBe(
44+
'import{a}from"@angular/common/http";' +
45+
'import{b}from"@angular/router";' +
46+
'import{c}from"@angular/core";',
47+
);
48+
});
49+
50+
it('should strip the prefix from an external with a deep import path', () => {
51+
const transform = getTransform(['@angular/common'], '/');
52+
53+
expect(transform('import{h}from"/@id/@angular/common/http";')).toBe(
54+
'import{h}from"@angular/common/http";',
55+
);
56+
});
57+
58+
it('should strip the prefix when a non-root base is configured', () => {
59+
const transform = getTransform(['@angular/router'], '/app/');
60+
61+
expect(transform('import{r}from"/app/@id/@angular/router";')).toBe(
62+
'import{r}from"@angular/router";',
63+
);
64+
});
65+
66+
it('should strip the prefix from multi-line (unminified) code', () => {
67+
const transform = getTransform(['@angular/common', '@angular/router'], '/');
68+
69+
const code =
70+
'import { CommonModule } from "/@id/@angular/common";\n' +
71+
'import { Router } from "/@id/@angular/router";\n';
72+
73+
expect(transform(code)).toBe(
74+
'import { CommonModule } from "@angular/common";\n' +
75+
'import { Router } from "@angular/router";\n',
76+
);
77+
});
78+
79+
it('should not modify imports that are not configured externals', () => {
80+
const transform = getTransform(['@angular/router'], '/');
81+
82+
const code = 'import{x}from"/@id/some-other-package";';
83+
expect(transform(code)).toBe(code);
84+
});
85+
86+
it('should not register a transform when there are no externals', () => {
87+
const plugin = createRemoveIdPrefixPlugin([]);
88+
const resolvedConfig = { base: '/', plugins: [] };
89+
90+
(plugin.configResolved as (config: unknown) => void)(resolvedConfig);
91+
92+
expect(resolvedConfig.plugins).toHaveSize(0);
93+
});
94+
});

0 commit comments

Comments
 (0)