Skip to content

Commit fa7f43b

Browse files
committed
fix(@angular/build): prepend deploy-url to file loader output paths
When using the `file` loader (either via `loader` config or import attributes), the `deploy-url` was not prepended to the imported file's URL in the JavaScript output. This was because esbuild's global `publicPath` option cannot be used as it also affects code-splitting chunk paths. This change introduces a targeted esbuild plugin that post-processes the JS output to prepend the publicPath (deploy-url) only to file loader asset references, identified from the build metafile. This ensures code-splitting chunk paths remain unaffected. Fixes angular#32789
1 parent 7fbc715 commit fa7f43b

3 files changed

Lines changed: 110 additions & 0 deletions

File tree

packages/angular/build/src/builders/application/tests/options/deploy-url_spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,34 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
5858
);
5959
});
6060

61+
it('should prepend deployUrl to file loader output paths in JS', async () => {
62+
await harness.writeFile('./src/a.svg', '<svg></svg>');
63+
await harness.writeFile(
64+
'./src/types.d.ts',
65+
'declare module "*.svg" { const url: string; export default url; }',
66+
);
67+
await harness.writeFile(
68+
'src/main.ts',
69+
`import svgUrl from './a.svg';\nconsole.log(svgUrl);`,
70+
);
71+
72+
harness.useTarget('build', {
73+
...BASE_OPTIONS,
74+
styles: [],
75+
deployUrl: 'https://cdn.example.com/assets/',
76+
loader: {
77+
'.svg': 'file',
78+
},
79+
});
80+
81+
const { result } = await harness.executeOnce();
82+
expect(result?.success).toBeTrue();
83+
harness
84+
.expectFile('dist/browser/main.js')
85+
.content.toContain('https://cdn.example.com/assets/media/a.svg');
86+
harness.expectFile('dist/browser/media/a.svg').toExist();
87+
});
88+
6189
it('should update resources component stylesheets to reference deployURL', async () => {
6290
await harness.writeFile('src/app/test.svg', '<svg></svg>');
6391
await harness.writeFile(

packages/angular/build/src/tools/esbuild/application-code-bundle.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { createAngularLocalizeInitWarningPlugin } from './angular-localize-init-
2626
import { BundlerOptionsFactory } from './bundler-context';
2727
import { createCompilerPluginOptions } from './compiler-plugin-options';
2828
import { createExternalPackagesPlugin } from './external-packages-plugin';
29+
import { createFileLoaderPublicPathPlugin } from './file-loader-public-path-plugin';
2930
import { createAngularLocaleDataPlugin } from './i18n-locale-plugin';
3031
import type { LoadResultCache } from './load-result-cache';
3132
import { createLoaderImportAttributePlugin } from './loader-import-attribute-plugin';
@@ -550,6 +551,7 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
550551
i18nOptions,
551552
customConditions,
552553
frameworkVersion,
554+
publicPath,
553555
} = options;
554556

555557
// Ensure unique hashes for i18n translation changes when using post-process inlining.
@@ -595,6 +597,13 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
595597
createSourcemapIgnorelistPlugin(),
596598
];
597599

600+
// Prepend publicPath (deploy-url) to file loader output paths in JS bundles.
601+
// This is done as a targeted post-process step rather than setting esbuild's global
602+
// `publicPath` option which would also incorrectly affect code-splitting chunk paths.
603+
if (publicPath) {
604+
plugins.push(createFileLoaderPublicPathPlugin(publicPath));
605+
}
606+
598607
let packages: BuildOptions['packages'] = 'bundle';
599608
if (options.externalPackages) {
600609
// Package files affected by a customized loader should not be implicitly marked as external
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 type { Plugin } from 'esbuild';
10+
11+
/**
12+
* Creates an esbuild plugin that prepends the publicPath (deploy-url) to file loader
13+
* output paths in JavaScript bundles. This is done as a targeted post-process step
14+
* to avoid setting esbuild's global `publicPath` option which would also affect
15+
* code-splitting chunk paths.
16+
*/
17+
export function createFileLoaderPublicPathPlugin(publicPath: string): Plugin {
18+
return {
19+
name: 'angular-file-loader-public-path',
20+
setup(build) {
21+
build.onEnd((result) => {
22+
if (!result.metafile || !result.outputFiles) {
23+
return;
24+
}
25+
26+
// Collect relative paths of file loader assets from the metafile.
27+
// These are output entries that are not JS, CSS, or sourcemap files.
28+
const assetPaths: string[] = [];
29+
for (const outputPath of Object.keys(result.metafile.outputs)) {
30+
if (
31+
!outputPath.endsWith('.js') &&
32+
!outputPath.endsWith('.css') &&
33+
!outputPath.endsWith('.map')
34+
) {
35+
assetPaths.push(outputPath);
36+
}
37+
}
38+
39+
if (assetPaths.length === 0) {
40+
return;
41+
}
42+
43+
// Ensure publicPath ends with a separator for correct URL joining.
44+
const normalizedPublicPath = publicPath.endsWith('/') ? publicPath : publicPath + '/';
45+
46+
// Update JS output files to prepend publicPath to file loader asset references.
47+
// esbuild references these assets as "./<assetPath>" in the JS output.
48+
for (const outputFile of result.outputFiles) {
49+
if (!outputFile.path.endsWith('.js')) {
50+
continue;
51+
}
52+
53+
let text = outputFile.text;
54+
let modified = false;
55+
56+
for (const assetPath of assetPaths) {
57+
const originalRef = './' + assetPath;
58+
const updatedRef = normalizedPublicPath + assetPath;
59+
60+
if (text.includes(originalRef)) {
61+
text = text.replaceAll(originalRef, updatedRef);
62+
modified = true;
63+
}
64+
}
65+
66+
if (modified) {
67+
outputFile.contents = new TextEncoder().encode(text);
68+
}
69+
}
70+
});
71+
},
72+
};
73+
}

0 commit comments

Comments
 (0)