Skip to content

Commit

Permalink
top-level-await support
Browse files Browse the repository at this point in the history
  • Loading branch information
ForsakenHarmony committed Jul 3, 2023
1 parent 17fa0b1 commit 3ba7bf7
Show file tree
Hide file tree
Showing 35 changed files with 1,022 additions and 66 deletions.
2 changes: 1 addition & 1 deletion crates/turbopack-core/src/resolve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1428,7 +1428,7 @@ pub async fn handle_resolve_error(
})
}

/// ModulePart represnts a part of a module.
/// ModulePart represents a part of a module.
///
/// Currently this is used only for ESMs.
#[turbo_tasks::value]
Expand Down
2 changes: 2 additions & 0 deletions crates/turbopack-ecmascript-runtime/js/src/build/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface RequireContextEntry {
type ExternalRequire = (id: ModuleId) => Exports | EsmNamespaceObject;

interface TurbopackNodeBuildContext {
a: AsyncModule;
e: Module["exports"];
r: CommonJsRequire;
x: ExternalRequire;
Expand Down Expand Up @@ -176,6 +177,7 @@ function instantiateModule(id: ModuleId, source: SourceInfo): Module {
// NOTE(alexkirsz) This can fail when the module encounters a runtime error.
try {
moduleFactory.call(module.exports, {
a: asyncModule.bind(null, module),
e: module.exports,
r: commonJsRequire.bind(null, module),
x: externalRequire,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type RefreshContext = {
type RefreshHelpers = RefreshRuntimeGlobals["$RefreshHelpers$"];

interface TurbopackDevBaseContext {
a: AsyncModule;
e: Module["exports"];
r: CommonJsRequire;
f: RequireContextFactory;
Expand Down Expand Up @@ -332,11 +333,12 @@ function instantiateModule(id: ModuleId, source: SourceInfo): Module {
moduleFactory.call(
module.exports,
augmentContext({
a: asyncModule.bind(null, module),
e: module.exports,
r: commonJsRequire.bind(null, module),
f: requireContext.bind(null, module),
i: esmImport.bind(null, module),
s: esmExport.bind(null, module),
s: esmExport.bind(null, module, module.exports),
j: cjsExport.bind(null, module.exports),
v: exportValue.bind(null, module),
n: exportNamespace.bind(null, module),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ interface RequireContextEntry {
}

type ExternalRequire = (id: ModuleId) => Exports | EsmNamespaceObject;
type ExternalImport = (id: ModuleId) => Promise<Exports | EsmNamespaceObject>;

interface TurbopackDevContext {
x: ExternalRequire;
y: ExternalImport;
}

function commonJsRequireContext(
Expand All @@ -27,6 +29,10 @@ function commonJsRequireContext(
: commonJsRequire(sourceModule, entry.id());
}

function externalImport(id: ModuleId) {
return import(id)
}

function externalRequire(
id: ModuleId,
esm: boolean = false
Expand Down Expand Up @@ -62,6 +68,7 @@ externalRequire.resolve = (
function augmentContext(context: TurbopackDevBaseContext): TurbopackDevContext {
const nodejsContext = context as TurbopackDevContext;
nodejsContext.x = externalRequire;
nodejsContext.y = externalImport;
return nodejsContext;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,19 @@ type CommonJsExport = (exports: Record<string, any>) => void;
type EsmImport = (
moduleId: ModuleId,
allowExportDefault: boolean
) => EsmNamespaceObject;
) => EsmNamespaceObject | Promise<EsmNamespaceObject>;
type EsmExport = (exportGetters: Record<string, () => any>) => void;
type ExportValue = (value: any) => void;

type LoadChunk = (chunkPath: ChunkPath) => Promise<any> | undefined;

type ModuleCache = Record<ModuleId, Module>;
type ModuleFactories = Record<ModuleId, ModuleFactory>;

type AsyncModule = (
body: (
handleAsyncDependencies: (deps: Dep[]) => Exports[] | Promise<() => Exports[]>,
asyncResult: (err?: any) => void
) => void,
hasAwait: boolean
) => void;
209 changes: 204 additions & 5 deletions crates/turbopack-ecmascript-runtime/js/src/shared/runtime-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,17 @@ interface Exports {

[key: string]: any;
}

type EsmNamespaceObject = Record<string, any>;

interface BaseModule {
exports: Exports;
exports: Exports | AsyncModulePromise;
error: Error | undefined;
loaded: boolean;
id: ModuleId;
children: ModuleId[];
parents: ModuleId[];
namespaceObject?: EsmNamespaceObject;
namespaceObject?: EsmNamespaceObject | AsyncModulePromise<EsmNamespaceObject>;
}

interface Module extends BaseModule {}
Expand All @@ -36,7 +37,9 @@ interface RequireContextEntry {

interface RequireContext {
(moduleId: ModuleId): Exports | EsmNamespaceObject;

keys(): ModuleId[];

resolve(moduleId: ModuleId): ModuleId;
}

Expand Down Expand Up @@ -76,8 +79,12 @@ function esm(exports: Exports, getters: Record<string, () => any>) {
/**
* Makes the module an ESM with exports
*/
function esmExport(module: Module, getters: Record<string, () => any>) {
esm((module.namespaceObject = module.exports), getters);
function esmExport(
module: Module,
exports: Exports,
getters: Record<string, () => any>
) {
esm((module.namespaceObject = exports), getters);
}

/**
Expand Down Expand Up @@ -112,6 +119,8 @@ const getProto: (obj: any) => any = Object.getPrototypeOf
const LEAF_PROTOTYPES = [null, getProto({}), getProto([]), getProto(getProto)];

/**
* @param raw
* @param ns
* @param allowExportDefault
* * `false`: will have the raw module as default export
* * `true`: will have the default property as default export
Expand All @@ -138,11 +147,37 @@ function interopEsm(
esm(ns, getters);
}

function esmImport(sourceModule: Module, id: ModuleId): EsmNamespaceObject {
function esmImport(
sourceModule: Module,
id: ModuleId
): EsmNamespaceObject | (Promise<EsmNamespaceObject> & AsyncModuleExt) {
const module = getOrInstantiateModuleFromParent(id, sourceModule);
if (module.error) throw module.error;
if (module.namespaceObject) return module.namespaceObject;
const raw = module.exports;

if (isPromise(raw)) {
const promise = raw.then((e) => {
const ns = {};
interopEsm(e, ns, e.__esModule);
return ns;
});

module.namespaceObject = Object.assign(promise, {
get [turbopackExports]() {
return raw[turbopackExports];
},
get [turbopackQueues]() {
return raw[turbopackQueues];
},
get [turbopackError]() {
return raw[turbopackError];
},
} satisfies AsyncModuleExt);

return module.namespaceObject;
}

const ns = (module.namespaceObject = {});
interopEsm(raw, ns, raw.__esModule);
return ns;
Expand Down Expand Up @@ -197,3 +232,167 @@ function requireContext(
function getChunkPath(chunkData: ChunkData): ChunkPath {
return typeof chunkData === "string" ? chunkData : chunkData.path;
}

function isPromise<T = any>(maybePromise: any): maybePromise is Promise<T> {
return (
maybePromise != null &&
typeof maybePromise === "object" &&
"then" in maybePromise &&
typeof maybePromise.then === "function"
);
}

function createPromise<T>() {
let resolve: (value: T | PromiseLike<T>) => void;
let reject: (reason?: any) => void;

const promise = new Promise<T>((res, rej) => {
reject = rej;
resolve = res;
});

return {
promise,
resolve: resolve!,
reject: reject!,
};
}

// everything below is adapted from webpack
// https://github.com/webpack/webpack/blob/6be4065ade1e252c1d8dcba4af0f43e32af1bdc1/lib/runtime/AsyncModuleRuntimeModule.js#L13

const turbopackQueues = Symbol("turbopack queues");
const turbopackExports = Symbol("turbopack exports");
const turbopackError = Symbol("turbopack error");

type AsyncQueueFn = (() => void) & { queueCount: number };
type AsyncQueue = AsyncQueueFn[] & { resolved: boolean };

function resolveQueue(queue?: AsyncQueue) {
if (queue && !queue.resolved) {
queue.resolved = true;
queue.forEach((fn) => fn.queueCount--);
queue.forEach((fn) => (fn.queueCount-- ? fn.queueCount++ : fn()));
}
}

type Dep = Exports | AsyncModulePromise | Promise<Exports>;

type AsyncModuleExt = {
[turbopackQueues]: (fn: (queue: AsyncQueue) => void) => void;
[turbopackExports]: Exports;
[turbopackError]?: any;
};

type AsyncModulePromise<T = Exports> = Promise<T> & AsyncModuleExt;

function wrapDeps(deps: Dep[]): AsyncModuleExt[] {
return deps.map((dep) => {
if (dep !== null && typeof dep === "object") {
if (turbopackQueues in dep) return dep;
if (isPromise(dep)) {
const queue: AsyncQueue = Object.assign([], { resolved: false });

const obj: AsyncModuleExt = {
[turbopackExports]: {},
[turbopackQueues]: (fn: (queue: AsyncQueue) => void) => fn(queue),
};

dep.then(
(res) => {
obj[turbopackExports] = res;
resolveQueue(queue);
},
(err) => {
obj[turbopackError] = err;
resolveQueue(queue);
}
);

return obj;
}
}

const ret: AsyncModuleExt = {
[turbopackExports]: dep,
[turbopackQueues]: () => {},
};

return ret;
});
}

function asyncModule(
module: Module,
body: (
handleAsyncDependencies: (deps: Dep[]) => Exports[] | Promise<() => Exports[]>,
asyncResult: (err?: any) => void
) => void,
hasAwait: boolean
) {
const queue: AsyncQueue | undefined = hasAwait
? Object.assign([], { resolved: true })
: undefined;

const depQueues: Set<AsyncQueue> = new Set();
const exports = module.exports;

const { resolve, reject, promise: rawPromise } = createPromise<Exports>();

const promise: AsyncModulePromise = Object.assign(rawPromise, {
[turbopackExports]: exports,
[turbopackQueues]: (fn) => {
queue && fn(queue);
depQueues.forEach(fn);
promise["catch"](() => {});
},
} satisfies AsyncModuleExt);

module.exports = promise;

function handleAsyncDependencies(deps: Dep[]) {
const currentDeps = wrapDeps(deps);

const getResult = () =>
currentDeps.map((d) => {
if (d[turbopackError]) throw d[turbopackError];
return d[turbopackExports];
});

const { promise, resolve } = createPromise<() => Exports[]>();

const fn: AsyncQueueFn = Object.assign(() => resolve(getResult), {
queueCount: 0,
});

function fnQueue(q: AsyncQueue) {
if (q !== queue && !depQueues.has(q)) {
depQueues.add(q);
if (q && !q.resolved) {
fn.queueCount++;
q.push(fn);
}
}
}

currentDeps.map((dep) => dep[turbopackQueues](fnQueue));

return fn.queueCount ? promise : getResult();
}

function asyncResult(err?: any) {
if (err) {
reject((promise[turbopackError] = err));
} else {
resolve(exports);
}

resolveQueue(queue);
}

body(handleAsyncDependencies, asyncResult);

if (queue) {
queue.resolved = false;
}
}
Loading

0 comments on commit 3ba7bf7

Please sign in to comment.