diff --git a/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts b/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts index 50bbcd8d11e2..74cd03179a7e 100644 --- a/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin.ts @@ -29,11 +29,7 @@ export function createRemoveIdPrefixPlugin(externals: string[]): Plugin { return; } - const escapedExternals = externals.map((e) => escapeRegexSpecialChars(e) + '(?:/.+)?'); - const prefixedExternalRegex = new RegExp( - `${resolvedConfig.base}${VITE_ID_PREFIX}(${escapedExternals.join('|')})`, - 'g', - ); + const transformFn = createTransformer(resolvedConfig.base, externals); // @ts-expect-error: Property 'push' does not exist on type 'readonly Plugin[]' // Reasoning: @@ -42,15 +38,34 @@ export function createRemoveIdPrefixPlugin(externals: string[]): Plugin { // AFTER the import-analysis. resolvedConfig.plugins.push({ name: 'angular-plugin-remove-id-prefix-transform', - transform: (code: string) => { - // don't do anything when code does not contain the Vite prefix - if (!code.includes(VITE_ID_PREFIX)) { - return code; - } - - return code.replace(prefixedExternalRegex, (_, externalName) => externalName); - }, + transform: transformFn, }); }, }; } + +/** + * Creates a transform function that removes the Vite ID prefix from externals. + * @param base The base path of the application. + * @param externals The external package names. + * @returns A function that transforms code by removing the Vite ID prefix. + */ +export function createTransformer(base: string, externals: string[]): (code: string) => string { + // The path suffix is bounded so that a match can never extend past the end of an + // import specifier string literal. With a greedy `.+`, minified (single-line) code + // would let the first match consume the remainder of the line, leaving all later + // `/@id/` occurrences on that line unstripped. + const escapedExternals = externals.map((e) => escapeRegexSpecialChars(e) + '(?:/[^\'"`\\s]+)?'); + + const prefixedExternalRegex = new RegExp( + `${base}${VITE_ID_PREFIX}(${escapedExternals.join('|')})`, + 'g', + ); + + return (code: string) => { + return code.includes(VITE_ID_PREFIX) + ? code.replace(prefixedExternalRegex, (_, externalName) => externalName) + : // don't do anything when code does not contain the Vite prefix + code; + }; +} diff --git a/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin_spec.ts b/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin_spec.ts new file mode 100644 index 000000000000..37d1bdf61fe6 --- /dev/null +++ b/packages/angular/build/src/tools/vite/plugins/id-prefix-plugin_spec.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { createTransformer } from './id-prefix-plugin'; + +describe('createTransformer', () => { + it('should strip the prefix from every occurrence on a single (minified) line', () => { + const transform = createTransformer('/', [ + '@angular/common', + '@angular/common/http', + '@angular/core', + '@angular/router', + ]); + + const minified = + 'import{a}from"/@id/@angular/common/http";' + + 'import{b}from"/@id/@angular/router";' + + 'import{c}from"/@id/@angular/core";'; + + expect(transform(minified)).toBe( + 'import{a}from"@angular/common/http";' + + 'import{b}from"@angular/router";' + + 'import{c}from"@angular/core";', + ); + }); + + it('should strip the prefix from an external with a deep import path', () => { + const transform = createTransformer('/', ['@angular/common']); + + expect(transform('import{h}from"/@id/@angular/common/http";')).toBe( + 'import{h}from"@angular/common/http";', + ); + }); + + it('should strip the prefix when a non-root base is configured', () => { + const transform = createTransformer('/app/', ['@angular/router']); + + expect(transform('import{r}from"/app/@id/@angular/router";')).toBe( + 'import{r}from"@angular/router";', + ); + }); + + it('should strip the prefix from multi-line (unminified) code', () => { + const transform = createTransformer('/', ['@angular/common', '@angular/router']); + + const code = + 'import { CommonModule } from "/@id/@angular/common";\n' + + 'import { Router } from "/@id/@angular/router";\n'; + + expect(transform(code)).toBe( + 'import { CommonModule } from "@angular/common";\n' + + 'import { Router } from "@angular/router";\n', + ); + }); + + it('should not modify imports that are not configured externals', () => { + const transform = createTransformer('/', ['@angular/router']); + + const code = 'import{x}from"/@id/some-other-package";'; + expect(transform(code)).toBe(code); + }); +});