fix(@angular/build): remap metafile paths when workspace root is a symlink or junction#33584
fix(@angular/build): remap metafile paths when workspace root is a symlink or junction#33584Arul1998 wants to merge 2 commits into
Conversation
…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
|
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. |
There was a problem hiding this comment.
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.
| for (const [key, value] of Object.entries(metafile.outputs)) { | ||
| if (value.entryPoint !== undefined) { | ||
| value.entryPoint = remap(value.entryPoint); | ||
| } | ||
| for (const importRecord of value.imports) { |
There was a problem hiding this comment.
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.
| 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) { |
| 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'); | ||
| }); |
There was a problem hiding this comment.
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.
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?
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: trueis set,ng buildandng serveproduce anindex.htmlwithout 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
preserveSymlinksis 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.BundlerContextassumes metafile paths are relative to the workspace root, e.g.:C:\temp-junction-reproBundlerContext:main-YHY6UYB4.js../../../../temp-junction-repro/main-YHY6UYB4.jsEvery 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,
entryPointvalues, 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 buildvia the junction injects<script src="main-....js" type="module">intoindex.htmland classifies the entry bundle as initial, andng servevia 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?
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).