diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index 2248a6632596..65832093e683 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -2604,6 +2604,17 @@ pub mod parse_worker { opts.lower_import_meta_main_for_node_js = true; } + // `import.meta` is a syntax error outside of an ES module: cjs output + // never is one, and iife output is a script except on bun, which loads + // `// @bun` output as a module. The runtime's `__require` definitions + // need the real `import.meta`, so the runtime is excluded. + opts.lower_import_meta = !task.source_index.is_runtime() + && match output_format { + options::Format::Cjs => true, + options::Format::Iife => !target.is_bun(), + options::Format::Esm | options::Format::InternalBakeDev => false, + }; + opts.tree_shaking = if task.source_index.is_runtime() { true } else { diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index d9a670faaaf4..529403c62f8f 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1585,6 +1585,7 @@ impl<'a> Transpiler<'a> { transform_only: self.options.transform_only, import_meta_main_value: None, lower_import_meta_main_for_node_js: false, + lower_import_meta: false, framework: None, repl_mode: self.options.repl_mode, lower_toml_datetimes: false, diff --git a/src/js_parser/fold.rs b/src/js_parser/fold.rs index 74125b583e17..acace95d2c3b 100644 --- a/src/js_parser/fold.rs +++ b/src/js_parser/fold.rs @@ -3,7 +3,7 @@ use bun_collections::VecExt; use bun_core::feature_flags as FeatureFlags; use crate::p::P; -use crate::parser::{self as js_parser, IdentifierOpts, RelocateVars, RelocateVarsMode}; +use crate::parser::{IdentifierOpts, RelocateVars, RelocateVarsMode}; use bun_ast::ast_result::CommonJSNamedExport; use bun_ast::{self as js_ast, Binding, E, Expr, Flags, G, LocRef, S}; @@ -130,6 +130,26 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut sw_data = target.data; 'sw: loop { match sw_data { + js_ast::ExprData::EImportMeta(_) => { + return p.maybe_rewrite_import_meta_property( + target, + name, + name_loc, + loc, + identifier_opts, + ); + } + // `options.lower_import_meta` already replaced `import.meta` with + // the `import_meta` stand-in when the target was visited. + js_ast::ExprData::EIdentifier(id) if p.is_import_meta_stand_in(id.ref_) => { + return p.maybe_rewrite_import_meta_property( + target, + name, + name_loc, + loc, + identifier_opts, + ); + } js_ast::ExprData::EIdentifier(id) => { // Rewrite property accesses on explicit namespace imports as an identifier. // This lets us replace them easily in the printer to rebind them to @@ -492,64 +512,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } } - js_ast::ExprData::EImportMeta(_) => { - if name == b"main" { - return Some(p.value_for_import_meta_main(false, target.loc)); - } - - if name == b"hot" { - return Some(Expr { - data: js_ast::ExprData::ESpecial( - if p.options.features.hot_module_reloading { - E::Special::HotEnabled - } else { - E::Special::HotDisabled - }, - ), - loc, - }); - } - - // Inline import.meta properties for Bake - if p.options.framework.is_some() - || (p.options.bundle - && p.options.output_format == js_parser::options::Format::Cjs) - { - if name == b"dir" || name == b"dirname" { - // Inline import.meta.dir - return Some( - p.new_expr(e_string_init(p.source.path.name().dir), name_loc), - ); - } else if name == b"file" { - // Inline import.meta.file (filename only) - return Some( - p.new_expr(e_string_init(p.source.path.name().filename), name_loc), - ); - } else if name == b"path" { - // Inline import.meta.path (full path) - return Some(p.new_expr(e_string_init(p.source.path.text), name_loc)); - } else if name == b"url" { - // Inline import.meta.url as file:// URL - let bunstr = bun_core::String::from_bytes(p.source.path.text); - let url = p.arena.alloc_slice_copy( - format!("{}", bun_url::file_url_from_string(&bunstr)).as_bytes(), - ); - return Some(p.new_expr(e_string_init(url), name_loc)); - } - } - - // Make all property accesses on `import.meta.url` side effect free. - return Some(p.new_expr( - E::Dot { - target, - name: name_static, - name_loc, - can_be_removed_if_unused: true, - ..Default::default() - }, - target.loc, - )); - } js_ast::ExprData::ERequireCallTarget => { if name == b"main" { return Some(Expr { @@ -747,6 +709,81 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O None } + /// `target` is `import.meta`, or the `import_meta` stand-in it was + /// rewritten to under `options.lower_import_meta`. + fn maybe_rewrite_import_meta_property( + &mut self, + target: Expr, + name: &'a [u8], + name_loc: bun_ast::Loc, + loc: bun_ast::Loc, + identifier_opts: IdentifierOpts, + ) -> Option { + let p = self; + + // `import.meta.url = x` / `delete import.meta.url` must stay a property + // access: inlining the value would print `"file:///..." = x`. + if identifier_opts.assign_target() != js_ast::AssignTarget::None + || identifier_opts.is_delete_target() + { + return None; + } + + let inlined: Option = if name == b"main" { + Some(p.value_for_import_meta_main(false, target.loc)) + } else if name == b"hot" { + Some(Expr { + data: js_ast::ExprData::ESpecial(if p.options.features.hot_module_reloading { + E::Special::HotEnabled + } else { + E::Special::HotDisabled + }), + loc, + }) + } else if p.options.framework.is_some() || p.options.lower_import_meta { + // Bake serves its own `import.meta` and non-module output has none, + // so properties with a bundle-time-known value are inlined. + match name { + b"dir" | b"dirname" => { + Some(p.new_expr(e_string_init(p.source.path.name().dir), name_loc)) + } + b"file" => Some(p.new_expr(e_string_init(p.source.path.name().filename), name_loc)), + b"path" | b"filename" => { + Some(p.new_expr(e_string_init(p.source.path.text), name_loc)) + } + b"url" => { + let bunstr = bun_core::String::from_bytes(p.source.path.text); + let url = p.arena.alloc_slice_copy( + format!("{}", bun_url::file_url_from_string(&bunstr)).as_bytes(), + ); + Some(p.new_expr(e_string_init(url), name_loc)) + } + _ => None, + } + } else { + None + }; + + if let Some(inlined) = inlined { + if matches!(target.data, js_ast::ExprData::EIdentifier(_)) { + p.ignore_usage_of_import_meta(&target); + } + return Some(inlined); + } + + // Make all property accesses on `import.meta` side effect free. + Some(p.new_expr( + E::Dot { + target, + name: E::Str::new(name), + name_loc, + can_be_removed_if_unused: true, + ..Default::default() + }, + target.loc, + )) + } + fn maybe_rewrite_property_access_for_namespace( &mut self, name: &'a [u8], diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 285111d1dc80..e0b44f45799e 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -237,7 +237,14 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { pub(crate) module_ref: Ref, pub(crate) filename_ref: Ref, pub(crate) dirname_ref: Ref, + /// Stand-in for `import.meta` in non-module output: the `$Bun_import_meta` + /// wrapper parameter for runtime CommonJS modules, or a per-file + /// `var import_meta = {}` under `options.lower_import_meta`. pub(crate) import_meta_ref: Ref, + /// `lower_import_meta` only: locations rewritten to the empty-object + /// stand-in, warned about after the visit pass. Inlined property accesses + /// remove their entry again (`ignore_usage_of_import_meta`). + pub(crate) empty_import_meta_locs: List<'a, bun_ast::Loc>, pub(crate) hmr_api_ref: Ref, /// If bake is enabled and this is a server-side file, we want to use @@ -5254,6 +5261,50 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } + /// `options.lower_import_meta`: replaces `import.meta` with a reference to + /// this file's `var import_meta = {}`, declared after the visit pass if a + /// reference survives `maybe_rewrite_import_meta_property`. + pub(crate) fn value_for_import_meta(&mut self, loc: bun_ast::Loc) -> Expr { + debug_assert!(self.options.lower_import_meta); + if self.import_meta_ref.is_empty() { + self.import_meta_ref = + self.declare_generated_symbol(js_ast::symbol::Kind::Other, b"import_meta"); + } + let ref_ = self.import_meta_ref; + self.record_usage(ref_); + // As with unresolvable dynamic imports, `import.meta` inside a `try` or + // in node_modules is not worth a warning the user cannot act on. + if !self.is_control_flow_dead + && self.fn_or_arrow_data_visit.try_body_count == 0 + && !self.source.path.is_node_module() + { + self.empty_import_meta_locs.push(loc); + } + self.new_expr(E::Identifier::init(ref_), loc) + } + + /// Whether `ref_` is the stand-in from [`Self::value_for_import_meta`]. + #[inline] + pub(crate) fn is_import_meta_stand_in(&self, ref_: Ref) -> bool { + !self.import_meta_ref.is_empty() && ref_.eql(self.import_meta_ref) + } + + /// Undoes [`Self::value_for_import_meta`] for `target` after its property + /// access was inlined: drops the usage and the pending warning. + pub(crate) fn ignore_usage_of_import_meta(&mut self, target: &Expr) { + debug_assert!( + matches!(target.data, js_ast::ExprData::EIdentifier(id) if self.is_import_meta_stand_in(id.ref_)) + ); + self.ignore_usage(self.import_meta_ref); + // rposition: the entry was usually pushed right before this call; the + // rotate keeps the rest in source order for the warnings. + let locs = self.empty_import_meta_locs.as_mut_slice(); + if let Some(i) = locs.iter().rposition(|loc| *loc == target.loc) { + locs[i..].rotate_left(1); + self.empty_import_meta_locs.pop(); + } + } + pub(crate) fn keep_expr_symbol_name(&mut self, _value: Expr, _name: &[u8]) -> Expr { _value } @@ -8657,6 +8708,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O filename_ref: Ref::NONE, dirname_ref: Ref::NONE, import_meta_ref: Ref::NONE, + empty_import_meta_locs: BumpVec::new_in(arena), hmr_api_ref: Ref::NONE, response_ref: Ref::NONE, bun_app_namespace_ref: Ref::NONE, diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 776c25011ac7..8cdc0693712d 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -94,6 +94,11 @@ pub struct Options<'a> { pub import_meta_main_value: Option, pub lower_import_meta_main_for_node_js: bool, + /// The output format is not an ES module, so `import.meta` is inlined + /// where its value is known and otherwise rewritten to a per-file + /// `var import_meta = {}` (`P::value_for_import_meta`). Bundler only. + pub lower_import_meta: bool, + /// When using react fast refresh or server components, the framework is /// able to customize what import sources are used. pub framework: Option<&'a options::Framework>, // TYPE_ONLY: was bun_runtime::bake::Framework @@ -136,6 +141,7 @@ impl<'a> Default for Options<'a> { transform_only: false, import_meta_main_value: None, lower_import_meta_main_for_node_js: false, + lower_import_meta: false, framework: None, repl_mode: false, lower_toml_datetimes: false, @@ -220,6 +226,7 @@ impl<'a> Options<'a> { transform_only: self.transform_only, import_meta_main_value: self.import_meta_main_value, lower_import_meta_main_for_node_js: self.lower_import_meta_main_for_node_js, + lower_import_meta: self.lower_import_meta, framework: self.framework, repl_mode: self.repl_mode, lower_toml_datetimes: self.lower_toml_datetimes, @@ -292,6 +299,7 @@ impl<'a> Options<'a> { transform_only: false, import_meta_main_value: None, lower_import_meta_main_for_node_js: false, + lower_import_meta: false, framework: None, repl_mode: false, lower_toml_datetimes: loader == options::Loader::Toml, @@ -1205,6 +1213,68 @@ impl<'a> Parser<'a> { } } + // `options.lower_import_meta`: declare the `var import_meta = {}` that + // non-inlined `import.meta` references point at, as a part that tree + // shaking can remove. + if p.options.lower_import_meta + && !p.import_meta_ref.is_empty() + && p.symbols.as_slice()[p.import_meta_ref.inner_index() as usize].use_count_estimate > 0 + { + let import_meta_ref = p.import_meta_ref; + let binding = p.b( + B::Identifier { + r#ref: import_meta_ref, + }, + bun_ast::Loc::EMPTY, + ); + let empty_object = p.new_expr(E::Object::default(), bun_ast::Loc::EMPTY); + let part_stmts = p.arena.alloc_slice_fill_with(1, |_| { + p.s( + S::Local { + kind: js_ast::LocalKind::KVar, + decls: G::DeclList::init_one(G::Decl { + binding, + value: Some(empty_object), + }), + ..Default::default() + }, + bun_ast::Loc::EMPTY, + ) + }); + let mut declared_symbols = + bun_ast::DeclaredSymbolList::init_capacity(1).expect("unreachable"); + declared_symbols.append_assume_capacity(DeclaredSymbol { + ref_: import_meta_ref, + is_top_level: true, + }); + before.push(js_ast::Part { + stmts: part_stmts.into(), + declared_symbols, + can_be_removed_if_unused: true, + ..Default::default() + }); + + let format_name = p.options.output_format.name(); + for i in 0..p.empty_import_meta_locs.len() { + let loc = p.empty_import_meta_locs[i]; + // Cover both tokens of `import.meta` when spelled normally. + let range = match p.source.contents.get(loc.to_usize()..) { + Some(rest) if rest.starts_with(b"import.meta") => bun_ast::Range { + loc, + len: "import.meta".len() as i32, + }, + _ => js_lexer::range_of_identifier(p.source, loc), + }; + p.log().add_range_warning_fmt( + Some(p.source), + range, + format_args!( + "\"import.meta\" is not available with the \"{format_name}\" output format and will be empty" + ), + ); + } + } + // This is a workaround for broken module environment checks in packages like lodash-es // https://github.com/lodash/lodash/issues/5660 let mut force_esm = false; diff --git a/src/js_parser/parser.rs b/src/js_parser/parser.rs index 20db6ddf9d31..1dc1b20b340d 100644 --- a/src/js_parser/parser.rs +++ b/src/js_parser/parser.rs @@ -45,6 +45,16 @@ pub mod options { pub(crate) const fn is_esm(self) -> bool { matches!(self, Format::Esm) } + + /// The `--format` spelling, for diagnostics. + pub(crate) const fn name(self) -> &'static str { + match self { + Format::Esm => "esm", + Format::Iife => "iife", + Format::Cjs => "cjs", + Format::InternalBakeDev => "internal_bake_dev", + } + } } /// Canonical home is here (the parser is the consumer /// — `P::should_allow_unresolved_dynamic_specifier`). `bun_bundler::options` diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs index c3aa10f99ae6..a7862196e391 100644 --- a/src/js_parser/visit/visit_expr.rs +++ b/src/js_parser/visit/visit_expr.rs @@ -158,6 +158,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O return; } } + + // The parent EDot/EIndex inlines known properties of this replacement + // via `maybe_rewrite_import_meta_property`. + if p.options.lower_import_meta { + *e = p.value_for_import_meta(expr.loc); + } } fn e_identifier(p: &mut Self, e: &mut Expr, in_: ExprIn) { diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index c9e970fa08be..ce71a0885e3e 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe, isBroken, isWindows, tempDir } from "harness"; import { readdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; +import { pathToFileURL } from "node:url"; import { decodeSourceMappingsLine, itBundled } from "./expectBundled"; // A public path composes with the referenced file's path relative to the output @@ -3223,6 +3224,326 @@ describe("bundler", () => { api.expectFile("/out.js").toContain("var arguments = 1;"); }, }); + + // `import.meta` is a syntax error outside of an ES module, which is what cjs + // output is (node rejects it; bun evaluates the `@bun-cjs` wrapper as a + // script, and `--bytecode` compiles that wrapper). The properties with a + // bundle-time value are inlined, everything else is rewritten to a per-file + // `var import_meta = {}`. The outputs run as `.cjs` under node on purpose: + // node would re-parse a `.js` file as ESM and bun accepts `import.meta` + // anywhere, so only that combination observes the syntax error. + const importMetaCjsFiles = { + "/entry.js": /* js */ ` + import { depMeta, depUrl } from "./dep.js"; + import cjsDep from "./cjs-dep.cjs"; + const meta = import.meta; + console.log(typeof meta, JSON.stringify(meta), meta === depMeta, depMeta === cjsDep.meta); + console.log(import.meta.env?.MODE, typeof import.meta.resolve, typeof import.meta.require); + console.log( + import.meta.url.startsWith("file:///"), + import.meta.url.endsWith("/entry.js"), + depUrl.endsWith("/dep.js"), + cjsDep.url.endsWith("/cjs-dep.cjs"), + ); + console.log(import.meta.file, import.meta.path.endsWith("entry.js"), import.meta.filename === import.meta.path); + console.log(import.meta.dirname === import.meta.dir, import.meta.path.startsWith(import.meta.dir)); + `, + "/dep.js": /* js */ ` + export const depMeta = import.meta; + export const depUrl = import.meta.url; + `, + "/cjs-dep.cjs": /* js */ ` + module.exports = { meta: import.meta, url: import.meta.url }; + `, + }; + const importMetaCjsStdout = ` + object {} false false + undefined undefined undefined + true true true true + entry.js true true + true true + `; + itBundled("edgecase/ImportMetaCjsBecomesEmptyObject", { + files: importMetaCjsFiles, + format: "cjs", + target: "node", + outfile: "out.cjs", + onAfterBundle(api) { + const code = api.readFile("out.cjs"); + expect(code).not.toContain("import.meta"); + // One object per file that still references it. + expect(code.match(/var import_meta\w* = \{\}/g)).toHaveLength(3); + }, + run: [{ runtime: "node", stdout: importMetaCjsStdout }, { stdout: importMetaCjsStdout }], + }); + itBundled("edgecase/ImportMetaCjsBecomesEmptyObjectMinified", { + files: importMetaCjsFiles, + format: "cjs", + target: "node", + outfile: "out.cjs", + minifyIdentifiers: true, + minifySyntax: true, + minifyWhitespace: true, + onAfterBundle(api) { + api.expectFile("out.cjs").not.toContain("import.meta"); + }, + run: { runtime: "node", stdout: importMetaCjsStdout }, + }); + // `--bytecode` defaults to cjs output and compiles the `@bun-cjs` wrapper, so + // a left over `import.meta` made JSC refuse to generate the bytecode + // ("Failed to generate bytecode") and then fail to load the output. + itBundled("edgecase/ImportMetaCjsBytecode", { + files: importMetaCjsFiles, + format: "cjs", + target: "bun", + bytecode: true, + outdir: "/out", + onAfterBundle(api) { + api.expectFile("/out/entry.js").toContain("// @bun @bytecode @bun-cjs"); + api.expectFile("/out/entry.js").not.toContain("import.meta"); + api.assertFileExists("/out/entry.js.jsc"); + }, + run: { stdout: importMetaCjsStdout }, + }); + // The inlined values are those of the source file being parsed, like + // `__dirname`: a file in a subdirectory gets its own directory, and `url` is + // the source file's URL rather than the bundle's. + const importMetaPathsFiles = { + "/entry.js": /* js */ ` + import { dep } from "./lib/dep.js"; + console.log( + JSON.stringify([ + import.meta.dir, + import.meta.dirname, + import.meta.file, + import.meta.path, + import.meta.filename, + import.meta.url, + dep, + ]), + ); + `, + "/lib/dep.js": /* js */ ` + export const dep = [import.meta.dir, import.meta.file, import.meta.path, import.meta.url]; + `, + }; + const importMetaPathsStdout = (root: string) => + JSON.stringify([ + root, + root, + "entry.js", + join(root, "entry.js"), + join(root, "entry.js"), + pathToFileURL(join(root, "entry.js")).href, + [join(root, "lib"), "dep.js", join(root, "lib", "dep.js"), pathToFileURL(join(root, "lib", "dep.js")).href], + ]); + itBundled("edgecase/ImportMetaCjsPathsAreInlinedPerFile", ({ root }) => ({ + files: importMetaPathsFiles, + format: "cjs", + target: "node", + outfile: "out.cjs", + onAfterBundle(api) { + api.expectFile("out.cjs").not.toContain("import.meta"); + api.expectFile("out.cjs").not.toContain("import_meta"); + }, + run: { runtime: "node", stdout: importMetaPathsStdout(root) }, + })); + itBundled("edgecase/ImportMetaCjsExpressionShapes", { + files: { + "/entry.js": /* js */ ` + var import_meta = "declared by the user"; + console.log(import_meta, typeof import.meta, import.meta?.env, import.meta["file"], typeof import.meta.hot); + console.log(delete import.meta, [import.meta].length, (0, import.meta).nothing); + // Assignment targets are not inlined: the writes land on the object. + const meta = import.meta; + import.meta.url = "assigned"; + import.meta.file++; + console.log(meta.url, meta.file, import.meta.url.startsWith("file:///"), import.meta.file); + `, + }, + format: "cjs", + target: "node", + outfile: "out.cjs", + onAfterBundle(api) { + const code = api.readFile("out.cjs"); + expect(code).not.toContain("import.meta"); + // The user's variable and the generated one get distinct names. + expect(code).toMatch(/var import_meta\w* = "declared by the user"/); + expect(code).toMatch(/var import_meta\w* = \{\}/); + expect(code).not.toContain(".hot"); + expect(code).toContain('.url = "assigned"'); + }, + run: { + runtime: "node", + stdout: ` + declared by the user object undefined entry.js undefined + true 1 undefined + assigned NaN true entry.js + `, + }, + }); + itBundled("edgecase/ImportMetaCjsInlinedAccessesNeedNoObject", { + files: { + "/entry.js": /* js */ ` + import { used } from "./dep.js"; + console.log(import.meta.url.startsWith("file:///"), import.meta.file, used); + `, + "/dep.js": /* js */ ` + export const used = import.meta.dir === import.meta.dirname; + export function unused() { + return import.meta.env; + } + export function alsoUnused() { + return import.meta; + } + `, + }, + format: "cjs", + target: "node", + outfile: "out.cjs", + onAfterBundle(api) { + const code = api.readFile("out.cjs"); + expect(code).not.toContain("import.meta"); + expect(code).not.toContain("unused"); + // Every access that made it into the output was inlined, and the + // references inside the tree shaken functions do not keep dep.js's + // object alive, so no object is declared in either file. + expect(code).not.toContain("import_meta"); + }, + run: { runtime: "node", stdout: "true entry.js true" }, + }); + itBundled("edgecase/ImportMetaCjsWarnsPerEmptiedReference", { + files: { + "/entry.js": /* js */ ` + import "dep"; + console.log(import.meta.url.startsWith("file:///"), import.meta.dir === import.meta.dirname); + console.log(typeof import.meta, import.meta.env); + try { + console.log(import.meta.resolve("./x")); + } catch { + console.log("caught"); + } + `, + "/node_modules/dep/index.js": /* js */ ` + console.log(JSON.stringify(import.meta), import.meta.env); + `, + }, + format: "cjs", + target: "node", + outfile: "out.cjs", + bundleWarnings: { + "/entry.js": [ + '"import.meta" is not available with the "cjs" output format and will be empty', + '"import.meta" is not available with the "cjs" output format and will be empty', + ], + }, + onAfterBundle(api) { + const [first, second] = api.warnings["/entry.js"]; + // Inlined accesses, the `try` body and node_modules are not reported. + expect([first.line, second.line]).toEqual(["3", "3"]); + }, + run: { + runtime: "node", + stdout: ` + {} undefined + true true + object undefined + caught + `, + }, + }); + // iife output is a classic script for every target but bun, which loads its + // `// @bun` output as a module, where the real `import.meta` keeps working. + itBundled("edgecase/ImportMetaIifeBecomesEmptyObject", { + files: { + "/entry.js": /* js */ ` + console.log(JSON.stringify(import.meta), import.meta.env, import.meta.url.startsWith("file:///"), import.meta.file); + `, + }, + format: "iife", + target: "browser", + outfile: "out.cjs", + bundleWarnings: { + "/entry.js": [ + '"import.meta" is not available with the "iife" output format and will be empty', + '"import.meta" is not available with the "iife" output format and will be empty', + ], + }, + onAfterBundle(api) { + api.expectFile("out.cjs").not.toContain("import.meta"); + }, + run: { runtime: "node", stdout: "{} undefined true entry.js" }, + }); + itBundled("edgecase/ImportMetaIifeBecomesEmptyObjectTargetNode", { + files: { + "/entry.js": /* js */ ` + console.log(JSON.stringify(import.meta), import.meta.env, import.meta.url.startsWith("file:///"), import.meta.file); + `, + }, + format: "iife", + target: "node", + outfile: "out.cjs", + onAfterBundle(api) { + api.expectFile("out.cjs").not.toContain("import.meta"); + }, + run: { runtime: "node", stdout: "{} undefined true entry.js" }, + }); + itBundled("edgecase/ImportMetaIifePathsAreInlinedPerFile", ({ root }) => ({ + files: importMetaPathsFiles, + format: "iife", + target: "browser", + outfile: "out.cjs", + onAfterBundle(api) { + api.expectFile("out.cjs").not.toContain("import.meta"); + api.expectFile("out.cjs").not.toContain("import_meta"); + }, + run: { runtime: "node", stdout: importMetaPathsStdout(root) }, + })); + itBundled("edgecase/ImportMetaIifePathsAreInlinedPerFileTargetNode", ({ root }) => ({ + files: importMetaPathsFiles, + format: "iife", + target: "node", + outfile: "out.cjs", + onAfterBundle(api) { + api.expectFile("out.cjs").not.toContain("import.meta"); + api.expectFile("out.cjs").not.toContain("import_meta"); + }, + run: { runtime: "node", stdout: importMetaPathsStdout(root) }, + })); + itBundled("edgecase/ImportMetaIifeTargetBunKeepsImportMeta", ({ root }) => ({ + files: { + "/entry.js": /* js */ ` + console.log(typeof import.meta.resolve, typeof import.meta.require, import.meta.url); + `, + }, + format: "iife", + target: "bun", + onAfterBundle(api) { + const code = api.readFile("out.js"); + expect(code).toContain("import.meta.resolve"); + expect(code).toContain("import.meta.url"); + expect(code).not.toContain("import_meta"); + }, + // The bundle's own `import.meta`, not entry.js's inlined one. + run: { stdout: `function function ${pathToFileURL(join(root, "out.js")).href}` }, + })); + itBundled("edgecase/ImportMetaEsmKeepsImportMeta", { + files: { + "/entry.js": /* js */ ` + const meta = import.meta; + import.meta.custom = "assigned"; + console.log(meta.custom, typeof meta.url, import.meta.url === meta.url); + `, + }, + format: "esm", + target: "bun", + onAfterBundle(api) { + const code = api.readFile("out.js"); + expect(code).toContain('import.meta.custom = "assigned"'); + expect(code).not.toContain("import_meta"); + }, + run: { stdout: "assigned string true" }, + }); }); for (const backend of ["api", "cli"] as const) {