diff --git a/crates/turbopack-core/src/resolve/mod.rs b/crates/turbopack-core/src/resolve/mod.rs index 40e78e64566a33..8edc3715ba45d4 100644 --- a/crates/turbopack-core/src/resolve/mod.rs +++ b/crates/turbopack-core/src/resolve/mod.rs @@ -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] diff --git a/crates/turbopack-ecmascript-runtime/js/src/build/runtime.ts b/crates/turbopack-ecmascript-runtime/js/src/build/runtime.ts index 978df9c65221ca..175b4696a065d0 100644 --- a/crates/turbopack-ecmascript-runtime/js/src/build/runtime.ts +++ b/crates/turbopack-ecmascript-runtime/js/src/build/runtime.ts @@ -31,6 +31,7 @@ interface RequireContextEntry { type ExternalRequire = (id: ModuleId) => Exports | EsmNamespaceObject; interface TurbopackNodeBuildContext { + a: AsyncModule; e: Module["exports"]; r: CommonJsRequire; x: ExternalRequire; @@ -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, diff --git a/crates/turbopack-ecmascript-runtime/js/src/dev/runtime/base/runtime-base.ts b/crates/turbopack-ecmascript-runtime/js/src/dev/runtime/base/runtime-base.ts index c10b9f91fc75ad..aec0862d9da297 100644 --- a/crates/turbopack-ecmascript-runtime/js/src/dev/runtime/base/runtime-base.ts +++ b/crates/turbopack-ecmascript-runtime/js/src/dev/runtime/base/runtime-base.ts @@ -33,6 +33,7 @@ type RefreshContext = { type RefreshHelpers = RefreshRuntimeGlobals["$RefreshHelpers$"]; interface TurbopackDevBaseContext { + a: AsyncModule; e: Module["exports"]; r: CommonJsRequire; f: RequireContextFactory; @@ -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: dynamicExport.bind(null, module), v: exportValue.bind(null, module), n: exportNamespace.bind(null, module), diff --git a/crates/turbopack-ecmascript-runtime/js/src/dev/runtime/nodejs/runtime-backend-nodejs.ts b/crates/turbopack-ecmascript-runtime/js/src/dev/runtime/nodejs/runtime-backend-nodejs.ts index 2a08b173813679..0449e30421d1c8 100644 --- a/crates/turbopack-ecmascript-runtime/js/src/dev/runtime/nodejs/runtime-backend-nodejs.ts +++ b/crates/turbopack-ecmascript-runtime/js/src/dev/runtime/nodejs/runtime-backend-nodejs.ts @@ -13,9 +13,11 @@ interface RequireContextEntry { } type ExternalRequire = (id: ModuleId) => Exports | EsmNamespaceObject; +type ExternalImport = (id: ModuleId) => Promise; interface TurbopackDevContext { x: ExternalRequire; + y: ExternalImport; } function commonJsRequireContext( @@ -27,6 +29,10 @@ function commonJsRequireContext( : commonJsRequire(sourceModule, entry.id()); } +function externalImport(id: ModuleId) { + return import(id) +} + function externalRequire( id: ModuleId, esm: boolean = false @@ -62,6 +68,7 @@ externalRequire.resolve = ( function augmentContext(context: TurbopackDevBaseContext): TurbopackDevContext { const nodejsContext = context as TurbopackDevContext; nodejsContext.x = externalRequire; + nodejsContext.y = externalImport; return nodejsContext; } diff --git a/crates/turbopack-ecmascript-runtime/js/src/shared/runtime-types.d.ts b/crates/turbopack-ecmascript-runtime/js/src/shared/runtime-types.d.ts index f9ed380b10767a..214ae451846702 100644 --- a/crates/turbopack-ecmascript-runtime/js/src/shared/runtime-types.d.ts +++ b/crates/turbopack-ecmascript-runtime/js/src/shared/runtime-types.d.ts @@ -25,7 +25,7 @@ type CommonJsExport = (exports: Record) => void; type EsmImport = ( moduleId: ModuleId, allowExportDefault: boolean -) => EsmNamespaceObject; +) => EsmNamespaceObject | Promise; type EsmExport = (exportGetters: Record any>) => void; type ExportValue = (value: any) => void; @@ -33,3 +33,11 @@ type LoadChunk = (chunkPath: ChunkPath) => Promise | undefined; type ModuleCache = Record; type ModuleFactories = Record; + +type AsyncModule = ( + body: ( + handleAsyncDependencies: (deps: Dep[]) => Exports[] | Promise<() => Exports[]>, + asyncResult: (err?: any) => void + ) => void, + hasAwait: boolean +) => void; diff --git a/crates/turbopack-ecmascript-runtime/js/src/shared/runtime-utils.ts b/crates/turbopack-ecmascript-runtime/js/src/shared/runtime-utils.ts index c099f3c20d1b15..e1a28252f45ffa 100644 --- a/crates/turbopack-ecmascript-runtime/js/src/shared/runtime-utils.ts +++ b/crates/turbopack-ecmascript-runtime/js/src/shared/runtime-utils.ts @@ -14,18 +14,19 @@ interface Exports { [key: string]: any; } + type EsmNamespaceObject = Record; const REEXPORTED_OBJECTS = Symbol("reexported objects"); interface BaseModule { - exports: Exports; + exports: Exports | AsyncModulePromise; error: Error | undefined; loaded: boolean; id: ModuleId; children: ModuleId[]; parents: ModuleId[]; - namespaceObject?: EsmNamespaceObject; + namespaceObject?: EsmNamespaceObject | AsyncModulePromise; [REEXPORTED_OBJECTS]?: any[]; } @@ -39,7 +40,9 @@ interface RequireContextEntry { interface RequireContext { (moduleId: ModuleId): Exports | EsmNamespaceObject; + keys(): ModuleId[]; + resolve(moduleId: ModuleId): ModuleId; } @@ -79,8 +82,12 @@ function esm(exports: Exports, getters: Record any>) { /** * Makes the module an ESM with exports */ -function esmExport(module: Module, getters: Record any>) { - esm((module.namespaceObject = module.exports), getters); +function esmExport( + module: Module, + exports: Exports, + getters: Record any> +) { + esm((module.namespaceObject = exports), getters); } /** @@ -142,6 +149,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 @@ -168,11 +177,37 @@ function interopEsm( esm(ns, getters); } -function esmImport(sourceModule: Module, id: ModuleId): EsmNamespaceObject { +function esmImport( + sourceModule: Module, + id: ModuleId +): EsmNamespaceObject | (Promise & 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; @@ -227,3 +262,167 @@ function requireContext( function getChunkPath(chunkData: ChunkData): ChunkPath { return typeof chunkData === "string" ? chunkData : chunkData.path; } + +function isPromise(maybePromise: any): maybePromise is Promise { + return ( + maybePromise != null && + typeof maybePromise === "object" && + "then" in maybePromise && + typeof maybePromise.then === "function" + ); +} + +function createPromise() { + let resolve: (value: T | PromiseLike) => void; + let reject: (reason?: any) => void; + + const promise = new Promise((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; + +type AsyncModuleExt = { + [turbopackQueues]: (fn: (queue: AsyncQueue) => void) => void; + [turbopackExports]: Exports; + [turbopackError]?: any; +}; + +type AsyncModulePromise = Promise & 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 = new Set(); + const exports = module.exports; + + const { resolve, reject, promise: rawPromise } = createPromise(); + + 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; + } +} diff --git a/crates/turbopack-ecmascript/src/analyzer/top_level_await.rs b/crates/turbopack-ecmascript/src/analyzer/top_level_await.rs index 507980c3ea554f..61d44e29c04736 100644 --- a/crates/turbopack-ecmascript/src/analyzer/top_level_await.rs +++ b/crates/turbopack-ecmascript/src/analyzer/top_level_await.rs @@ -1,19 +1,24 @@ -use swc_core::ecma::{ - ast::*, - visit::{noop_visit_type, Visit, VisitWith}, +use swc_core::{ + common::Span, + ecma::{ + ast::*, + visit::{noop_visit_type, Visit, VisitWith}, + }, }; -pub(crate) fn has_top_level_await(m: &Program) -> bool { +/// Checks if the program contains a top level await, if it does it will returns +/// the span of the first await. +pub(crate) fn has_top_level_await(m: &Program) -> Option { let mut visitor = TopLevelAwaitVisitor::default(); m.visit_with(&mut visitor); - visitor.has_top_level_await + visitor.top_level_await_span } #[derive(Default)] struct TopLevelAwaitVisitor { - has_top_level_await: bool, + top_level_await_span: Option, } macro_rules! noop { @@ -23,8 +28,10 @@ macro_rules! noop { } impl Visit for TopLevelAwaitVisitor { - fn visit_await_expr(&mut self, _: &AwaitExpr) { - self.has_top_level_await = true; + fn visit_await_expr(&mut self, n: &AwaitExpr) { + if self.top_level_await_span.is_none() { + self.top_level_await_span = Some(n.span); + } } // prevent non top level items from visiting their children diff --git a/crates/turbopack-ecmascript/src/chunk/item.rs b/crates/turbopack-ecmascript/src/chunk/item.rs index 3394440e043c27..0d7a0ab5bcbc44 100644 --- a/crates/turbopack-ecmascript/src/chunk/item.rs +++ b/crates/turbopack-ecmascript/src/chunk/item.rs @@ -41,6 +41,7 @@ impl EcmascriptChunkItemContentVc { pub async fn new( content: EcmascriptModuleContentVc, context: EcmascriptChunkingContextVc, + async_module: OptionAsyncModuleOptionsVc, ) -> Result { let refresh = *context.has_react_refresh().await?; let externals = *context.environment().node_externals().await?; @@ -50,9 +51,12 @@ impl EcmascriptChunkItemContentVc { inner_code: content.inner_code.clone(), source_map: content.source_map, options: if content.is_esm { + let async_module = async_module.await?.clone_value(); + EcmascriptChunkItemOptions { refresh, externals, + async_module, ..Default::default() } } else { @@ -88,8 +92,12 @@ impl EcmascriptChunkItemContentVc { // HACK "__dirname", ]; + if this.options.async_module.is_some() { + args.push("a: __turbopack_async_module__"); + } if this.options.externals { args.push("x: __turbopack_external_require__"); + args.push("y: __turbopack_external_import__"); } if this.options.refresh { args.push("k: __turbopack_refresh__"); @@ -108,8 +116,23 @@ impl EcmascriptChunkItemContentVc { write!(code, "(({{ {} }}) => (() => {{\n\n", args,)?; } + if this.options.async_module.is_some() { + code += "__turbopack_async_module__(async (__turbopack_handle_async_dependencies__, \ + __turbopack_async_result__) => { try {"; + } + let source_map = this.source_map.map(|sm| sm.as_generate_source_map()); code.push_source(&this.inner_code, source_map); + + if let Some(opts) = &this.options.async_module { + write!( + code, + "__turbopack_async_result__();\n}} catch(e) {{ __turbopack_async_result__(e); }} \ + }}, {});", + opts.has_top_level_await + )?; + } + if this.options.this { code += "\n}.call(this) })"; } else { @@ -119,6 +142,14 @@ impl EcmascriptChunkItemContentVc { } } +#[turbo_tasks::value(transparent)] +pub struct OptionAsyncModuleOptions(Option); + +#[derive(PartialEq, Eq, Default, Debug, Clone, Serialize, Deserialize, TraceRawVcs)] +pub struct AsyncModuleOptions { + pub has_top_level_await: bool, +} + #[derive(PartialEq, Eq, Default, Debug, Clone, Serialize, Deserialize, TraceRawVcs)] pub struct EcmascriptChunkItemOptions { /// Whether this chunk item's module factory should include a @@ -133,6 +164,9 @@ pub struct EcmascriptChunkItemOptions { /// Whether this chunk item's module factory should include a /// `__turbopack_external_require__` argument. pub externals: bool, + /// Whether this chunk item's module is async (either has a top level await + /// or is importing async modules). + pub async_module: Option, pub this: bool, pub placeholder_for_future_extensions: (), } diff --git a/crates/turbopack-ecmascript/src/lib.rs b/crates/turbopack-ecmascript/src/lib.rs index 2489045c2a6a07..e32de926eaaa35 100644 --- a/crates/turbopack-ecmascript/src/lib.rs +++ b/crates/turbopack-ecmascript/src/lib.rs @@ -85,7 +85,10 @@ use self::{ tree_shake::asset::EcmascriptModulePartAssetVc, }; use crate::{ - chunk::{EcmascriptChunkPlaceable, EcmascriptChunkPlaceableVc}, + chunk::{ + item::OptionAsyncModuleOptionsVc, EcmascriptChunkPlaceable, EcmascriptChunkPlaceableVc, + EcmascriptExports, + }, code_gen::CodeGenerateable, references::analyze_ecmascript_module, transform::remove_shebang, @@ -135,7 +138,6 @@ struct MemoizedSuccessfulAnalysis { operation: RawVc, references: AssetReferencesReadRef, exports: EcmascriptExportsReadRef, - has_top_level_await: bool, } pub struct EcmascriptModuleAssetBuilder { @@ -313,13 +315,11 @@ impl EcmascriptModuleAssetVc { // We need to store the ReadRefs since we want to keep a snapshot. references: result_value.references.await?, exports: result_value.exports.await?, - has_top_level_await: result_value.has_top_level_await, })); } else if let Some(MemoizedSuccessfulAnalysis { operation, references, exports, - has_top_level_await, }) = &*this.last_successful_analysis.get() { // It's important to connect to the last operation here to keep it active, so @@ -329,7 +329,6 @@ impl EcmascriptModuleAssetVc { references: ReadRef::cell(references.clone()), exports: ReadRef::cell(exports.clone()), code_generation: result_value.code_generation, - has_top_level_await: *has_top_level_await, successful: false, } .cell()); @@ -516,7 +515,19 @@ impl EcmascriptChunkItem for ModuleChunkItem { ) -> Result { let this = self_vc.await?; let content = this.module.module_content(this.context, availability_info); - Ok(EcmascriptChunkItemContentVc::new(content, this.context)) + + let exports = this.module.get_exports().await?; + let async_module_options = if let EcmascriptExports::EsmExports(exports) = *exports { + exports.await?.async_module.module_options() + } else { + OptionAsyncModuleOptionsVc::cell(None) + }; + + Ok(EcmascriptChunkItemContentVc::new( + content, + this.context, + async_module_options, + )) } } diff --git a/crates/turbopack-ecmascript/src/references/async_module.rs b/crates/turbopack-ecmascript/src/references/async_module.rs new file mode 100644 index 00000000000000..f7fc5ca9c60f1d --- /dev/null +++ b/crates/turbopack-ecmascript/src/references/async_module.rs @@ -0,0 +1,132 @@ +use anyhow::Result; +use indexmap::IndexSet; +use swc_core::{ + common::DUMMY_SP, + ecma::ast::{ArrayLit, ArrayPat, Expr, Ident, Pat, Program}, + quote, +}; +use turbo_tasks::{primitives::BoolVc, TryJoinIterExt}; + +use crate::{ + chunk::{ + item::{AsyncModuleOptions, OptionAsyncModuleOptionsVc}, + EcmascriptChunkingContextVc, + }, + code_gen::{CodeGenerateable, CodeGeneration, CodeGenerationVc}, + create_visitor, + references::esm::{base::insert_hoisted_stmt, EsmAssetReferenceVc}, + CodeGenerateableVc, +}; + +#[turbo_tasks::value(shared)] +pub struct AsyncModule { + pub(super) references: IndexSet, + pub(super) has_top_level_await: bool, +} + +#[turbo_tasks::value_impl] +impl AsyncModuleVc { + #[turbo_tasks::function] + pub(super) async fn is_async(self) -> Result { + let this = self.await?; + + if this.has_top_level_await { + return Ok(BoolVc::cell(this.has_top_level_await)); + } + + let references_async = this + .references + .iter() + .map(|r| async { anyhow::Ok(*r.is_async().await?) }) + .try_join() + .await?; + + Ok(BoolVc::cell(references_async.contains(&true))) + } + + #[turbo_tasks::function] + pub(crate) async fn module_options(self) -> Result { + if !*self.is_async().await? { + return Ok(OptionAsyncModuleOptionsVc::cell(None)); + } + + Ok(OptionAsyncModuleOptionsVc::cell(Some(AsyncModuleOptions { + has_top_level_await: self.await?.has_top_level_await, + }))) + } +} + +#[turbo_tasks::value_impl] +impl CodeGenerateable for AsyncModule { + #[turbo_tasks::function] + async fn code_generation( + self_vc: AsyncModuleVc, + _context: EcmascriptChunkingContextVc, + ) -> Result { + let this = self_vc.await?; + let mut visitors = Vec::new(); + + if *self_vc.is_async().await? { + let reference_idents: Vec> = this + .references + .iter() + .map(|r| async { + let referenced_asset = r.get_referenced_asset().await?; + let ident = referenced_asset.get_ident().await?; + anyhow::Ok(ident) + }) + .try_join() + .await?; + + let reference_idents = reference_idents + .into_iter() + .flatten() + .collect::>(); + + if !reference_idents.is_empty() { + visitors.push(create_visitor!(visit_mut_program(program: &mut Program) { + add_async_dependency_handler(program, &reference_idents); + })); + } + } + + Ok(CodeGeneration { visitors }.into()) + } +} + +fn add_async_dependency_handler(program: &mut Program, idents: &IndexSet) { + let idents = idents + .iter() + .map(|ident| Ident::new(ident.clone().into(), DUMMY_SP)) + .collect::>(); + + let stmt = quote!( + "var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__($deps);" + as Stmt, + deps: Expr = Expr::Array(ArrayLit { + span: DUMMY_SP, + elems: idents + .iter() + .map(|ident| { Some(Expr::Ident(ident.clone()).into()) }) + .collect(), + }), + ); + + insert_hoisted_stmt(program, stmt); + + let stmt = quote!( + "($deps = __turbopack_async_dependencies__.then ? (await \ + __turbopack_async_dependencies__)() : __turbopack_async_dependencies__);" as Stmt, + deps: Pat = Pat::Array(ArrayPat { + span: DUMMY_SP, + elems: idents + .into_iter() + .map(|ident| { Some(ident.into()) }) + .collect(), + optional: false, + type_ann: None, + }), + ); + + insert_hoisted_stmt(program, stmt); +} diff --git a/crates/turbopack-ecmascript/src/references/esm/base.rs b/crates/turbopack-ecmascript/src/references/esm/base.rs index 5031e548523135..5fe5676364ce0c 100644 --- a/crates/turbopack-ecmascript/src/references/esm/base.rs +++ b/crates/turbopack-ecmascript/src/references/esm/base.rs @@ -5,7 +5,10 @@ use swc_core::{ ecma::ast::{Expr, ExprStmt, Ident, Lit, Module, ModuleItem, Program, Script, Stmt}, quote, }; -use turbo_tasks::{primitives::StringVc, Value, ValueToString, ValueToStringVc}; +use turbo_tasks::{ + primitives::{BoolVc, StringVc}, + Value, ValueToString, ValueToStringVc, +}; use turbopack_core::{ asset::Asset, chunk::{ @@ -23,7 +26,10 @@ use turbopack_core::{ use crate::{ analyzer::imports::ImportAnnotations, - chunk::{EcmascriptChunkPlaceable, EcmascriptChunkPlaceableVc, EcmascriptChunkingContextVc}, + chunk::{ + EcmascriptChunkPlaceable, EcmascriptChunkPlaceableVc, EcmascriptChunkingContextVc, + EcmascriptExports, + }, code_gen::{CodeGenerateable, CodeGenerateableVc, CodeGeneration, CodeGenerationVc}, create_visitor, magic_identifier, references::util::{request_to_string, throw_module_not_found_expr}, @@ -116,16 +122,6 @@ impl EsmAssetReference { #[turbo_tasks::value_impl] impl EsmAssetReferenceVc { - #[turbo_tasks::function] - pub(super) async fn get_referenced_asset(self) -> Result { - let this = self.await?; - - Ok(ReferencedAssetVc::from_resolve_result( - self.resolve_reference(), - this.request, - )) - } - #[turbo_tasks::function] pub fn new( origin: ResolveOriginVc, @@ -140,6 +136,37 @@ impl EsmAssetReferenceVc { export_name, }) } + + #[turbo_tasks::function] + pub(crate) async fn get_referenced_asset(self) -> Result { + let this = self.await?; + + Ok(ReferencedAssetVc::from_resolve_result( + self.resolve_reference(), + this.request, + )) + } + + #[turbo_tasks::function] + pub(crate) async fn is_async(self) -> Result { + let asset = self.get_referenced_asset().await?; + + let placeable = match &*asset { + ReferencedAsset::Some(placeable) => placeable, + // TODO(WEB-1259): we need to detect if external modules are esm + ReferencedAsset::OriginalReferenceTypeExternal(_) | ReferencedAsset::None => { + return Ok(BoolVc::cell(false)); + } + }; + + let exports = placeable.get_exports().await?; + + let EcmascriptExports::EsmExports(exports) = &*exports else { + return Ok(BoolVc::cell(false)); + }; + + Ok(exports.await?.async_module.is_async()) + } } #[turbo_tasks::value_impl] diff --git a/crates/turbopack-ecmascript/src/references/esm/export.rs b/crates/turbopack-ecmascript/src/references/esm/export.rs index 0e7023eb513240..b13e25570761e8 100644 --- a/crates/turbopack-ecmascript/src/references/esm/export.rs +++ b/crates/turbopack-ecmascript/src/references/esm/export.rs @@ -27,7 +27,7 @@ use crate::{ }, code_gen::{CodeGenerateable, CodeGenerateableVc, CodeGeneration, CodeGenerationVc}, create_visitor, - references::esm::base::insert_hoisted_stmt, + references::{async_module::AsyncModuleVc, esm::base::insert_hoisted_stmt}, }; #[derive(Clone, Hash, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)] @@ -136,6 +136,7 @@ async fn expand_star_exports(root_asset: EcmascriptChunkPlaceableVc) -> Result, pub star_exports: Vec, + pub async_module: AsyncModuleVc, } #[turbo_tasks::value_impl] diff --git a/crates/turbopack-ecmascript/src/references/mod.rs b/crates/turbopack-ecmascript/src/references/mod.rs index cebab8e317b857..9967181802a3ad 100644 --- a/crates/turbopack-ecmascript/src/references/mod.rs +++ b/crates/turbopack-ecmascript/src/references/mod.rs @@ -1,4 +1,5 @@ pub mod amd; +pub mod async_module; pub mod cjs; pub mod constant_condition; pub mod constant_value; @@ -45,7 +46,7 @@ use swc_core::{ }, }; use turbo_tasks::{ - primitives::{BoolVc, RegexVc}, + primitives::{BoolVc, RegexVc, StringVc}, TryJoinIterExt, Value, }; use turbo_tasks_fs::{FileJsonContent, FileSystemPathVc}; @@ -53,7 +54,7 @@ use turbopack_core::{ asset::{Asset, AssetVc}, compile_time_info::{CompileTimeInfoVc, FreeVarReference}, error::PrettyPrintError, - issue::{IssueSourceVc, OptionIssueSourceVc}, + issue::{analyze::AnalyzeIssue, IssueSeverity, IssueSourceVc, OptionIssueSourceVc}, reference::{AssetReferenceVc, AssetReferencesVc, SourceMapReferenceVc}, reference_type::{CommonJsReferenceSubType, ReferenceType}, resolve::{ @@ -118,6 +119,7 @@ use crate::{ }, magic_identifier, references::{ + async_module::AsyncModule, cjs::{ CjsRequireAssetReferenceVc, CjsRequireCacheAccess, CjsRequireResolveAssetReferenceVc, }, @@ -136,7 +138,6 @@ pub struct AnalyzeEcmascriptModuleResult { pub references: AssetReferencesVc, pub code_generation: CodeGenerateablesVc, pub exports: EcmascriptExportsVc, - pub has_top_level_await: bool, /// `true` when the analysis was successful. pub successful: bool, } @@ -173,7 +174,6 @@ pub(crate) struct AnalyzeEcmascriptModuleResultBuilder { references: IndexSet, code_gens: Vec, exports: EcmascriptExports, - has_top_level_await: bool, successful: bool, } @@ -183,7 +183,6 @@ impl AnalyzeEcmascriptModuleResultBuilder { references: IndexSet::new(), code_gens: Vec::new(), exports: EcmascriptExports::None, - has_top_level_await: false, successful: false, } } @@ -222,11 +221,6 @@ impl AnalyzeEcmascriptModuleResultBuilder { self.exports = exports; } - /// Sets whether the analysed module has a top level await. - pub fn set_top_level_await(&mut self, top_level_await: bool) { - self.has_top_level_await = top_level_await; - } - /// Sets whether the analysis was successful. pub fn set_successful(&mut self, successful: bool) { self.successful = successful; @@ -254,7 +248,6 @@ impl AnalyzeEcmascriptModuleResultBuilder { references: AssetReferencesVc::cell(references), code_generation: CodeGenerateablesVc::cell(self.code_gens), exports: self.exports.into(), - has_top_level_await: self.has_top_level_await, successful: self.successful, }, )) @@ -450,11 +443,6 @@ pub(crate) async fn analyze_ecmascript_module( }), ); - let has_top_level_await = - set_handler_and_globals(&handler, globals, || has_top_level_await(program)); - - analysis.set_top_level_await(has_top_level_await); - let mut var_graph = set_handler_and_globals(&handler, globals, || create_graph(program, eval_context)); @@ -568,6 +556,10 @@ pub(crate) async fn analyze_ecmascript_module( } } + let top_level_await_span = + set_handler_and_globals(&handler, globals, || has_top_level_await(program)); + let has_top_level_await = top_level_await_span.is_some(); + let exports = if !esm_exports.is_empty() || !esm_star_exports.is_empty() { if matches!(specified_type, SpecifiedModuleType::CommonJs) { SpecifiedModuleTypeIssue { @@ -579,15 +571,30 @@ pub(crate) async fn analyze_ecmascript_module( .emit(); } + let async_module = AsyncModule { + references: import_references.iter().copied().collect(), + has_top_level_await, + } + .cell(); + let esm_exports: EsmExportsVc = EsmExports { exports: esm_exports, star_exports: esm_star_exports, + async_module, } .cell(); analysis.add_code_gen(esm_exports); + analysis.add_code_gen(async_module); EcmascriptExports::EsmExports(esm_exports) } else if matches!(specified_type, SpecifiedModuleType::EcmaScript) { + let async_module = AsyncModule { + references: import_references.iter().copied().collect(), + has_top_level_await, + } + .cell(); + analysis.add_code_gen(async_module); + match detect_dynamic_export(program) { DetectedDynamicExportType::CommonJs => { SpecifiedModuleTypeIssue { @@ -601,6 +608,7 @@ pub(crate) async fn analyze_ecmascript_module( EsmExports { exports: Default::default(), star_exports: Default::default(), + async_module, } .cell(), ) @@ -612,6 +620,7 @@ pub(crate) async fn analyze_ecmascript_module( EsmExports { exports: Default::default(), star_exports: Default::default(), + async_module, } .cell(), ), @@ -632,6 +641,25 @@ pub(crate) async fn analyze_ecmascript_module( } }; + if let Some(span) = top_level_await_span { + if !matches!(exports, EcmascriptExports::EsmExports(_)) { + AnalyzeIssue { + code: None, + category: StringVc::cell("analyze".to_string()), + message: StringVc::cell( + "top level await is only supported in ESM modules.".to_string(), + ), + source_ident: source.ident(), + severity: IssueSeverity::Error.into(), + source: Some(issue_source(source, span)), + title: StringVc::cell("unexpected top level await".to_string()), + } + .cell() + .as_issue() + .emit(); + } + } + analysis.set_exports(exports); let effects = take(&mut var_graph.effects); diff --git a/crates/turbopack-ecmascript/src/tree_shake/chunk_item.rs b/crates/turbopack-ecmascript/src/tree_shake/chunk_item.rs index 94b4c8bcbbde4c..b0fc3917c3efb6 100644 --- a/crates/turbopack-ecmascript/src/tree_shake/chunk_item.rs +++ b/crates/turbopack-ecmascript/src/tree_shake/chunk_item.rs @@ -10,8 +10,9 @@ use turbopack_core::{ use super::{asset::EcmascriptModulePartAssetVc, part_of_module, split_module}; use crate::{ chunk::{ - EcmascriptChunkItem, EcmascriptChunkItemContentVc, EcmascriptChunkItemVc, - EcmascriptChunkingContextVc, + item::OptionAsyncModuleOptionsVc, EcmascriptChunkItem, EcmascriptChunkItemContentVc, + EcmascriptChunkItemVc, EcmascriptChunkPlaceable, EcmascriptChunkingContextVc, + EcmascriptExports, }, EcmascriptModuleContentVc, }; @@ -56,7 +57,18 @@ impl EcmascriptChunkItem for EcmascriptModulePartChunkItem { availability_info, ); - Ok(EcmascriptChunkItemContentVc::new(content, this.context)) + let exports = module.full_module.get_exports().await?; + let async_module_options = if let EcmascriptExports::EsmExports(exports) = *exports { + exports.await?.async_module.module_options() + } else { + OptionAsyncModuleOptionsVc::cell(None) + }; + + Ok(EcmascriptChunkItemContentVc::new( + content, + this.context, + async_module_options, + )) } #[turbo_tasks::function] diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/Actions.js b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/Actions.js new file mode 100644 index 00000000000000..0f2170c5a2d335 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/Actions.js @@ -0,0 +1,28 @@ +// import() doesn't care about whether a module is an async module or not +const UserApi = import("./UserAPI.js"); + +export const CreateUserAction = async (name) => { + console.log("Creating user", name); + // These are normal awaits, because they are in an async function + const { createUser } = await UserApi; + await createUser(name); +}; + +// You can place import() where you like +// Placing it at top-level will start loading and evaluating on +// module evaluation. +// see CreateUserAction above +// Here: Connecting to the DB starts when the application starts +// Placing it inside of an (async) function will start loading +// and evaluating when the function is called for the first time +// which basically makes it lazy-loaded. +// see AlternativeCreateUserAction below +// Here: Connecting to the DB starts when AlternativeCreateUserAction +// is called +export const AlternativeCreateUserAction = async (name) => { + const { createUser } = await import("./UserAPI.js"); + await createUser(name); +}; + +// Note: Using await import() at top-level doesn't make much sense +// except in rare cases. It will import modules sequentially. diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/README.md b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/README.md new file mode 100644 index 00000000000000..723f730dc51101 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/README.md @@ -0,0 +1,2 @@ +Adapted from webpack +https://github.com/webpack/webpack/blob/6be4065ade1e252c1d8dcba4af0f43e32af1bdc1/examples/top-level-await/README.md diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js new file mode 100644 index 00000000000000..810fe24f9aa7f5 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js @@ -0,0 +1,7 @@ +import { dbCall } from "./db-connection.js"; + +export const createUser = async (name) => { + const command = `CREATE USER ${name}`; + // This is a normal await, because it's in an async function + await dbCall({ command }); +}; diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/db-connection.js b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/db-connection.js new file mode 100644 index 00000000000000..412eee71b72cf5 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/db-connection.js @@ -0,0 +1,18 @@ +const connectToDB = async (url) => { + console.log("connecting to db", url); + await new Promise((r) => setTimeout(r, 1000)); +}; + +// This is a top-level-await +await connectToDB("my-sql://example.com"); + +export const dbCall = async (data) => { + console.log("dbCall", data); + // This is a normal await, because it's in an async function + await new Promise((r) => setTimeout(r, 100)); + return "fake data"; +}; + +export const close = () => { + console.log("closes the DB connection"); +}; diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/index.js b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/index.js new file mode 100644 index 00000000000000..db6ef5c78f9a9d --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/index.js @@ -0,0 +1,6 @@ +import { CreateUserAction } from "./Actions.js"; + +(async () => { + await CreateUserAction("John"); + console.log("created user John"); +})(); diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_030d34.js b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_030d34.js new file mode 100644 index 00000000000000..a1cab153afd7d9 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_030d34.js @@ -0,0 +1,48 @@ +(globalThis.TURBOPACK = globalThis.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_030d34.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/db-connection.js (ecmascript)": (({ r: __turbopack_require__, f: __turbopack_require_context__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, n: __turbopack_export_namespace__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, g: global, __dirname, a: __turbopack_async_module__, k: __turbopack_refresh__ }) => (() => { + +__turbopack_async_module__(async (__turbopack_handle_async_dependencies__, __turbopack_async_result__) => { try {__turbopack_esm__({ + "close": ()=>close, + "dbCall": ()=>dbCall +}); +const connectToDB = async (url)=>{ + console.log("connecting to db", url); + await new Promise((r)=>setTimeout(r, 1000)); +}; +await connectToDB("my-sql://example.com"); +const dbCall = async (data)=>{ + console.log("dbCall", data); + await new Promise((r)=>setTimeout(r, 100)); + return "fake data"; +}; +const close = ()=>{ + console.log("closes the DB connection"); +}; +__turbopack_async_result__(); +} catch(e) { __turbopack_async_result__(e); } }, true); +})()), +"[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js (ecmascript)": (({ r: __turbopack_require__, f: __turbopack_require_context__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, n: __turbopack_export_namespace__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, g: global, __dirname, a: __turbopack_async_module__, k: __turbopack_refresh__ }) => (() => { + +__turbopack_async_module__(async (__turbopack_handle_async_dependencies__, __turbopack_async_result__) => { try {__turbopack_esm__({ + "createUser": ()=>createUser +}); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$top$2d$level$2d$await$2f$input$2f$db$2d$connection$2e$js__$28$ecmascript$29$__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/db-connection.js (ecmascript)"); +var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__([ + __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$top$2d$level$2d$await$2f$input$2f$db$2d$connection$2e$js__$28$ecmascript$29$__ +]); +[__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$top$2d$level$2d$await$2f$input$2f$db$2d$connection$2e$js__$28$ecmascript$29$__] = __turbopack_async_dependencies__.then ? (await __turbopack_async_dependencies__)() : __turbopack_async_dependencies__; +"__TURBOPACK__ecmascript__hoisting__location__"; +; +const createUser = async (name)=>{ + const command = `CREATE USER ${name}`; + await __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$top$2d$level$2d$await$2f$input$2f$db$2d$connection$2e$js__$28$ecmascript$29$__["dbCall"]({ + command + }); +}; +__turbopack_async_result__(); +} catch(e) { __turbopack_async_result__(e); } }, false); +})()), +}]); + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_030d34.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_030d34.js.map b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_030d34.js.map new file mode 100644 index 00000000000000..fada634db8b2e3 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_030d34.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 113}, "map": {"version":3,"sources":["/turbopack/[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/db-connection.js"],"sourcesContent":["const connectToDB = async (url) => {\n console.log(\"connecting to db\", url);\n await new Promise((r) => setTimeout(r, 1000));\n};\n\n// This is a top-level-await\nawait connectToDB(\"my-sql://example.com\");\n\nexport const dbCall = async (data) => {\n console.log(\"dbCall\", data);\n // This is a normal await, because it's in an async function\n await new Promise((r) => setTimeout(r, 100));\n return \"fake data\";\n};\n\nexport const close = () => {\n console.log(\"closes the DB connection\");\n};\n"],"names":[],"mappings":";;;;AAAA,MAAM,cAAc,OAAO;IACzB,QAAQ,GAAG,CAAC,oBAAoB;IAChC,MAAM,IAAI,QAAQ,CAAC,IAAM,WAAW,GAAG;AACzC;AAGA,MAAM,YAAY;AAEX,MAAM,SAAS,OAAO;IAC3B,QAAQ,GAAG,CAAC,UAAU;IAEtB,MAAM,IAAI,QAAQ,CAAC,IAAM,WAAW,GAAG;IACvC,OAAO;AACT;AAEO,MAAM,QAAQ;IACnB,QAAQ,GAAG,CAAC;AACd"}}, + {"offset": {"line": 21, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 26, "column": 113}, "map": {"version":3,"sources":["/turbopack/[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js"],"sourcesContent":["import { dbCall } from \"./db-connection.js\";\n\nexport const createUser = async (name) => {\n const command = `CREATE USER ${name}`;\n // This is a normal await, because it's in an async function\n await dbCall({ command });\n};\n"],"names":[],"mappings":";;;;;;;;;;AAEO,MAAM,aAAa,OAAO;IAC/B,MAAM,UAAU,CAAC,YAAY,EAAE,KAAK,CAAC;IAErC,MAAM,uMAAO;QAAE;IAAQ;AACzB"}}, + {"offset": {"line": 42, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_6657ac.js b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_6657ac.js new file mode 100644 index 00000000000000..8c05e55adcc789 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_6657ac.js @@ -0,0 +1,11 @@ +(globalThis.TURBOPACK = globalThis.TURBOPACK || []).push([ + "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_6657ac.js", + {}, +]); +(globalThis.TURBOPACK_CHUNK_LISTS = globalThis.TURBOPACK_CHUNK_LISTS || []).push({ + "path": "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_6657ac.js", + "chunks": [ + "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_d6e51f.js" + ], + "source": "dynamic" +}); \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_853a42.js b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_853a42.js new file mode 100644 index 00000000000000..bed5a724b7d384 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_853a42.js @@ -0,0 +1,11 @@ +(globalThis.TURBOPACK = globalThis.TURBOPACK || []).push([ + "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_853a42.js", + {}, +]); +(globalThis.TURBOPACK_CHUNK_LISTS = globalThis.TURBOPACK_CHUNK_LISTS || []).push({ + "path": "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_853a42.js", + "chunks": [ + "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_030d34.js" + ], + "source": "dynamic" +}); \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_d6e51f.js b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_d6e51f.js new file mode 100644 index 00000000000000..c691f42dd32db4 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_d6e51f.js @@ -0,0 +1,16 @@ +(globalThis.TURBOPACK = globalThis.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_d6e51f.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js (ecmascript, manifest chunk)": (({ r: __turbopack_require__, f: __turbopack_require_context__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, n: __turbopack_export_namespace__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, g: global, __dirname }) => (() => { + +__turbopack_export_value__([ + { + "path": "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_030d34.js", + "included": [ + "[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js (ecmascript)" + ] + }, + "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_853a42.js" +]); + +})()), +}]); \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_d6e51f.js.map b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_d6e51f.js.map new file mode 100644 index 00000000000000..a12b83d3337ca5 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_d6e51f.js.map @@ -0,0 +1,4 @@ +{ + "version": 3, + "sections": [] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_5771e1.js b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_5771e1.js new file mode 100644 index 00000000000000..976c8cdd3f9cf0 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_5771e1.js @@ -0,0 +1,11 @@ +(globalThis.TURBOPACK = globalThis.TURBOPACK || []).push([ + "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_5771e1.js", + {}, +]); +(globalThis.TURBOPACK_CHUNK_LISTS = globalThis.TURBOPACK_CHUNK_LISTS || []).push({ + "path": "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_5771e1.js", + "chunks": [ + "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_b53fce.js" + ], + "source": "entry" +}); \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_b53fce.js b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_b53fce.js new file mode 100644 index 00000000000000..686ae54561c451 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_b53fce.js @@ -0,0 +1,47 @@ +(globalThis.TURBOPACK = globalThis.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_b53fce.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, f: __turbopack_require_context__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, n: __turbopack_export_namespace__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, g: global, __dirname }) => (() => { + +__turbopack_export_value__((__turbopack_import__) => { + return Promise.all([{"path":"output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_d6e51f.js","included":["[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js (ecmascript, manifest chunk)"]},"output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_UserAPI_6657ac.js"].map((chunk) => __turbopack_load__(chunk))).then(() => { + return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js (ecmascript, manifest chunk)"); + }).then((chunks) => { + return Promise.all(chunks.map((chunk) => __turbopack_load__(chunk))); + }).then(() => { + return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js (ecmascript)"); + }); +}); + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/Actions.js (ecmascript)": (({ r: __turbopack_require__, f: __turbopack_require_context__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, n: __turbopack_export_namespace__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, g: global, __dirname, k: __turbopack_refresh__ }) => (() => { + +__turbopack_esm__({ + "AlternativeCreateUserAction": ()=>AlternativeCreateUserAction, + "CreateUserAction": ()=>CreateUserAction +}); +const UserApi = __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js (ecmascript, manifest chunk, loader)")(__turbopack_import__); +const CreateUserAction = async (name)=>{ + console.log("Creating user", name); + const { createUser } = await UserApi; + await createUser(name); +}; +const AlternativeCreateUserAction = async (name)=>{ + const { createUser } = await __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/UserAPI.js (ecmascript, manifest chunk, loader)")(__turbopack_import__); + await createUser(name); +}; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/index.js (ecmascript)": (({ r: __turbopack_require__, f: __turbopack_require_context__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, n: __turbopack_export_namespace__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, g: global, __dirname, k: __turbopack_refresh__ }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$top$2d$level$2d$await$2f$input$2f$Actions$2e$js__$28$ecmascript$29$__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/Actions.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +(async ()=>{ + await __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$top$2d$level$2d$await$2f$input$2f$Actions$2e$js__$28$ecmascript$29$__["CreateUserAction"]("John"); + console.log("created user John"); +})(); + +})()), +}]); + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_b53fce.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_b53fce.js.map b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_b53fce.js.map new file mode 100644 index 00000000000000..071ae668d4723c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_b53fce.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 17, "column": 0}, "map": {"version":3,"sources":["/turbopack/[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/Actions.js"],"sourcesContent":["// import() doesn't care about whether a module is an async module or not\nconst UserApi = import(\"./UserAPI.js\");\n\nexport const CreateUserAction = async (name) => {\n console.log(\"Creating user\", name);\n // These are normal awaits, because they are in an async function\n const { createUser } = await UserApi;\n await createUser(name);\n};\n\n// You can place import() where you like\n// Placing it at top-level will start loading and evaluating on\n// module evaluation.\n// see CreateUserAction above\n// Here: Connecting to the DB starts when the application starts\n// Placing it inside of an (async) function will start loading\n// and evaluating when the function is called for the first time\n// which basically makes it lazy-loaded.\n// see AlternativeCreateUserAction below\n// Here: Connecting to the DB starts when AlternativeCreateUserAction\n// is called\nexport const AlternativeCreateUserAction = async (name) => {\n const { createUser } = await import(\"./UserAPI.js\");\n await createUser(name);\n};\n\n// Note: Using await import() at top-level doesn't make much sense\n// except in rare cases. It will import modules sequentially.\n"],"names":[],"mappings":";;;;AACA,MAAM,UAAU;AAET,MAAM,mBAAmB,OAAO;IACrC,QAAQ,GAAG,CAAC,iBAAiB;IAE7B,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM;IAC7B,MAAM,WAAW;AACnB;AAaO,MAAM,8BAA8B,OAAO;IAChD,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM;IAC7B,MAAM,WAAW;AACnB"}}, + {"offset": {"line": 31, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 35, "column": 0}, "map": {"version":3,"sources":["/turbopack/[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/index.js"],"sourcesContent":["import { CreateUserAction } from \"./Actions.js\";\n\n(async () => {\n await CreateUserAction(\"John\");\n console.log(\"created user John\");\n})();\n"],"names":[],"mappings":";;;AAEC,CAAA;IACC,MAAM,wMAAiB;IACvB,QAAQ,GAAG,CAAC;AACd,CAAA"}}, + {"offset": {"line": 42, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_d8ac4f.js b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_d8ac4f.js new file mode 100644 index 00000000000000..7906e87cf8af44 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_d8ac4f.js @@ -0,0 +1,6 @@ +(globalThis.TURBOPACK = globalThis.TURBOPACK || []).push([ + "output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_d8ac4f.js", + {}, + {"otherChunks":[{"path":"output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_b53fce.js","included":["[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/index.js (ecmascript)"]}],"runtimeModuleIds":["[project]/crates/turbopack-tests/tests/snapshot/basic/top-level-await/input/index.js (ecmascript)"]} +]); +// Dummy runtime \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_d8ac4f.js.map b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_d8ac4f.js.map new file mode 100644 index 00000000000000..a12b83d3337ca5 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/top-level-await/output/crates_turbopack-tests_tests_snapshot_basic_top-level-await_input_index_d8ac4f.js.map @@ -0,0 +1,4 @@ +{ + "version": 3, + "sections": [] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/runtime/default_build_runtime/output/[turbopack]_runtime.js b/crates/turbopack-tests/tests/snapshot/runtime/default_build_runtime/output/[turbopack]_runtime.js index eea9eef065bbd0..b7b3de4387ee86 100644 --- a/crates/turbopack-tests/tests/snapshot/runtime/default_build_runtime/output/[turbopack]_runtime.js +++ b/crates/turbopack-tests/tests/snapshot/runtime/default_build_runtime/output/[turbopack]_runtime.js @@ -24,8 +24,8 @@ function esm(exports, getters) { }); } } -function esmExport(module, getters) { - esm(module.namespaceObject = module.exports, getters); +function esmExport(module, exports, getters) { + esm(module.namespaceObject = exports, getters); } function dynamicExport(module, object) { let reexportedObjects = module[REEXPORTED_OBJECTS]; @@ -88,6 +88,25 @@ function esmImport(sourceModule, id) { 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]; + } + }); + return module.namespaceObject; + } const ns = module.namespaceObject = {}; interopEsm(raw, ns, raw.__esModule); return ns; @@ -120,6 +139,112 @@ function requireContext(sourceModule, map) { function getChunkPath(chunkData) { return typeof chunkData === "string" ? chunkData : chunkData.path; } +function isPromise(maybePromise) { + return maybePromise != null && typeof maybePromise === "object" && "then" in maybePromise && typeof maybePromise.then === "function"; +} +function createPromise() { + let resolve; + let reject; + const promise = new Promise((res, rej)=>{ + reject = rej; + resolve = res; + }); + return { + promise, + resolve: resolve, + reject: reject + }; +} +const turbopackQueues = Symbol("turbopack queues"); +const turbopackExports = Symbol("turbopack exports"); +const turbopackError = Symbol("turbopack error"); +function resolveQueue(queue) { + if (queue && !queue.resolved) { + queue.resolved = true; + queue.forEach((fn)=>fn.queueCount--); + queue.forEach((fn)=>fn.queueCount-- ? fn.queueCount++ : fn()); + } +} +function wrapDeps(deps) { + return deps.map((dep)=>{ + if (dep !== null && typeof dep === "object") { + if (turbopackQueues in dep) return dep; + if (isPromise(dep)) { + const queue = Object.assign([], { + resolved: false + }); + const obj = { + [turbopackExports]: {}, + [turbopackQueues]: (fn)=>fn(queue) + }; + dep.then((res)=>{ + obj[turbopackExports] = res; + resolveQueue(queue); + }, (err)=>{ + obj[turbopackError] = err; + resolveQueue(queue); + }); + return obj; + } + } + const ret = { + [turbopackExports]: dep, + [turbopackQueues]: ()=>{} + }; + return ret; + }); +} +function asyncModule(module, body, hasAwait) { + const queue = hasAwait ? Object.assign([], { + resolved: true + }) : undefined; + const depQueues = new Set(); + const exports = module.exports; + const { resolve, reject, promise: rawPromise } = createPromise(); + const promise = Object.assign(rawPromise, { + [turbopackExports]: exports, + [turbopackQueues]: (fn)=>{ + queue && fn(queue); + depQueues.forEach(fn); + promise["catch"](()=>{}); + } + }); + module.exports = promise; + function handleAsyncDependencies(deps) { + const currentDeps = wrapDeps(deps); + const getResult = ()=>currentDeps.map((d)=>{ + if (d[turbopackError]) throw d[turbopackError]; + return d[turbopackExports]; + }); + const { promise, resolve } = createPromise(); + const fn = Object.assign(()=>resolve(getResult), { + queueCount: 0 + }); + function fnQueue(q) { + 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) { + if (err) { + reject(promise[turbopackError] = err); + } else { + resolve(exports); + } + resolveQueue(queue); + } + body(handleAsyncDependencies, asyncResult); + if (queue) { + queue.resolved = false; + } +} ; var SourceType; (function(SourceType) { @@ -214,6 +339,7 @@ function instantiateModule(id, source) { moduleCache[id] = module1; try { moduleFactory.call(module1.exports, { + a: asyncModule.bind(null, module1), e: module1.exports, r: commonJsRequire.bind(null, module1), x: externalRequire,