Skip to content

Commit 004cc41

Browse files
authored
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.
1 parent 85c53d9 commit 004cc41

2 files changed

Lines changed: 95 additions & 13 deletions

File tree

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

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

32-
const escapedExternals = externals.map((e) => escapeRegexSpecialChars(e) + '(?:/.+)?');
33-
const prefixedExternalRegex = new RegExp(
34-
`${resolvedConfig.base}${VITE_ID_PREFIX}(${escapedExternals.join('|')})`,
35-
'g',
36-
);
32+
const transformFn = createTransformer(resolvedConfig.base, externals);
3733

3834
// @ts-expect-error: Property 'push' does not exist on type 'readonly Plugin<any>[]'
3935
// Reasoning:
@@ -42,15 +38,34 @@ export function createRemoveIdPrefixPlugin(externals: string[]): Plugin {
4238
// AFTER the import-analysis.
4339
resolvedConfig.plugins.push({
4440
name: 'angular-plugin-remove-id-prefix-transform',
45-
transform: (code: string) => {
46-
// don't do anything when code does not contain the Vite prefix
47-
if (!code.includes(VITE_ID_PREFIX)) {
48-
return code;
49-
}
50-
51-
return code.replace(prefixedExternalRegex, (_, externalName) => externalName);
52-
},
41+
transform: transformFn,
5342
});
5443
},
5544
};
5645
}
46+
47+
/**
48+
* Creates a transform function that removes the Vite ID prefix from externals.
49+
* @param base The base path of the application.
50+
* @param externals The external package names.
51+
* @returns A function that transforms code by removing the Vite ID prefix.
52+
*/
53+
export function createTransformer(base: string, externals: string[]): (code: string) => string {
54+
// The path suffix is bounded so that a match can never extend past the end of an
55+
// import specifier string literal. With a greedy `.+`, minified (single-line) code
56+
// would let the first match consume the remainder of the line, leaving all later
57+
// `/@id/` occurrences on that line unstripped.
58+
const escapedExternals = externals.map((e) => escapeRegexSpecialChars(e) + '(?:/[^\'"`\\s]+)?');
59+
60+
const prefixedExternalRegex = new RegExp(
61+
`${base}${VITE_ID_PREFIX}(${escapedExternals.join('|')})`,
62+
'g',
63+
);
64+
65+
return (code: string) => {
66+
return code.includes(VITE_ID_PREFIX)
67+
? code.replace(prefixedExternalRegex, (_, externalName) => externalName)
68+
: // don't do anything when code does not contain the Vite prefix
69+
code;
70+
};
71+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 { createTransformer } from './id-prefix-plugin';
10+
11+
describe('createTransformer', () => {
12+
it('should strip the prefix from every occurrence on a single (minified) line', () => {
13+
const transform = createTransformer('/', [
14+
'@angular/common',
15+
'@angular/common/http',
16+
'@angular/core',
17+
'@angular/router',
18+
]);
19+
20+
const minified =
21+
'import{a}from"/@id/@angular/common/http";' +
22+
'import{b}from"/@id/@angular/router";' +
23+
'import{c}from"/@id/@angular/core";';
24+
25+
expect(transform(minified)).toBe(
26+
'import{a}from"@angular/common/http";' +
27+
'import{b}from"@angular/router";' +
28+
'import{c}from"@angular/core";',
29+
);
30+
});
31+
32+
it('should strip the prefix from an external with a deep import path', () => {
33+
const transform = createTransformer('/', ['@angular/common']);
34+
35+
expect(transform('import{h}from"/@id/@angular/common/http";')).toBe(
36+
'import{h}from"@angular/common/http";',
37+
);
38+
});
39+
40+
it('should strip the prefix when a non-root base is configured', () => {
41+
const transform = createTransformer('/app/', ['@angular/router']);
42+
43+
expect(transform('import{r}from"/app/@id/@angular/router";')).toBe(
44+
'import{r}from"@angular/router";',
45+
);
46+
});
47+
48+
it('should strip the prefix from multi-line (unminified) code', () => {
49+
const transform = createTransformer('/', ['@angular/common', '@angular/router']);
50+
51+
const code =
52+
'import { CommonModule } from "/@id/@angular/common";\n' +
53+
'import { Router } from "/@id/@angular/router";\n';
54+
55+
expect(transform(code)).toBe(
56+
'import { CommonModule } from "@angular/common";\n' +
57+
'import { Router } from "@angular/router";\n',
58+
);
59+
});
60+
61+
it('should not modify imports that are not configured externals', () => {
62+
const transform = createTransformer('/', ['@angular/router']);
63+
64+
const code = 'import{x}from"/@id/some-other-package";';
65+
expect(transform(code)).toBe(code);
66+
});
67+
});

0 commit comments

Comments
 (0)