Skip to content

Commit 070155f

Browse files
FestimShyticlydin
authored andcommitted
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. (cherry picked from commit 004cc41)
1 parent 9906add commit 070155f

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
@@ -27,11 +27,7 @@ export function createRemoveIdPrefixPlugin(externals: string[]): Plugin {
2727
return;
2828
}
2929

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

3632
// @ts-expect-error: Property 'push' does not exist on type 'readonly Plugin<any>[]'
3733
// Reasoning:
@@ -40,15 +36,34 @@ export function createRemoveIdPrefixPlugin(externals: string[]): Plugin {
4036
// AFTER the import-analysis.
4137
resolvedConfig.plugins.push({
4238
name: 'angular-plugin-remove-id-prefix-transform',
43-
transform: (code: string) => {
44-
// don't do anything when code does not contain the Vite prefix
45-
if (!code.includes(VITE_ID_PREFIX)) {
46-
return code;
47-
}
48-
49-
return code.replace(prefixedExternalRegex, (_, externalName) => externalName);
50-
},
39+
transform: transformFn,
5140
});
5241
},
5342
};
5443
}
44+
45+
/**
46+
* Creates a transform function that removes the Vite ID prefix from externals.
47+
* @param base The base path of the application.
48+
* @param externals The external package names.
49+
* @returns A function that transforms code by removing the Vite ID prefix.
50+
*/
51+
export function createTransformer(base: string, externals: string[]): (code: string) => string {
52+
// The path suffix is bounded so that a match can never extend past the end of an
53+
// import specifier string literal. With a greedy `.+`, minified (single-line) code
54+
// would let the first match consume the remainder of the line, leaving all later
55+
// `/@id/` occurrences on that line unstripped.
56+
const escapedExternals = externals.map((e) => escapeRegexSpecialChars(e) + '(?:/[^\'"`\\s]+)?');
57+
58+
const prefixedExternalRegex = new RegExp(
59+
`${base}${VITE_ID_PREFIX}(${escapedExternals.join('|')})`,
60+
'g',
61+
);
62+
63+
return (code: string) => {
64+
return code.includes(VITE_ID_PREFIX)
65+
? code.replace(prefixedExternalRegex, (_, externalName) => externalName)
66+
: // don't do anything when code does not contain the Vite prefix
67+
code;
68+
};
69+
}
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)