|
| 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 | +}) |
0 commit comments