diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index ea8170943457..9c11bb9c488f 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -1936,6 +1936,22 @@ pub(crate) unsafe fn pending_part_range_prologue<'a>( (part_range, c_ptr, chunk_ptr, worker) } +/// What replaces an `import` or `export ... from` statement in a chunk. +#[derive(Clone, Copy)] +pub(crate) enum ImportStmtLowering { + /// The statement stays as it is. + Keep, + /// Nothing. + Remove, + /// `var ns = require("path")`, for an external module. + RequireExternal, + /// `var ns = require_x()`, for a file in a `__commonJS` wrapper. + RequireCommonJs, + /// `init_x()`, the call of the imported file's `__esm` wrapper. An async wrapper returns a + /// promise, and the call is awaited. + Init { wrapper_ref: Ref, is_async: bool }, +} + impl<'a> LinkerContext<'a> { pub(crate) fn generate_isolated_hash(&mut self, chunk: &Chunk, arena: &Bump) -> u64 { let _trace = bun::perf::trace("Bundler.generateIsolatedHash"); @@ -2169,22 +2185,10 @@ impl<'a> LinkerContext<'a> { if parent_result_tla_keyword.len > 0 { let source = &input_files[other_source_index as usize]; tla_pretty_path = source.path.pretty; - let mut text = Vec::new(); - use std::io::Write; - write!( - &mut text, - "The top-level await in {} is here:", - bstr::BStr::new(tla_pretty_path) - ) - .expect("infallible: in-memory write"); - notes.push(Data { - text: text.into(), - location: bun_ast::Location::init_or_null( - Some(source), - parent_result_tla_keyword, - ), - ..Default::default() - }); + notes.push(Self::top_level_await_note( + source, + parent_result_tla_keyword, + )); break; } @@ -2198,31 +2202,14 @@ impl<'a> LinkerContext<'a> { other_source_index = parent_tla_check.parent; - let mut text = Vec::new(); - use std::io::Write; - write!( - &mut text, - "The file {} imports the file {} here:", - bstr::BStr::new( - &input_files[parent_source_index as usize].path.pretty - ), - bstr::BStr::new( - &input_files[other_source_index as usize].path.pretty - ), - ) - .unwrap(); - notes.push(Data { - text: text.into(), - location: bun_ast::Location::init_or_null( - Some(&input_files[parent_source_index as usize]), - ast_import_records[parent_source_index as usize].as_slice() - [tla_checks[parent_source_index as usize] - .import_record_index - as usize] - .range, - ), - ..Default::default() - }); + notes.push(Self::import_toward_top_level_await_note( + &input_files[parent_source_index as usize], + &input_files[other_source_index as usize], + ast_import_records[parent_source_index as usize].as_slice() + [tla_checks[parent_source_index as usize].import_record_index + as usize] + .range, + )); } let source: &Source = &input_files[source_index as usize]; @@ -2249,117 +2236,122 @@ impl<'a> LinkerContext<'a> { } } - pub(crate) fn should_remove_import_export_stmt( - &mut self, - stmts: &mut StmtList, - loc: Loc, + /// Decides what `convert_stmts_for_chunk` prints in the place of an `import` or + /// `export ... from` statement. `namespace_ref` is the namespace symbol of the statement. + #[inline(always)] + pub(crate) fn import_stmt_lowering( + &self, + importer_exports_kind: ExportsKind, + importer_exports_ref: Ref, namespace_ref: Ref, - import_record_index: u32, - alloc: &Bump, - ast: &JSAst<'_>, - ) -> Result { - let record = &ast.import_records[import_record_index as usize]; + record: &ImportRecord, + ) -> ImportStmtLowering { // Barrel optimization: deferred import records should be dropped if record.flags.contains(bun_ast::ImportRecordFlags::IS_UNUSED) { - return Ok(true); + return ImportStmtLowering::Remove; } // Is this an external import? if !record.source_index.is_valid() { // Keep the "import" statement if import statements are supported if self.options.output_format.keep_es6_import_export_syntax() { - return Ok(false); + return ImportStmtLowering::Keep; } // Otherwise, replace this statement with a call to "require()" - stmts - .inside_wrapper_prefix - .append_non_dependency(Stmt::alloc( - S::Local { - decls: G::DeclList::from_slice(&[G::Decl { - binding: Binding::alloc( - alloc, - bun_ast::b::Identifier { - r#ref: namespace_ref, - }, - loc, - ), - value: Some(Expr::init( - E::RequireString { - import_record_index, - ..Default::default() - }, - loc, - )), - }]), - ..Default::default() - }, - record.range.loc, - )) - .expect("unreachable"); - return Ok(true); + return ImportStmtLowering::RequireExternal; } // We don't need a call to "require()" if this is a self-import inside a // CommonJS-style module, since we can just reference the exports directly. - if ast.exports_kind == ExportsKind::Cjs + if importer_exports_kind == ExportsKind::Cjs && self .graph .symbols .follow(namespace_ref) - .eql(ast.exports_ref) + .eql(importer_exports_ref) { - return Ok(true); + return ImportStmtLowering::Remove; } - let other_flags = self.graph.meta.items_flags()[record.source_index.get() as usize]; + let other_source_index = record.source_index.get() as usize; + let other_flags = self.graph.meta.items_flags()[other_source_index]; match other_flags.wrap { - WrapKind::None => {} - WrapKind::Cjs => { - // Replace the statement with a call to "require()" since the other module is CJS-wrapped - stmts - .inside_wrapper_prefix - .append_non_dependency(Stmt::alloc( - S::Local { - decls: G::DeclList::from_slice(&[G::Decl { - binding: Binding::alloc( - alloc, - bun_ast::b::Identifier { - r#ref: namespace_ref, - }, - loc, - ), - value: Some(Expr::init( - E::RequireString { - import_record_index, - ..Default::default() - }, - loc, - )), - }]), - ..Default::default() - }, - loc, - ))?; - } + WrapKind::None => ImportStmtLowering::Remove, + // Replace the statement with a call to "require()" since the other module is CJS-wrapped + WrapKind::Cjs => ImportStmtLowering::RequireCommonJs, WrapKind::Esm => { // Ignore this file if it's not included in the bundle. This can happen for // wrapped ESM files but not for wrapped CommonJS files because we allow // tree shaking inside wrapped ESM files. - if !self - .graph - .files_live - .is_set(record.source_index.get() as usize) - { - return Ok(true); + if !self.graph.files_live.is_set(other_source_index) { + return ImportStmtLowering::Remove; } - let wrapper_ref = - self.graph.ast.items_wrapper_ref()[record.source_index.get() as usize]; + let wrapper_ref = self.graph.ast.items_wrapper_ref()[other_source_index]; if wrapper_ref.is_empty() { - return Ok(true); + return ImportStmtLowering::Remove; } // Replace the statement with a call to "init()" + ImportStmtLowering::Init { + wrapper_ref, + is_async: other_flags.is_async_or_has_async_dependency, + } + } + } + } + + pub(crate) fn should_remove_import_export_stmt( + &mut self, + stmts: &mut StmtList, + loc: Loc, + namespace_ref: Ref, + import_record_index: u32, + alloc: &Bump, + ast: &JSAst<'_>, + ) -> Result { + let record = &ast.import_records[import_record_index as usize]; + let require_stmt = |stmt_loc: Loc| { + Stmt::alloc( + S::Local { + decls: G::DeclList::from_slice(&[G::Decl { + binding: Binding::alloc( + alloc, + bun_ast::b::Identifier { + r#ref: namespace_ref, + }, + loc, + ), + value: Some(Expr::init( + E::RequireString { + import_record_index, + ..Default::default() + }, + loc, + )), + }]), + ..Default::default() + }, + stmt_loc, + ) + }; + match self.import_stmt_lowering(ast.exports_kind, ast.exports_ref, namespace_ref, record) { + ImportStmtLowering::Keep => return Ok(false), + ImportStmtLowering::Remove => {} + ImportStmtLowering::RequireExternal => { + stmts + .inside_wrapper_prefix + .append_non_dependency(require_stmt(record.range.loc))?; + } + ImportStmtLowering::RequireCommonJs => { + stmts + .inside_wrapper_prefix + .append_non_dependency(require_stmt(loc))?; + } + ImportStmtLowering::Init { + wrapper_ref, + is_async, + } => { let init_call = Expr::init( E::Call { target: Expr::init_identifier(wrapper_ref, loc), @@ -2368,7 +2360,7 @@ impl<'a> LinkerContext<'a> { loc, ); - if other_flags.is_async_or_has_async_dependency { + if is_async { stmts .inside_wrapper_prefix .append_async_dependency(init_call, self.promise_all_runtime_ref)?; @@ -2383,6 +2375,251 @@ impl<'a> LinkerContext<'a> { Ok(true) } + /// Reports every `import` that would be printed as `await init_x()` inside a `__commonJS` + /// wrapper. That wrapper is a plain function, so the output would not parse. esbuild prints + /// the `await` in its `__commonJS` callback. + /// + /// Runs after tree shaking: only then is it known which files are wrapped and which + /// statements are printed. + pub(crate) fn validate_commonjs_imports_of_async_modules(&self) -> Result<(), BunError> { + // This format has no `__commonJS` wrapper. + if self.options.output_format == Format::InternalBakeDev { + return Ok(()); + } + + let flags = self.graph.meta.items_flags(); + let import_records = self.graph.ast.items_import_records(); + let exports_kinds = self.graph.ast.items_exports_kind(); + let exports_refs = self.graph.ast.items_exports_ref(); + let parts = self.graph.ast.items_parts(); + // Built for the first error. + let mut chains: Option> = None; + + for source_index in self.graph.reachable_files.slice() { + let id = source_index.get() as usize; + if !self.graph.files_live.is_set(id) || flags[id].wrap != WrapKind::Cjs { + continue; + } + + // Most CommonJS files import no async file: skip their statements. + if !import_records[id].as_slice().iter().any(|record| { + record.source_index.is_valid() + && flags + .get(record.source_index.get() as usize) + .is_some_and(|other| other.is_async_or_has_async_dependency) + }) { + continue; + } + + let parts_live = &self.graph.parts_live[id]; + for (part_index, part) in parts[id].as_slice().iter().enumerate() { + if !parts_live.is_set(part_index) { + continue; + } + for stmt in part.stmts.slice() { + // The statements that `convert_stmts_for_chunk` gives to + // `should_remove_import_export_stmt`. It prints `export * from` without an + // alias as a bare `init_x()`, so that statement needs no `await`. + let (namespace_ref, import_record_index) = match stmt.data { + bun_ast::StmtData::SImport(s) => (s.namespace_ref, s.import_record_index), + bun_ast::StmtData::SExportFrom(s) => { + (s.namespace_ref, s.import_record_index) + } + bun_ast::StmtData::SExportStar(s) if s.alias.is_some() => { + (s.namespace_ref, s.import_record_index) + } + _ => continue, + }; + let record = &import_records[id].as_slice()[import_record_index as usize]; + if let ImportStmtLowering::Init { is_async: true, .. } = self + .import_stmt_lowering( + exports_kinds[id], + exports_refs[id], + namespace_ref, + record, + ) + { + let chains = + chains.get_or_insert_with(|| self.imports_toward_top_level_await()); + self.report_commonjs_import_of_async_module(id, record, chains); + } + } + } + } + + match chains { + Some(_) => Err(BunError::BuildFailed), + None => Ok(()), + } + } + + #[cold] + fn report_commonjs_import_of_async_module( + &self, + importer: usize, + record: &ImportRecord, + chains: &[(u32, u32)], + ) { + use std::io::Write; + let sources = self.parse_graph().input_files.items_source(); + let tla_keywords = self.parse_graph().ast.items_top_level_await_keyword(); + let import_records = self.graph.ast.items_import_records(); + let importer_source = &sources[importer]; + let imported = record.source_index.get() as usize; + + // One note for each `import` on the way to the `await`, then one for the `await`. + let mut notes: Vec = Vec::new(); + let mut file = imported; + while let Some(&(next, record_index)) = chains.get(file).filter(|next| next.0 != u32::MAX) { + notes.push(Self::import_toward_top_level_await_note( + &sources[file], + &sources[next as usize], + import_records[file].as_slice()[record_index as usize].range, + )); + file = next as usize; + } + debug_assert!(tla_keywords[file].len > 0); + notes.push(Self::top_level_await_note( + &sources[file], + tla_keywords[file], + )); + + let mut hint = Vec::new(); + write!( + &mut hint, + "CommonJS code runs synchronously and cannot wait for a top-level await. Load {} with import() inside a function instead", + bun_core::fmt::quote(sources[imported].path.pretty), + ) + .expect("infallible: in-memory write"); + notes.push(Data { + text: hint.into(), + ..Default::default() + }); + + if self.graph.ast.items_module_scope()[importer].contains_direct_eval { + let mut hint = Vec::new(); + write!( + &mut hint, + "A direct eval makes a file without an \"export\" CommonJS. Add an \"export\" to {}, or call eval as (0, eval)(code)", + bun_core::fmt::quote(importer_source.path.pretty), + ) + .expect("infallible: in-memory write"); + notes.push(Data { + text: hint.into(), + ..Default::default() + }); + } + + let mut text = Vec::new(); + write!( + &mut text, + "This import is not allowed because {} is bundled as CommonJS and the {} {} contains a top-level await", + bun_core::fmt::quote(importer_source.path.pretty), + if file == imported { + "imported file" + } else { + "transitive dependency" + }, + bun_core::fmt::quote(sources[file].path.pretty), + ) + .expect("infallible: in-memory write"); + + self.log_disjoint().add_range_error_with_notes( + Some(importer_source), + record.range, + text, + notes.into_boxed_slice(), + ); + } + + /// For each file, the `import` to follow on a shortest chain of static imports to a file + /// with a top-level await: (imported file, import record). `u32::MAX` for a file with a + /// top-level await and for a file with no such chain. + #[cold] + fn imports_toward_top_level_await(&self) -> Vec<(u32, u32)> { + let tla_keywords = self.parse_graph().ast.items_top_level_await_keyword(); + let import_records = self.graph.ast.items_import_records(); + let len = import_records.len(); + + let mut importers: Vec> = vec![Vec::new(); len]; + for (importer, records) in import_records.iter().enumerate() { + for (record_index, record) in records.as_slice().iter().enumerate() { + if record.kind != ImportKind::Stmt || !record.source_index.is_valid() { + continue; + } + if let Some(importers) = importers.get_mut(record.source_index.get() as usize) { + importers.push((importer as u32, record_index as u32)); + } + } + } + + // Breadth-first from the files with a top-level await. + let mut next = vec![(u32::MAX, u32::MAX); len]; + let mut distance = vec![u32::MAX; len]; + let mut queue: Vec = Vec::new(); + for (file, keyword) in tla_keywords.iter().enumerate().take(len) { + if keyword.len > 0 { + distance[file] = 0; + queue.push(file as u32); + } + } + let mut head = 0; + while let Some(&imported) = queue.get(head) { + head += 1; + let importer_distance = distance[imported as usize] + 1; + for &(importer, record_index) in &importers[imported as usize] { + let importer = importer as usize; + if distance[importer] == u32::MAX { + distance[importer] = importer_distance; + queue.push(importer as u32); + next[importer] = (imported, record_index); + } else if distance[importer] == importer_distance && record_index < next[importer].1 + { + // Of two imports at the same distance, the first one in the file. + next[importer] = (imported, record_index); + } + } + } + next + } + + /// The note that points at the top-level await of `source`. + fn top_level_await_note(source: &Source, keyword: Range) -> Data { + use std::io::Write; + let mut text = Vec::new(); + write!( + &mut text, + "The top-level await in {} is here:", + bstr::BStr::new(source.path.pretty) + ) + .expect("infallible: in-memory write"); + Data { + text: text.into(), + location: bun_ast::Location::init_or_null(Some(source), keyword), + } + } + + /// The note that points at an `import` on the way to a top-level await. + fn import_toward_top_level_await_note( + importer: &Source, + imported: &Source, + import: Range, + ) -> Data { + use std::io::Write; + let mut text = Vec::new(); + write!( + &mut text, + "The file {} imports the file {} here:", + bstr::BStr::new(importer.path.pretty), + bstr::BStr::new(imported.path.pretty), + ) + .expect("infallible: in-memory write"); + Data { + text: text.into(), + location: bun_ast::Location::init_or_null(Some(importer), import), + } + } + pub(crate) fn print_code_for_file_in_chunk_js( &mut self, r: renamer::Renamer, diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 9e3251214021..5560c3f6d40a 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -45,6 +45,15 @@ pub(crate) fn generate_chunks_in_parallel( ) -> crate::Result> { let _trace = bun_core::perf::trace("Bundler.generateChunksInParallel"); + // SAFETY: c points to LinkerContext which is the `linker` field of BundleV2. + let has_top_level_await = unsafe { + (*LinkerContext::bundle_v2_ptr(std::ptr::from_mut::(c))) + .has_any_top_level_await_modules + }; + if !IS_DEV_SERVER && has_top_level_await { + c.validate_commonjs_imports_of_async_modules()?; + } + c.mangle_local_css(); let mut has_js_chunk = false; diff --git a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs index 06550ac429c6..950aa1df5006 100644 --- a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs +++ b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs @@ -514,6 +514,10 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( } } + // A `__commonJS` wrapper is not an async function: the link step fails the build for an + // `import` that needs an `await` in one (`validate_commonjs_imports_of_async_modules`). + debug_assert!(flags.wrap != WrapKind::Cjs || !stmts.inside_wrapper_prefix.has_async_dependency); + // Hoist all import statements before any normal statements. ES6 imports // are different than CommonJS imports. All modules imported via ES6 import // statements are evaluated before the module doing the importing is diff --git a/test/bundler/bundler_cjs.test.ts b/test/bundler/bundler_cjs.test.ts index 81a4caed037a..28179708e36f 100644 --- a/test/bundler/bundler_cjs.test.ts +++ b/test/bundler/bundler_cjs.test.ts @@ -1,4 +1,6 @@ -import { describe } from "bun:test"; +import type { BuildOutput } from "bun"; +import { describe, expect } from "bun:test"; +import { basename } from "node:path"; import { itBundled } from "./expectBundled"; // Tests for CommonJS <> ESM interop, specifically the __toESM helper behavior. @@ -609,6 +611,617 @@ describe("bundler", () => { }, }); + // ============================================================================ + // An import of a module with top-level await in a file that is printed + // inside a `__commonJS` wrapper + // + // The `import` becomes a call inside the wrapper, which is not an async + // function: it cannot wait for the module. The import is a build error. + // ============================================================================ + + const tlaFile = `await 0;\nexport const t = 1;`; + const importsTLAFile = `import { t } from "./tla.js";\nmodule.exports = { t };`; + const logDefaultT = `import c from "./c.js";\nconsole.log(c.t);`; + // Tree shaking removes this package when nothing reads its exports. + const shakeableTLAPackage = { + "/node_modules/pkg/package.json": `{ "name": "pkg", "main": "index.js", "sideEffects": false }`, + "/node_modules/pkg/index.js": tlaFile, + }; + + const tlaImportError = (importer: string, imported: string) => + `This import is not allowed because "${importer}" is bundled as CommonJS and the imported file "${imported}" contains a top-level await`; + const tlaImportHint = (imported: string) => + `CommonJS code runs synchronously and cannot wait for a top-level await. Load "${imported}" with import() inside a function instead`; + const directEvalHint = (importer: string) => + `A direct eval makes a file without an "export" CommonJS. Add an "export" to "${importer}", or call eval as (0, eval)(code)`; + // `file:line:column` of a log or of a note + const logPosition = (position: { file: string; line: number; column: number } | null) => + position && `${basename(position.file)}:${position.line}:${position.column}`; + const logsOf = (build: BuildOutput) => + build.logs.map((log: any) => ({ + level: log.level, + message: log.message, + at: logPosition(log.position), + notes: log.notes.map((note: any) => ({ message: note.message, at: logPosition(note.position) })), + })); + // The error for an import of "tla.js" at `line:column` of `importer` + const tlaImportLog = (importer: string, at: string, tla = "tla.js") => ({ + level: "error", + message: tlaImportError(importer, tla), + at: `${importer}:${at}`, + notes: [ + { message: `The top-level await in ${tla} is here:`, at: `${tla}:1:1` }, + { message: tlaImportHint(tla), at: null }, + ], + }); + + itBundled("cjs/tla_import_module_exports", { + files: { + "/entry.js": logDefaultT, + "/c.js": importsTLAFile, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/c.js": [ + 'This import is not allowed because "c.js" is bundled as CommonJS and the imported file "tla.js" contains a top-level await', + ], + }, + onAfterApiBundle(build) { + expect(build.success).toBe(false); + expect(logsOf(build)).toEqual([ + { + level: "error", + message: + 'This import is not allowed because "c.js" is bundled as CommonJS and the imported file "tla.js" contains a top-level await', + at: "c.js:1:19", + notes: [ + { message: "The top-level await in tla.js is here:", at: "tla.js:1:1" }, + { + message: + 'CommonJS code runs synchronously and cannot wait for a top-level await. Load "tla.js" with import() inside a function instead', + at: null, + }, + ], + }, + ]); + }, + }); + + // `bun build`, with the source map tasks that a failed link has to wait for + itBundled("cjs/tla_import_module_exports_cli", { + backend: "cli", + sourceMap: "inline", + files: { + "/entry.js": logDefaultT, + "/c.js": importsTLAFile, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "tla.js")], + }, + }); + + itBundled("cjs/tla_import_module_exports_compile", { + compile: true, + files: { + "/entry.js": logDefaultT, + "/c.js": importsTLAFile, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "tla.js")], + }, + }); + + // The build fails as a whole: the file of `b.js` would not parse. + itBundled("cjs/tla_import_one_of_two_entry_points", { + files: { + "/a.js": `console.log("a");`, + "/b.js": logDefaultT, + "/c.js": importsTLAFile, + "/tla.js": tlaFile, + }, + entryPoints: ["/a.js", "/b.js"], + outdir: "/out", + bundleErrors: { + "/c.js": [tlaImportError("c.js", "tla.js")], + }, + onAfterApiBundle(build) { + expect(build.outputs).toEqual([]); + expect(logsOf(build)).toEqual([tlaImportLog("c.js", "1:19")]); + }, + }); + + // `exports.t = t` alone is lifted to `export { t }`. A default import that + // is held as a value brings the wrapper back. + itBundled("cjs/tla_import_exports_with_held_default_import", { + files: { + "/entry.js": logDefaultT, + "/c.js": `import { t } from "./tla.js";\nexports.t = t;`, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("c.js", "1:19")]); + }, + }); + + itBundled("cjs/tla_import_entry_point", { + files: { + "/entry.js": importsTLAFile, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/entry.js": [tlaImportError("entry.js", "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("entry.js", "1:19")]); + }, + }); + + for (const splitting of [false, true]) { + itBundled(`cjs/tla_import_file_loaded_with_import_call${splitting ? "_splitting" : ""}`, { + files: { + "/entry.js": `const c = await import("./c.js");\nconsole.log(c.default.t);`, + "/c.js": importsTLAFile, + "/tla.js": tlaFile, + }, + splitting, + outdir: "/out", + bundleErrors: { + "/c.js": [tlaImportError("c.js", "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("c.js", "1:19")]); + }, + }); + } + + // One note for each `import` on the way to the `await`. An `import()` is not + // on the way: the chain of `mid.js` goes through `mid2.js`, not `near.js`. + itBundled("cjs/tla_import_transitive", { + files: { + "/entry.js": logDefaultT, + "/c.js": `import { t } from "./mid.js";\nmodule.exports = { t };`, + "/mid.js": `export { t } from "./mid2.js";\nexport const near = () => import("./near.js");`, + "/mid2.js": `export { t } from "./tla.js";`, + "/near.js": `await 0;\nexport const n = 1;`, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/c.js": [ + 'This import is not allowed because "c.js" is bundled as CommonJS and the transitive dependency "tla.js" contains a top-level await', + ], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([ + { + level: "error", + message: + 'This import is not allowed because "c.js" is bundled as CommonJS and the transitive dependency "tla.js" contains a top-level await', + at: "c.js:1:19", + notes: [ + { message: "The file mid.js imports the file mid2.js here:", at: "mid.js:1:19" }, + { message: "The file mid2.js imports the file tla.js here:", at: "mid2.js:1:19" }, + { message: "The top-level await in tla.js is here:", at: "tla.js:1:1" }, + { message: tlaImportHint("mid.js"), at: null }, + ], + }, + ]); + }, + }); + + // Of two imports at the same distance from an `await`, the notes take the first. + itBundled("cjs/tla_import_transitive_two_awaits", { + files: { + "/entry.js": logDefaultT, + "/c.js": `import { t } from "./mid.js";\nmodule.exports = { t };`, + "/mid.js": `export { t } from "./tla.js";\nimport "./tla2.js";`, + "/tla.js": tlaFile, + "/tla2.js": `await 0;`, + }, + bundleErrors: { + "/c.js": [ + 'This import is not allowed because "c.js" is bundled as CommonJS and the transitive dependency "tla.js" contains a top-level await', + ], + }, + onAfterApiBundle(build) { + expect(logsOf(build)[0].notes).toEqual([ + { message: "The file mid.js imports the file tla.js here:", at: "mid.js:1:19" }, + { message: "The top-level await in tla.js is here:", at: "tla.js:1:1" }, + { message: tlaImportHint("mid.js"), at: null }, + ]); + }, + }); + + itBundled("cjs/tla_import_bare", { + files: { + "/entry.js": logDefaultT, + "/c.js": `import "./tla.js";\nmodule.exports = { t: 2 };`, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("c.js", "1:8")]); + }, + }); + + itBundled("cjs/tla_import_cjs_extension", { + files: { + "/entry.js": `import c from "./c.cjs";\nconsole.log(c.t);`, + "/c.cjs": importsTLAFile, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/c.cjs": [tlaImportError("c.cjs", "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("c.cjs", "1:19")]); + }, + }); + + // The CommonJS file still runs when nothing reads its exports. + itBundled("cjs/tla_import_unused_default_import", { + files: { + "/entry.js": `import c from "./c.js";\nconsole.log("entry");`, + "/c.js": importsTLAFile, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("c.js", "1:19")]); + }, + }); + + // One error for each import of a module with top-level await, and none for `plain.js`. + itBundled("cjs/tla_import_two_modules", { + files: { + "/entry.js": `import c from "./c.js";\nconsole.log(c.t, c.u, c.p);`, + "/c.js": `import { t } from "./tla.js";\nimport { p } from "./plain.js";\nimport { u } from "./tla2.js";\nmodule.exports = { t, u, p };`, + "/plain.js": `console.log("plain");\nexport const p = 3;`, + "/tla.js": tlaFile, + "/tla2.js": `await 0;\nexport const u = 2;`, + }, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "tla.js"), tlaImportError("c.js", "tla2.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("c.js", "1:19"), tlaImportLog("c.js", "3:19", "tla2.js")]); + }, + }); + + // `Object.keys(exports)` keeps these two files CommonJS with their `export`: + // the bundler drops the `export` and keeps the import of the file. + itBundled("cjs/tla_import_export_from", { + files: { + "/entry.js": `import c from "./c.js";\nconsole.log(c.a);`, + "/c.js": `export { t } from "./tla.js";\nexports.a = 1;\nObject.keys(exports);`, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("c.js", "1:19")]); + }, + }); + + itBundled("cjs/tla_import_export_star_as", { + files: { + "/entry.js": `import c from "./c.js";\nconsole.log(c.a);`, + "/c.js": `export * as ns from "./tla.js";\nexports.a = 1;\nObject.keys(exports);`, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("c.js", "1:21")]); + }, + }); + + // `require("react")` is unwrapped to an `import` of the package. + itBundled("cjs/tla_import_unwrapped_require", { + files: { + "/entry.js": `import c from "./c.js";\nconsole.log(c.v);`, + "/c.js": `const React = require("react");\nmodule.exports = { v: React.version };`, + "/node_modules/react/package.json": `{ "name": "react", "version": "19.0.0", "main": "index.js", "type": "module" }`, + "/node_modules/react/index.js": `await 0;\nexport const version = "19";`, + }, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "node_modules/react/index.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([ + { + level: "error", + message: tlaImportError("c.js", "node_modules/react/index.js"), + at: "c.js:1:23", + notes: [ + { message: "The top-level await in node_modules/react/index.js is here:", at: "index.js:1:1" }, + { message: tlaImportHint("node_modules/react/index.js"), at: null }, + ], + }, + ]); + }, + }); + + // Files that are CommonJS for the bundler without `module.exports` or `exports.x` + + // A direct eval can read `module` and `exports`. + for (const importer of ["c.js", "entry.js"]) { + itBundled(`cjs/tla_import_direct_eval${importer === "entry.js" ? "_entry_point" : ""}`, { + files: { + "/entry.js": `import "./c.js";`, + [`/${importer}`]: `import { t } from "./tla.js";\nconsole.log(eval("t"));`, + "/tla.js": tlaFile, + }, + bundleErrors: { + [`/${importer}`]: [tlaImportError(importer, "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([ + { + level: "error", + message: tlaImportError(importer, "tla.js"), + at: `${importer}:1:19`, + notes: [ + { message: "The top-level await in tla.js is here:", at: "tla.js:1:1" }, + { message: tlaImportHint("tla.js"), at: null }, + { message: directEvalHint(importer), at: null }, + ], + }, + ]); + }, + }); + } + + // The two ways out that the note of the direct eval names + itBundled("cjs/tla_import_direct_eval_with_export", { + files: { + "/entry.js": `import "./c.js";`, + "/c.js": `import { t } from "./tla.js";\nconsole.log(eval("1 + 1"), t);\nexport {};`, + "/tla.js": tlaFile, + }, + run: { + stdout: "2 1", + }, + }); + + itBundled("cjs/tla_import_indirect_eval", { + files: { + "/entry.js": `import "./c.js";`, + "/c.js": `import { t } from "./tla.js";\nconsole.log((0, eval)("1 + 1"), t);`, + "/tla.js": tlaFile, + }, + run: { + stdout: "2 1", + }, + }); + + // A reference to `module` is enough, also in a `.mjs` file. + itBundled("cjs/tla_import_typeof_module_in_mjs", { + files: { + "/entry.mjs": `import { t } from "./tla.js";\nconsole.log(t, typeof module);`, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/entry.mjs": [tlaImportError("entry.mjs", "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("entry.mjs", "1:19")]); + }, + }); + + itBundled("cjs/tla_import_module_hot", { + files: { + "/entry.js": `import { t } from "./tla.js";\nif (module.hot) module.hot.accept();\nconsole.log(t);`, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/entry.js": [tlaImportError("entry.js", "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("entry.js", "1:19")]); + }, + }); + + itBundled("cjs/tla_import_ts_export_equals", { + files: { + "/entry.js": `import c from "./c.ts";\nconsole.log(c.t);`, + "/c.ts": `import { t } from "./tla.js";\nexport = { t };`, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/c.ts": [tlaImportError("c.ts", "tla.js")], + }, + onAfterApiBundle(build) { + expect(logsOf(build)).toEqual([tlaImportLog("c.ts", "1:19")]); + }, + }); + + // Tree shaking decides whether the import is in the bundle. + + // The package stays in the bundle when its export is read. + itBundled("cjs/tla_import_used_export_of_shakeable_package", { + files: { + "/entry.js": logDefaultT, + "/c.js": `import { t } from "pkg";\nmodule.exports = { t };`, + ...shakeableTLAPackage, + }, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "node_modules/pkg/index.js")], + }, + }); + + // The export is not read: the import is not in the bundle. `plain.js` is, and runs. + const unusedExportOfShakeablePackage = { + "/entry.js": logDefaultT, + "/c.js": `import { t } from "pkg";\nimport "./plain.js";\nmodule.exports = { t: 5 };`, + "/plain.js": `console.log("plain");`, + ...shakeableTLAPackage, + }; + itBundled("cjs/tla_import_unused_export_of_shakeable_package", { + files: unusedExportOfShakeablePackage, + run: { + stdout: "plain\n5", + }, + }); + + // The two options that keep the package in the bundle + itBundled("cjs/tla_import_unused_export_of_shakeable_package_ignore_dce_annotations", { + files: unusedExportOfShakeablePackage, + ignoreDCEAnnotations: true, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "node_modules/pkg/index.js")], + }, + }); + + itBundled("cjs/tla_import_unused_export_of_shakeable_package_no_tree_shaking", { + files: unusedExportOfShakeablePackage, + backend: "api", + treeShaking: false, + bundleErrors: { + "/c.js": [tlaImportError("c.js", "node_modules/pkg/index.js")], + }, + }); + + // The package is in the bundle for `entry.js`. The import in `c.js` is still unused. + itBundled("cjs/tla_import_unused_export_of_package_used_elsewhere", { + files: { + "/entry.js": `import c from "./c.js";\nimport { t } from "pkg";\nconsole.log(c.v, t);`, + "/c.js": `import { t } from "pkg";\nmodule.exports = { v: 5 };`, + ...shakeableTLAPackage, + }, + run: { + stdout: "5 1", + }, + }); + + // `tla.ts` is in the bundle for `entry.js`. The import in `c.ts` names a type only. + itBundled("cjs/tla_import_type_only", { + files: { + "/entry.js": `import c from "./c.ts";\nimport { t } from "./tla.ts";\nconsole.log(c.t, t);`, + "/c.ts": `import { T } from "./tla.ts";\nconst x: T = 5;\nmodule.exports = { t: x };`, + "/tla.ts": `await 0;\nexport const t = 1;\nexport type T = number;`, + }, + run: { + stdout: "5 1", + }, + }); + + // The CommonJS file is not in the bundle: nothing reads `c`. + const unusedFileOfShakeablePackage = { + "/entry.js": `import { a } from "pkg";\nconsole.log(a);`, + "/node_modules/pkg/package.json": `{ "name": "pkg", "main": "index.js", "sideEffects": false }`, + "/node_modules/pkg/index.js": `export const a = "a";\nexport { default as c } from "./c.js";`, + "/node_modules/pkg/c.js": importsTLAFile, + "/node_modules/pkg/tla.js": tlaFile, + }; + itBundled("cjs/tla_import_in_unused_file_of_shakeable_package", { + files: unusedFileOfShakeablePackage, + run: { + stdout: "a", + }, + }); + + itBundled("cjs/tla_import_in_unused_file_of_shakeable_package_ignore_dce_annotations", { + files: unusedFileOfShakeablePackage, + ignoreDCEAnnotations: true, + bundleErrors: { + "/node_modules/pkg/c.js": [tlaImportError("node_modules/pkg/c.js", "node_modules/pkg/tla.js")], + }, + }); + + // The next tests build: no `await` is inside a `__commonJS` wrapper. + + // Every importer reads `exports.t` by name: the file stays lifted to an ES module. + itBundled("cjs/tla_import_lifted_exports_file", { + files: { + "/entry.js": `import { t } from "./c.js";\nconsole.log(t);`, + "/c.js": `import { t } from "./tla.js";\nexports.t = t;`, + "/tla.js": tlaFile, + }, + run: { + stdout: "1", + }, + }); + + itBundled("cjs/tla_import_lifted_exports_file_splitting", { + files: { + "/entry.js": `const c = await import("./c.js");\nconsole.log(c.t);`, + "/c.js": `import { t } from "./tla.js";\nexports.t = t;`, + "/tla.js": tlaFile, + }, + splitting: true, + outdir: "/out", + run: { + file: "/out/entry.js", + stdout: "1", + }, + }); + + // `export *` without an alias calls the wrapper of "tla.js" and does not wait for it. + // When it waits, it needs the same error. + itBundled("cjs/tla_import_export_star", { + files: { + "/entry.js": `import c from "./c.js";\nconsole.log(c.a);`, + "/c.js": `export * from "./tla.js";\nexports.a = 1;\nObject.keys(exports);`, + "/tla.js": tlaFile, + }, + run: { + stdout: "1", + }, + }); + + itBundled("cjs/tla_import_of_sync_file_beside_tla_file", { + files: { + "/entry.js": `import { t } from "./tla.js";\nimport c from "./c.js";\nconsole.log(t, c.p);`, + "/c.js": `import { p } from "./plain.js";\nmodule.exports = { p };`, + "/plain.js": `export const p = 2;`, + "/tla.js": tlaFile, + }, + run: { + stdout: "1 2", + }, + }); + + // This format has no `__commonJS` wrapper. + itBundled("cjs/tla_import_internal_bake_dev_format", { + format: "internal_bake_dev", + files: { + "/entry.js": logDefaultT, + "/c.js": importsTLAFile, + "/tla.js": tlaFile, + }, + onAfterBundle(api) { + expect(api.readFile("/out.js")).toContain('"c.js"(hmr, module, exports) {'); + }, + }); + + // The `require()` error is reported first, and alone. + itBundled("cjs/tla_import_file_loaded_with_require", { + files: { + "/entry.js": `const c = require("./c.js");\nconsole.log(c.t);`, + "/c.js": importsTLAFile, + "/tla.js": tlaFile, + }, + bundleErrors: { + "/entry.js": [ + 'This require call is not allowed because the transitive dependency "tla.js" contains a top-level await', + ], + }, + onAfterApiBundle(build) { + expect(build.logs.length).toBe(1); + }, + }); + // ============================================================================ // isNodeMode follows the importer's module type: the file extension first, // then the nearest package.json "type". One test per importer kind.