Skip to content

fix(@angular/build): prepend deploy-url to file loader output paths#32937

Open
maruthang wants to merge 1 commit intoangular:mainfrom
maruthang:fix-32789-deploy-url-file-loader
Open

fix(@angular/build): prepend deploy-url to file loader output paths#32937
maruthang wants to merge 1 commit intoangular:mainfrom
maruthang:fix-32789-deploy-url-file-loader

Conversation

@maruthang
Copy link
Copy Markdown
Contributor

PR Checklist

PR Type

  • Bugfix

What is the current behavior?

When using esbuild's file loader (either via loader build option or import attributes), the deploy-url is not prepended to imported file URLs in JavaScript output. For example, import svgUrl from './image.svg' resolves to ./media/image.svg instead of <deploy-url>/media/image.svg.

This is because esbuild's global publicPath option cannot be set — it would also affect code-splitting chunk import paths, breaking lazy loading.

Issue Number: #32789

What is the new behavior?

A targeted esbuild onEnd plugin post-processes JS output files to prepend publicPath (deploy-url) only to file loader asset references. Asset paths are identified from the build metafile (non-JS, non-CSS, non-sourcemap outputs). Code-splitting chunk paths remain unaffected.

Example:

  • Before: var svg_default = "./media/image.svg";
  • After: var svg_default = "https://un5neftqgjkmem4kvumj8.julianrbryant.com/assets/media/image.svg";

No regression risk: The plugin only modifies JS string references to media assets. It does not affect:

  • Code-splitting chunk paths (dynamic imports)
  • Stylesheet resource URLs (already handled by esbuild's native publicPath in the stylesheet bundler)
  • Component stylesheet external references (already handled by ComponentStylesheetBundler)

Does this PR introduce a breaking change?

  • Yes
  • No

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
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new esbuild plugin to prepend the publicPath (deploy-url) to file loader asset references in JavaScript bundles, ensuring assets are correctly linked when a deployment URL is used. The review feedback highlights several areas for improvement: expanding file extension support to include .mjs and .cjs, handling asset path resolution more accurately when files are in subdirectories, and optimizing the string replacement logic to improve performance and prevent partial path matching issues.

Comment on lines +29 to +37
for (const outputPath of Object.keys(result.metafile.outputs)) {
if (
!outputPath.endsWith('.js') &&
!outputPath.endsWith('.css') &&
!outputPath.endsWith('.map')
) {
assetPaths.push(outputPath);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic for identifying assets should be updated to exclude other JavaScript extensions such as .mjs and .cjs. Currently, if a build produces .mjs files (which is common in server bundles or when outExtension is configured), they will be incorrectly treated as assets and potentially modified if referenced elsewhere.

Suggested change
for (const outputPath of Object.keys(result.metafile.outputs)) {
if (
!outputPath.endsWith('.js') &&
!outputPath.endsWith('.css') &&
!outputPath.endsWith('.map')
) {
assetPaths.push(outputPath);
}
}
for (const outputPath of Object.keys(result.metafile.outputs)) {
if (
!/\.[cm]?js$/.test(outputPath) &&
!outputPath.endsWith('.css') &&
!outputPath.endsWith('.map')
) {
assetPaths.push(outputPath);
}
}

Comment on lines +49 to +51
if (!outputFile.path.endsWith('.js')) {
continue;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This check should also include .mjs and .cjs files to ensure that asset references are updated in all JavaScript output formats, not just those ending in .js.

Suggested change
if (!outputFile.path.endsWith('.js')) {
continue;
}
if (!/\.[cm]?js$/.test(outputFile.path)) {
continue;
}

Comment on lines +56 to +64
for (const assetPath of assetPaths) {
const originalRef = './' + assetPath;
const updatedRef = normalizedPublicPath + assetPath;

if (text.includes(originalRef)) {
text = text.replaceAll(originalRef, updatedRef);
modified = true;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current path matching and replacement logic is likely to fail when the build output is in a subdirectory. You should calculate the relative path from the JS file to the asset for matching, and strip the base output directory from assetPath when constructing the updatedRef.

Comment on lines +56 to +64
for (const assetPath of assetPaths) {
const originalRef = './' + assetPath;
const updatedRef = normalizedPublicPath + assetPath;

if (text.includes(originalRef)) {
text = text.replaceAll(originalRef, updatedRef);
modified = true;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Iterating over all assets and performing text.replaceAll for each one is inefficient (O(N*M)). Consider using a single regular expression to perform all replacements in a single pass. Additionally, sorting assetPaths by length in descending order is recommended to prevent incorrect partial replacements if one asset path is a prefix of another.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant