Skip to content

fix(@angular/build): remap metafile paths when workspace root is a symlink or junction#33584

Open
Arul1998 wants to merge 2 commits into
angular:mainfrom
Arul1998:fix/junction-workspace-root-metafile
Open

fix(@angular/build): remap metafile paths when workspace root is a symlink or junction#33584
Arul1998 wants to merge 2 commits into
angular:mainfrom
Arul1998:fix/junction-workspace-root-metafile

Conversation

@Arul1998

Copy link
Copy Markdown

esbuild always resolves its working directory through symbolic links and Windows directory junctions, so when preserveSymlinks is enabled the metafile paths are relative to a different base than the workspace root. This caused initial file detection to silently fail and index.html to be generated without script tags. The metafile paths are now remapped to be relative to the workspace root.

Fixes #32306

PR Checklist

Please check to confirm your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Issue Number: #32306

On Windows, when a project is accessed through a directory junction (or any symlinked path) and preserveSymlinks: true is set, ng build and ng serve produce an index.html without any injected script tags. The build reports success with no errors or warnings, and the entry bundle is misclassified as a lazy chunk in the console output, so the application loads as a blank page.

Root cause: when preserveSymlinks is enabled, the workspace root is intentionally not resolved through symlinks (packages/angular/build/src/builders/application/options.ts). However, esbuild always resolves its working directory through symbolic links and Windows directory junctions, so the paths in the metafile it produces are relative to the resolved directory. BundlerContext assumes metafile paths are relative to the workspace root, e.g.:

  • workspace root (junction): C:\temp-junction-repro
  • lookup key computed by BundlerContext: main-YHY6UYB4.js
  • actual metafile key produced by esbuild: ../../../../temp-junction-repro/main-YHY6UYB4.js

Every metafile lookup therefore misses, no output is registered as an initial file, and index generation silently emits no script tags.

Reproduced on Windows 11 following the steps in #32306 (ng new, preserveSymlinks: true, mklink /J, build/serve via the junction path). The same project builds correctly via its real path.

What is the new behavior?

After each build, if the workspace root resolves through symlinks to a different path, the metafile paths (input/output keys, entryPoint values, and non-external import paths) are remapped to be relative to the workspace root, restoring the invariant the rest of the pipeline relies on. Virtual files (angular: namespaced, bundler-generated <...> files) and external imports are left untouched, and remapped paths keep esbuild's POSIX separator convention. When the workspace root is not behind a symlink, the metafile is untouched.

Verified on Windows 11 against the reproduction from the issue: with this change, ng build via the junction injects <script src="main-....js" type="module"> into index.html and classifies the entry bundle as initial, and ng serve via the junction serves the script tags correctly. Builds via the real path are unaffected. Unit tests for the remapping (including virtual-file and external-import passthrough) are included.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

This also explains #27119, which was closed as not reproducible — that reproduction required a Windows junction plus preserveSymlinks, which is difficult to trigger on non-Windows dev machines (though the same mismatch applies to POSIX symlinked workspace roots).

…mlink or junction

esbuild always resolves its working directory through symbolic links and Windows directory junctions, so when preserveSymlinks is enabled the metafile paths are relative to a different base than the workspace root. This caused initial file detection to silently fail and index.html to be generated without script tags. The metafile paths are now remapped to be relative to the workspace root.

Fixes angular#32306
@google-cla

google-cla Bot commented Jul 16, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

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

Copy link
Copy Markdown

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 mechanism to remap relative paths within an esbuild metafile from a resolved base directory to the target workspace root when preserveSymlinks is enabled. This ensures downstream consumers can rely on metafile paths being relative to the workspace root. The feedback suggests extending this remapping logic to also cover the cssBundle property in the metafile outputs, along with adding a corresponding unit test assertion to verify its behavior.

Comment on lines +547 to +551
for (const [key, value] of Object.entries(metafile.outputs)) {
if (value.entryPoint !== undefined) {
value.entryPoint = remap(value.entryPoint);
}
for (const importRecord of value.imports) {

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 cssBundle property in esbuild's metafile outputs contains a relative path to the generated CSS bundle from the working directory. When preserveSymlinks is enabled, this path will also be relative to the resolved base directory (fromBase) rather than the workspace root (toBase). We should remap cssBundle as well to ensure all relative paths in the metafile are correctly remapped.

Suggested change
for (const [key, value] of Object.entries(metafile.outputs)) {
if (value.entryPoint !== undefined) {
value.entryPoint = remap(value.entryPoint);
}
for (const importRecord of value.imports) {
for (const [key, value] of Object.entries(metafile.outputs)) {
if (value.entryPoint !== undefined) {
value.entryPoint = remap(value.entryPoint);
}
if (value.cssBundle !== undefined) {
value.cssBundle = remap(value.cssBundle);
}
for (const importRecord of value.imports) {

Comment on lines +23 to +48
it('remaps input and output paths onto the target base directory', () => {
const metafile: Metafile = {
inputs: {
[fromBaseRelative('src/main.ts')]: { bytes: 10, imports: [] },
},
outputs: {
[fromBaseRelative('main.js')]: {
bytes: 100,
inputs: { [fromBaseRelative('src/main.ts')]: { bytesInOutput: 10 } },
imports: [{ path: fromBaseRelative('chunk-ABC.js'), kind: 'import-statement' }],
exports: [],
entryPoint: fromBaseRelative('src/main.ts'),
},
},
};

remapMetafileBasePath(metafile, fromBase, toBase);

expect(Object.keys(metafile.inputs)).toEqual(['src/main.ts']);
expect(Object.keys(metafile.outputs)).toEqual(['main.js']);

const output = metafile.outputs['main.js'];
expect(output.entryPoint).toBe('src/main.ts');
expect(Object.keys(output.inputs)).toEqual(['src/main.ts']);
expect(output.imports[0].path).toBe('chunk-ABC.js');
});

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

Add a test assertion to verify that the cssBundle property is also correctly remapped by remapMetafileBasePath.

  it('remaps input and output paths onto the target base directory', () => {
    const metafile: Metafile = {
      inputs: {
        [fromBaseRelative('src/main.ts')]: { bytes: 10, imports: [] },
      },
      outputs: {
        [fromBaseRelative('main.js')]: {
          bytes: 100,
          inputs: { [fromBaseRelative('src/main.ts')]: { bytesInOutput: 10 } },
          imports: [{ path: fromBaseRelative('chunk-ABC.js'), kind: 'import-statement' }],
          exports: [],
          entryPoint: fromBaseRelative('src/main.ts'),
          cssBundle: fromBaseRelative('main.css'),
        },
      },
    };

    remapMetafileBasePath(metafile, fromBase, toBase);

    expect(Object.keys(metafile.inputs)).toEqual(['src/main.ts']);
    expect(Object.keys(metafile.outputs)).toEqual(['main.js']);

    const output = metafile.outputs['main.js'];
    expect(output.entryPoint).toBe('src/main.ts');
    expect(output.cssBundle).toBe('main.css');
    expect(Object.keys(output.inputs)).toEqual(['src/main.ts']);
    expect(output.imports[0].path).toBe('chunk-ABC.js');
  });

Extends the metafile base path remapping to the cssBundle property of outputs, keeping all metafile path properties consistent with the workspace root. Addresses code review feedback.
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.

ng serve / ng build does not work with windows junctions

1 participant