@@ -17,7 +17,9 @@ import {
1717 context ,
1818} from 'esbuild' ;
1919import assert from 'node:assert' ;
20- import { basename , extname , join , relative } from 'node:path' ;
20+ import { realpathSync } from 'node:fs' ;
21+ import { basename , extname , join , relative , resolve } from 'node:path' ;
22+ import { toPosixPath } from '../../utils/path' ;
2123import { SERVER_GENERATED_EXTERNALS } from '../../utils/server-rendering/manifest' ;
2224import {
2325 type BuildOutputFile ,
@@ -64,6 +66,7 @@ export class BundlerContext {
6466 #optionsFactory: BundlerOptionsFactory < BuildOptions & { metafile : true ; write : false } > ;
6567 #shouldCacheResult: boolean ;
6668 #loadCache?: MemoryLoadResultCache ;
69+ #realWorkspaceRoot?: string ;
6770 readonly watchFiles = new Set < string > ( ) ;
6871
6972 constructor (
@@ -261,6 +264,17 @@ export class BundlerContext {
261264 }
262265 }
263266
267+ // esbuild always resolves its working directory through symbolic links (including
268+ // Windows directory junctions) and generates metafile paths relative to the resolved
269+ // path. When `preserveSymlinks` is enabled, the workspace root is intentionally not
270+ // resolved, and the metafile paths are then relative to a different base directory.
271+ // The paths are remapped so that all downstream consumers can rely on the documented
272+ // invariant that metafile paths are relative to the workspace root.
273+ this . #realWorkspaceRoot ??= realpathSync ( this . workspaceRoot ) ;
274+ if ( this . #realWorkspaceRoot !== this . workspaceRoot ) {
275+ remapMetafileBasePath ( result . metafile , this . #realWorkspaceRoot, this . workspaceRoot ) ;
276+ }
277+
264278 // Update files that should be watched.
265279 // While this should technically not be linked to incremental mode, incremental is only
266280 // currently enabled with watch mode where watch files are needed.
@@ -487,6 +501,68 @@ export class BundlerContext {
487501 }
488502}
489503
504+ /**
505+ * Remaps all relative paths within an esbuild metafile from one base directory to another.
506+ * Virtual files (e.g., `angular:` namespaced or bundler generated), external imports, and
507+ * non-relative paths are left unmodified.
508+ *
509+ * @param metafile The metafile to update in place.
510+ * @param fromBase The absolute base directory the metafile paths are currently relative to.
511+ * @param toBase The absolute base directory the metafile paths should be made relative to.
512+ */
513+ export function remapMetafileBasePath ( metafile : Metafile , fromBase : string , toBase : string ) : void {
514+ const remapped = new Map < string , string > ( ) ;
515+ const remap = ( value : string ) : string => {
516+ // Skip virtual files and paths with a scheme-like or namespace prefix (e.g., `angular:`)
517+ if (
518+ isInternalAngularFile ( value ) ||
519+ isInternalBundlerFile ( value ) ||
520+ / ^ [ ^ \\ / . ] { 2 , } : / . test ( value )
521+ ) {
522+ return value ;
523+ }
524+
525+ let result = remapped . get ( value ) ;
526+ if ( result === undefined ) {
527+ // esbuild metafile paths always use POSIX path separators
528+ result = toPosixPath ( relative ( toBase , resolve ( fromBase , value ) ) ) ;
529+ remapped . set ( value , result ) ;
530+ }
531+
532+ return result ;
533+ } ;
534+
535+ const inputs : Metafile [ 'inputs' ] = { } ;
536+ for ( const [ key , value ] of Object . entries ( metafile . inputs ) ) {
537+ for ( const importRecord of value . imports ) {
538+ if ( ! importRecord . external ) {
539+ importRecord . path = remap ( importRecord . path ) ;
540+ }
541+ }
542+ inputs [ remap ( key ) ] = value ;
543+ }
544+ metafile . inputs = inputs ;
545+
546+ const outputs : Metafile [ 'outputs' ] = { } ;
547+ for ( const [ key , value ] of Object . entries ( metafile . outputs ) ) {
548+ if ( value . entryPoint !== undefined ) {
549+ value . entryPoint = remap ( value . entryPoint ) ;
550+ }
551+ for ( const importRecord of value . imports ) {
552+ if ( ! importRecord . external ) {
553+ importRecord . path = remap ( importRecord . path ) ;
554+ }
555+ }
556+ const outputInputs : ( typeof value ) [ 'inputs' ] = { } ;
557+ for ( const [ inputKey , inputValue ] of Object . entries ( value . inputs ) ) {
558+ outputInputs [ remap ( inputKey ) ] = inputValue ;
559+ }
560+ value . inputs = outputInputs ;
561+ outputs [ remap ( key ) ] = value ;
562+ }
563+ metafile . outputs = outputs ;
564+ }
565+
490566function isInternalAngularFile ( file : string ) {
491567 return file . startsWith ( 'angular:' ) ;
492568}
0 commit comments