66 * found in the LICENSE file at https://angular.io/license
77 */
88
9- import { join , relative } from 'node:path' ;
9+ import assert from 'node:assert' ;
10+ import { randomUUID } from 'node:crypto' ;
11+ import { join } from 'node:path' ;
1012import { pathToFileURL } from 'node:url' ;
1113import { fileURLToPath } from 'url' ;
1214import { JavaScriptTransformer } from '../../../tools/esbuild/javascript-transformer' ;
@@ -17,14 +19,14 @@ import { callInitializeIfNeeded } from './node-18-utils';
1719 * @see : https://nodejs.org/api/esm.html#loaders for more information about loaders.
1820 */
1921
22+ const MEMORY_URL_SCHEME = 'memory://' ;
23+
2024export interface ESMInMemoryFileLoaderWorkerData {
2125 outputFiles : Record < string , string > ;
2226 workspaceRoot : string ;
2327}
2428
25- const TRANSFORMED_FILES : Record < string , string > = { } ;
26- const CHUNKS_REGEXP = / f i l e : \/ \/ \/ ( (?: m a i n | r e n d e r - u t i l s ) \. s e r v e r | c h u n k - \w + ) \. m j s / ;
27- let workspaceRootFile : string ;
29+ let memoryVirtualRootUrl : string ;
2830let outputFiles : Record < string , string > ;
2931
3032const javascriptTransformer = new JavaScriptTransformer (
@@ -38,7 +40,14 @@ const javascriptTransformer = new JavaScriptTransformer(
3840callInitializeIfNeeded ( initialize ) ;
3941
4042export function initialize ( data : ESMInMemoryFileLoaderWorkerData ) {
41- workspaceRootFile = pathToFileURL ( join ( data . workspaceRoot , 'index.mjs' ) ) . href ;
43+ // This path does not actually exist but is used to overlay the in memory files with the
44+ // actual filesystem for resolution purposes.
45+ // A custom URL schema (such as `memory://`) cannot be used for the resolve output because
46+ // the in-memory files may use `import.meta.url` in ways that assume a file URL.
47+ // `createRequire` is one example of this usage.
48+ memoryVirtualRootUrl = pathToFileURL (
49+ join ( data . workspaceRoot , `.angular/prerender-root/${ randomUUID ( ) } /` ) ,
50+ ) . href ;
4251 outputFiles = data . outputFiles ;
4352}
4453
@@ -47,49 +56,93 @@ export function resolve(
4756 context : { parentURL : undefined | string } ,
4857 nextResolve : Function ,
4958) {
50- if ( ! isFileProtocol ( specifier ) ) {
51- const normalizedSpecifier = specifier . replace ( / ^ \. \/ / , '' ) ;
52- if ( normalizedSpecifier in outputFiles ) {
59+ // In-memory files loaded from external code will contain a memory scheme
60+ if ( specifier . startsWith ( MEMORY_URL_SCHEME ) ) {
61+ let memoryUrl ;
62+ try {
63+ memoryUrl = new URL ( specifier ) ;
64+ } catch {
65+ assert . fail ( 'External code attempted to use malformed memory scheme: ' + specifier ) ;
66+ }
67+
68+ // Resolve with a URL based from the virtual filesystem root
69+ return {
70+ format : 'module' ,
71+ shortCircuit : true ,
72+ url : new URL ( memoryUrl . pathname . slice ( 1 ) , memoryVirtualRootUrl ) . href ,
73+ } ;
74+ }
75+
76+ // Use next/default resolve if the parent is not from the virtual root
77+ if ( ! context . parentURL ?. startsWith ( memoryVirtualRootUrl ) ) {
78+ return nextResolve ( specifier , context ) ;
79+ }
80+
81+ // Check for `./` and `../` relative specifiers
82+ const isRelative =
83+ specifier [ 0 ] === '.' &&
84+ ( specifier [ 1 ] === '/' || ( specifier [ 1 ] === '.' && specifier [ 2 ] === '/' ) ) ;
85+
86+ // Relative specifiers from memory file should be based from the parent memory location
87+ if ( isRelative ) {
88+ let specifierUrl ;
89+ try {
90+ specifierUrl = new URL ( specifier , context . parentURL ) ;
91+ } catch { }
92+
93+ if (
94+ specifierUrl ?. pathname &&
95+ Object . hasOwn ( outputFiles , specifierUrl . href . slice ( memoryVirtualRootUrl . length ) )
96+ ) {
5397 return {
5498 format : 'module' ,
5599 shortCircuit : true ,
56- // File URLs need to absolute. In Windows these also need to include the drive.
57- // The `/` will be resolved to the drive letter.
58- url : pathToFileURL ( '/' + normalizedSpecifier ) . href ,
100+ url : specifierUrl . href ,
59101 } ;
60102 }
103+
104+ assert . fail (
105+ `In-memory ESM relative file should always exist: '${ context . parentURL } ' --> '${ specifier } '` ,
106+ ) ;
61107 }
62108
109+ // Update the parent URL to allow for module resolution for the workspace.
110+ // This handles bare specifiers (npm packages) and absolute paths.
63111 // Defer to the next hook in the chain, which would be the
64112 // Node.js default resolve if this is the last user-specified loader.
65- return nextResolve (
66- specifier ,
67- isBundleEntryPointOrChunk ( context ) ? { ... context , parentURL : workspaceRootFile } : context ,
68- ) ;
113+ return nextResolve ( specifier , {
114+ ... context ,
115+ parentURL : new URL ( 'index.js' , memoryVirtualRootUrl ) . href ,
116+ } ) ;
69117}
70118
71119export async function load ( url : string , context : { format ?: string | null } , nextLoad : Function ) {
72120 const { format } = context ;
73121
74- // CommonJs modules require no transformations and are not in memory.
75- if ( format !== 'commonjs' && isFileProtocol ( url ) ) {
76- const filePath = fileURLToPath ( url ) ;
77- // Remove '/' or drive letter for Windows that was added in the above 'resolve'.
78- let source = outputFiles [ relative ( '/' , filePath ) ] ?? TRANSFORMED_FILES [ filePath ] ;
122+ // Load the file from memory if the URL is based in the virtual root
123+ if ( url . startsWith ( memoryVirtualRootUrl ) ) {
124+ const source = outputFiles [ url . slice ( memoryVirtualRootUrl . length ) ] ;
125+ assert ( source !== undefined , 'Resolved in-memory ESM file should always exist: ' + url ) ;
126+
127+ // In-memory files have already been transformer during bundling and can be returned directly
128+ return {
129+ format,
130+ shortCircuit : true ,
131+ source,
132+ } ;
133+ }
79134
80- if ( source === undefined ) {
81- source = TRANSFORMED_FILES [ filePath ] = Buffer . from (
82- await javascriptTransformer . transformFile ( filePath ) ,
83- ) . toString ( 'utf-8' ) ;
84- }
135+ // Only module files potentially require transformation. Angular libraries that would
136+ // need linking are ESM only.
137+ if ( format === 'module' && isFileProtocol ( url ) ) {
138+ const filePath = fileURLToPath ( url ) ;
139+ const source = await javascriptTransformer . transformFile ( filePath ) ;
85140
86- if ( source !== undefined ) {
87- return {
88- format,
89- shortCircuit : true ,
90- source,
91- } ;
92- }
141+ return {
142+ format,
143+ shortCircuit : true ,
144+ source,
145+ } ;
93146 }
94147
95148 // Let Node.js handle all other URLs.
@@ -104,10 +157,6 @@ function handleProcessExit(): void {
104157 void javascriptTransformer . close ( ) ;
105158}
106159
107- function isBundleEntryPointOrChunk ( context : { parentURL : undefined | string } ) : boolean {
108- return ! ! context . parentURL && CHUNKS_REGEXP . test ( context . parentURL ) ;
109- }
110-
111160process . once ( 'exit' , handleProcessExit ) ;
112161process . once ( 'SIGINT' , handleProcessExit ) ;
113162process . once ( 'uncaughtException' , handleProcessExit ) ;
0 commit comments