|
export default async function bundle(entryPointURL, allowDynamicImport) { |
|
const loader = new BundleLoader(); |
|
await loader.loadEntryPoint(entryPointURL); |
|
|
|
let bundleSource = ` |
|
const modules = new Map(); |
|
|
|
function importHook(specifier) { |
|
const url = new URL(specifier, this.context.url); |
|
if (modules.has(url)) return modules.get(url); |
|
|
|
${ |
|
allowDynamicImport |
|
? `return import(url, { module: true });` |
|
: `throw new Error(url + " has not been bundled.");` |
|
} |
|
} |
|
|
|
function importMetaHook(meta) { |
|
meta.url = this.context.url; |
|
} |
|
|
|
`; |
|
for (const [url, sourceText] of loader.iterateSources()) { |
|
const urlStr = JSON.stringify(url); |
|
bundleSource += ` |
|
modules.set( |
|
${urlStr}, |
|
new Module(module {${sourceText}}.source, { |
|
importHook, |
|
importMetaHook, |
|
context: { url: ${urlStr} }, |
|
}) |
|
); |
|
`; |
|
} |
|
bundleSource += ` |
|
// Run |
|
await import(modules.get(${JSON.stringify(entryPointURL)})); |
|
`; |
|
|
|
return bundleSource; |
|
} |
|
|
|
class BundleLoader { |
|
#loadedModules = new Map(); |
|
#sources = new Map(); |
|
|
|
iterateSources() { |
|
return this.#sources.entries(); |
|
} |
|
|
|
async loadEntryPoint(url) { |
|
const bootLoader = new Module(BundleLoader.#bootSource, { |
|
importHook: BundleLoader.#bootImportHook, |
|
context: { entryPointURL, loader: this }, |
|
}); |
|
await import(bootLoader); |
|
} |
|
|
|
static #doNotEvaluate = module { |
|
throw new Error("bundler/do-not-evaluate"); |
|
}; |
|
|
|
static #bootSource = module { |
|
import "do-not-evaluate"; |
|
import "entrypoint"; |
|
}.source; |
|
|
|
static #bootImportHook = function (specifier) { |
|
if (specifier === "do-not-evaluate") { |
|
return BundleLoader.#doNotEvaluate; |
|
} |
|
if (specifier === "entrypoint") { |
|
return this.context.loader.#loadModuleCached(this.context.entryPointURL); |
|
} |
|
throw new Error("Assert: unreachable"); |
|
}; |
|
|
|
#loadModuleCached(url) { |
|
if (loadedModules.has(url)) { |
|
return loadedModules.get(url); |
|
} |
|
const res = this.#loadModule(url); |
|
this.#loadedModules.set(url, res); |
|
return res; |
|
} |
|
|
|
async #loadModule(url) { |
|
const response = await fetch(url); |
|
const text = await response.text(); |
|
|
|
const module = new Module(new ModuleSource(text), { |
|
importHook: this.#importHook, |
|
context: { url, loader: this }, |
|
}); |
|
this.#sources.set(url, text); |
|
|
|
return module; |
|
} |
|
|
|
static #importHook = function (specifier) { |
|
const url = new URL(specifier, this.context.url); |
|
return this.context.loader.#loadModuleCached(url); |
|
}; |
|
} |