Skip to content

Commit fdaf929

Browse files
committed
feat(turbopack-node): emit loader assets that can be run with systemjs
1 parent 74edbe1 commit fdaf929

File tree

11 files changed

+1180
-5
lines changed

11 files changed

+1180
-5
lines changed
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
3+
/// <reference path="../shared/runtime-utils.ts" />
4+
/// <reference path="../shared-node/base-externals-utils.ts" />
5+
/// <reference path="../shared-node/node-externals-utils.ts" />
6+
/// <reference path="../shared-node/node-wasm-utils.ts" />
7+
8+
// @ts-ignore
9+
10+
const path = require('path')
11+
12+
// @ts-ignore
13+
14+
const RUNTIME_ROOT = './'
15+
16+
enum SourceType {
17+
/**
18+
* The module was instantiated because it was included in an evaluated chunk's
19+
* runtime.
20+
* SourceData is a ChunkPath.
21+
*/
22+
Runtime = 0,
23+
/**
24+
* The module was instantiated because a parent module imported it.
25+
* SourceData is a ModuleId.
26+
*/
27+
Parent = 1,
28+
}
29+
30+
type SourceData = ChunkPath | ModuleId
31+
32+
process.env.TURBOPACK = '1'
33+
34+
interface TurbopackNodeBuildContext extends TurbopackBaseContext<Module> {
35+
R: ResolvePathFromModule
36+
x: ExternalRequire
37+
y: ExternalImport
38+
}
39+
40+
const nodeContextPrototype = Context.prototype as TurbopackNodeBuildContext
41+
42+
type ModuleFactory = (
43+
this: Module['exports'],
44+
context: TurbopackNodeBuildContext
45+
) => unknown
46+
47+
const url = require('url') as typeof import('url')
48+
49+
const moduleFactories: ModuleFactories = new Map()
50+
nodeContextPrototype.M = moduleFactories
51+
const moduleCache: ModuleCache<Module> = Object.create(null)
52+
nodeContextPrototype.c = moduleCache
53+
54+
/**
55+
* Returns an absolute path to the given module's id.
56+
*/
57+
function resolvePathFromModule(
58+
this: TurbopackBaseContext<Module>,
59+
moduleId: string
60+
): string {
61+
const exported = this.r(moduleId)
62+
const exportedPath = exported?.default ?? exported
63+
if (typeof exportedPath !== 'string') {
64+
return exported as any
65+
}
66+
67+
const strippedAssetPrefix = exportedPath.slice(ASSET_PREFIX.length)
68+
const resolved = path.resolve(RUNTIME_ROOT, strippedAssetPrefix)
69+
70+
return url.pathToFileURL(resolved).href
71+
}
72+
nodeContextPrototype.R = resolvePathFromModule
73+
74+
function loadRuntimeChunk(sourcePath: ChunkPath, chunkData: ChunkData): void {
75+
if (typeof chunkData === 'string') {
76+
loadRuntimeChunkPath(sourcePath, chunkData)
77+
} else {
78+
loadRuntimeChunkPath(sourcePath, chunkData.path)
79+
}
80+
}
81+
82+
const loadedChunks = new Set<ChunkPath>()
83+
const unsupportedLoadChunk = Promise.resolve(undefined)
84+
const loadedChunk: Promise<void> = Promise.resolve(undefined)
85+
const chunkCache = new Map<ChunkPath, Promise<void>>()
86+
87+
function clearChunkCache() {
88+
chunkCache.clear()
89+
}
90+
91+
function loadRuntimeChunkPath(
92+
sourcePath: ChunkPath,
93+
chunkPath: ChunkPath
94+
): void {
95+
if (!isJs(chunkPath)) {
96+
// We only support loading JS chunks in Node.js.
97+
// This branch can be hit when trying to load a CSS chunk.
98+
return
99+
}
100+
101+
if (loadedChunks.has(chunkPath)) {
102+
return
103+
}
104+
105+
try {
106+
const resolved = `${RUNTIME_ROOT}${chunkPath}`
107+
const chunkModules: CompressedModuleFactories = require(resolved)
108+
installCompressedModuleFactories(chunkModules, 0, moduleFactories)
109+
loadedChunks.add(chunkPath)
110+
} catch (e) {
111+
let errorMessage = `Failed to load chunk ${chunkPath}`
112+
113+
if (sourcePath) {
114+
errorMessage += ` from runtime for chunk ${sourcePath}`
115+
}
116+
117+
throw new Error(errorMessage, {
118+
cause: e,
119+
})
120+
}
121+
}
122+
123+
function loadChunkAsync(
124+
this: TurbopackBaseContext<Module>,
125+
chunkData: ChunkData
126+
): Promise<void> {
127+
const chunkPath = typeof chunkData === 'string' ? chunkData : chunkData.path
128+
if (!isJs(chunkPath)) {
129+
// We only support loading JS chunks in Node.js.
130+
// This branch can be hit when trying to load a CSS chunk.
131+
return unsupportedLoadChunk
132+
}
133+
134+
let entry = chunkCache.get(chunkPath)
135+
if (entry === undefined) {
136+
try {
137+
// resolve to an absolute path to simplify `require` handling
138+
const resolved = path.resolve(RUNTIME_ROOT, chunkPath)
139+
// TODO: consider switching to `import()` to enable concurrent chunk loading and async file io
140+
// However this is incompatible with hot reloading (since `import` doesn't use the require cache)
141+
const chunkModules: CompressedModuleFactories = require(resolved)
142+
installCompressedModuleFactories(chunkModules, 0, moduleFactories)
143+
entry = loadedChunk
144+
} catch (e) {
145+
const errorMessage = `Failed to load chunk ${chunkPath} from module ${this.m.id}`
146+
147+
// Cache the failure promise, future requests will also get this same rejection
148+
entry = Promise.reject(
149+
new Error(errorMessage, {
150+
cause: e,
151+
})
152+
)
153+
}
154+
chunkCache.set(chunkPath, entry)
155+
}
156+
// TODO: Return an instrumented Promise that React can use instead of relying on referential equality.
157+
return entry
158+
}
159+
contextPrototype.l = loadChunkAsync
160+
161+
function loadChunkAsyncByUrl(
162+
this: TurbopackBaseContext<Module>,
163+
chunkUrl: string
164+
) {
165+
const path = url.fileURLToPath(new URL(chunkUrl, RUNTIME_ROOT)) as ChunkPath
166+
return loadChunkAsync.call(this, path)
167+
}
168+
contextPrototype.L = loadChunkAsyncByUrl
169+
170+
function loadWebAssembly(
171+
chunkPath: ChunkPath,
172+
_edgeModule: () => WebAssembly.Module,
173+
imports: WebAssembly.Imports
174+
) {
175+
const resolved = path.resolve(RUNTIME_ROOT, chunkPath)
176+
177+
return instantiateWebAssemblyFromPath(resolved, imports)
178+
}
179+
contextPrototype.w = loadWebAssembly
180+
181+
function loadWebAssemblyModule(
182+
chunkPath: ChunkPath,
183+
_edgeModule: () => WebAssembly.Module
184+
) {
185+
const resolved = path.resolve(RUNTIME_ROOT, chunkPath)
186+
187+
return compileWebAssemblyFromPath(resolved)
188+
}
189+
contextPrototype.u = loadWebAssemblyModule
190+
191+
function getWorkerBlobURL(_chunks: ChunkPath[]): string {
192+
throw new Error('Worker blobs are not implemented yet for Node.js')
193+
}
194+
195+
nodeContextPrototype.b = getWorkerBlobURL
196+
197+
function instantiateModule(
198+
id: ModuleId,
199+
sourceType: SourceType,
200+
sourceData: SourceData
201+
): Module {
202+
const moduleFactory = moduleFactories.get(id)
203+
if (typeof moduleFactory !== 'function') {
204+
// This can happen if modules incorrectly handle HMR disposes/updates,
205+
// e.g. when they keep a `setTimeout` around which still executes old code
206+
// and contains e.g. a `require("something")` call.
207+
let instantiationReason: string
208+
switch (sourceType) {
209+
case SourceType.Runtime:
210+
instantiationReason = `as a runtime entry of chunk ${sourceData}`
211+
break
212+
case SourceType.Parent:
213+
instantiationReason = `because it was required from module ${sourceData}`
214+
break
215+
default:
216+
invariant(
217+
sourceType,
218+
(sourceType) => `Unknown source type: ${sourceType}`
219+
)
220+
}
221+
throw new Error(
222+
`Module ${id} was instantiated ${instantiationReason}, but the module factory is not available.`
223+
)
224+
}
225+
226+
const module: Module = createModuleObject(id)
227+
const exports = module.exports
228+
moduleCache[id] = module
229+
230+
const context = new (Context as any as ContextConstructor<Module>)(
231+
module,
232+
exports
233+
)
234+
// NOTE(alexkirsz) This can fail when the module encounters a runtime error.
235+
try {
236+
moduleFactory(context, module, exports)
237+
} catch (error) {
238+
module.error = error as any
239+
throw error
240+
}
241+
242+
module.loaded = true
243+
if (module.namespaceObject && module.exports !== module.namespaceObject) {
244+
// in case of a circular dependency: cjs1 -> esm2 -> cjs1
245+
interopEsm(module.exports, module.namespaceObject)
246+
}
247+
248+
return module
249+
}
250+
251+
/**
252+
* Retrieves a module from the cache, or instantiate it if it is not cached.
253+
*/
254+
// @ts-ignore
255+
function getOrInstantiateModuleFromParent(
256+
id: ModuleId,
257+
sourceModule: Module
258+
): Module {
259+
const module = moduleCache[id]
260+
261+
if (module) {
262+
if (module.error) {
263+
throw module.error
264+
}
265+
266+
return module
267+
}
268+
269+
return instantiateModule(id, SourceType.Parent, sourceModule.id)
270+
}
271+
272+
/**
273+
* Instantiates a runtime module.
274+
*/
275+
function instantiateRuntimeModule(
276+
chunkPath: ChunkPath,
277+
moduleId: ModuleId
278+
): Module {
279+
return instantiateModule(moduleId, SourceType.Runtime, chunkPath)
280+
}
281+
282+
/**
283+
* Retrieves a module from the cache, or instantiate it as a runtime module if it is not cached.
284+
*/
285+
// @ts-ignore TypeScript doesn't separate this module space from the browser runtime
286+
function getOrInstantiateRuntimeModule(
287+
chunkPath: ChunkPath,
288+
moduleId: ModuleId
289+
): Module {
290+
const module = moduleCache[moduleId]
291+
if (module) {
292+
if (module.error) {
293+
throw module.error
294+
}
295+
return module
296+
}
297+
298+
return instantiateRuntimeModule(chunkPath, moduleId)
299+
}
300+
301+
const regexJsUrl = /\.js(?:\?[^#]*)?(?:#.*)?$/
302+
/**
303+
* Checks if a given path/URL ends with .js, optionally followed by ?query or #fragment.
304+
*/
305+
function isJs(chunkUrlOrPath: ChunkUrl | ChunkPath): boolean {
306+
return regexJsUrl.test(chunkUrlOrPath)
307+
}
308+
309+
module.exports = (sourcePath: ChunkPath) => ({
310+
m: (id: ModuleId) => getOrInstantiateRuntimeModule(sourcePath, id),
311+
c: (chunkData: ChunkData) => loadRuntimeChunk(sourcePath, chunkData),
312+
})

turbopack/crates/turbopack-ecmascript-runtime/src/nodejs_runtime.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use turbopack_core::{
99
use crate::{asset_context::get_runtime_asset_context, embed_js::embed_static_code};
1010

1111
/// Returns the code for the Node.js production ECMAScript runtime.
12+
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1213
#[turbo_tasks::function]
1314
pub async fn get_nodejs_runtime_code(
1415
environment: ResolvedVc<Environment>,
@@ -51,3 +52,38 @@ pub async fn get_nodejs_runtime_code(
5152

5253
Ok(Code::cell(code.build()))
5354
}
55+
56+
/// Returns the code for the Node.js production ECMAScript runtime.
57+
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
58+
#[turbo_tasks::function]
59+
pub async fn get_nodejs_runtime_code(
60+
environment: ResolvedVc<Environment>,
61+
generate_source_map: bool,
62+
) -> Result<Vc<Code>> {
63+
let asset_context = get_runtime_asset_context(*environment).resolve().await?;
64+
65+
let shared_runtime_utils_code = embed_static_code(
66+
asset_context,
67+
rcstr!("shared/runtime-utils.ts"),
68+
generate_source_map,
69+
);
70+
71+
let shared_base_external_utils_code = embed_static_code(
72+
asset_context,
73+
rcstr!("shared-node/base-externals-utils.ts"),
74+
generate_source_map,
75+
);
76+
77+
let runtime_code = embed_static_code(
78+
asset_context,
79+
rcstr!("nodejs/runtime.web.ts"),
80+
generate_source_map,
81+
);
82+
83+
let mut code = CodeBuilder::default();
84+
code.push_code(&*shared_runtime_utils_code.await?);
85+
code.push_code(&*shared_base_external_utils_code.await?);
86+
code.push_code(&*runtime_code.await?);
87+
88+
Ok(Code::cell(code.build()))
89+
}

0 commit comments

Comments
 (0)