6
6
* found in the LICENSE file at https://angular.io/license
7
7
*/
8
8
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' ;
10
12
import { pathToFileURL } from 'node:url' ;
11
13
import { fileURLToPath } from 'url' ;
12
14
import { JavaScriptTransformer } from '../../../tools/esbuild/javascript-transformer' ;
@@ -17,14 +19,14 @@ import { callInitializeIfNeeded } from './node-18-utils';
17
19
* @see : https://nodejs.org/api/esm.html#loaders for more information about loaders.
18
20
*/
19
21
22
+ const MEMORY_URL_SCHEME = 'memory://' ;
23
+
20
24
export interface ESMInMemoryFileLoaderWorkerData {
21
25
outputFiles : Record < string , string > ;
22
26
workspaceRoot : string ;
23
27
}
24
28
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 ;
28
30
let outputFiles : Record < string , string > ;
29
31
30
32
const javascriptTransformer = new JavaScriptTransformer (
@@ -38,7 +40,14 @@ const javascriptTransformer = new JavaScriptTransformer(
38
40
callInitializeIfNeeded ( initialize ) ;
39
41
40
42
export 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 ;
42
51
outputFiles = data . outputFiles ;
43
52
}
44
53
@@ -47,49 +56,93 @@ export function resolve(
47
56
context : { parentURL : undefined | string } ,
48
57
nextResolve : Function ,
49
58
) {
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
+ ) {
53
97
return {
54
98
format : 'module' ,
55
99
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 ,
59
101
} ;
60
102
}
103
+
104
+ assert . fail (
105
+ `In-memory ESM relative file should always exist: '${ context . parentURL } ' --> '${ specifier } '` ,
106
+ ) ;
61
107
}
62
108
109
+ // Update the parent URL to allow for module resolution for the workspace.
110
+ // This handles bare specifiers (npm packages) and absolute paths.
63
111
// Defer to the next hook in the chain, which would be the
64
112
// 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
+ } ) ;
69
117
}
70
118
71
119
export async function load ( url : string , context : { format ?: string | null } , nextLoad : Function ) {
72
120
const { format } = context ;
73
121
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
+ }
79
134
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 ) ;
85
140
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
+ } ;
93
146
}
94
147
95
148
// Let Node.js handle all other URLs.
@@ -104,10 +157,6 @@ function handleProcessExit(): void {
104
157
void javascriptTransformer . close ( ) ;
105
158
}
106
159
107
- function isBundleEntryPointOrChunk ( context : { parentURL : undefined | string } ) : boolean {
108
- return ! ! context . parentURL && CHUNKS_REGEXP . test ( context . parentURL ) ;
109
- }
110
-
111
160
process . once ( 'exit' , handleProcessExit ) ;
112
161
process . once ( 'SIGINT' , handleProcessExit ) ;
113
162
process . once ( 'uncaughtException' , handleProcessExit ) ;
0 commit comments