Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
robobun marked this conversation as resolved.
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 {
Expand Down
1 change: 1 addition & 0 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
155 changes: 96 additions & 59 deletions src/js_parser/fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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.
Comment thread
robobun marked this conversation as resolved.
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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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`.
Comment thread
robobun marked this conversation as resolved.
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<Expr> {
let p = self;

// `import.meta.url = x` / `delete import.meta.url` must stay a property
// access: inlining the value would print `"file:///..." = x`.
Comment thread
robobun marked this conversation as resolved.
if identifier_opts.assign_target() != js_ast::AssignTarget::None
|| identifier_opts.is_delete_target()
{
return None;
}

let inlined: Option<Expr> = 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.
Comment thread
robobun marked this conversation as resolved.
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],
Expand Down
52 changes: 52 additions & 0 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Comment thread
robobun marked this conversation as resolved.
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`).
Comment thread
robobun marked this conversation as resolved.
pub(crate) empty_import_meta_locs: List<'a, bun_ast::Loc>,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
pub(crate) hmr_api_ref: Ref,

/// If bake is enabled and this is a server-side file, we want to use
Expand Down Expand Up @@ -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`.
Comment thread
robobun marked this conversation as resolved.
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.
Comment thread
robobun marked this conversation as resolved.
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.
Comment thread
robobun marked this conversation as resolved.
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.
Comment thread
robobun marked this conversation as resolved.
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
}
Expand Down Expand Up @@ -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,
Expand Down
70 changes: 70 additions & 0 deletions src/js_parser/parse/parse_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ pub struct Options<'a> {
pub import_meta_main_value: Option<bool>,
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.
Comment thread
robobun marked this conversation as resolved.
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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Comment thread
robobun marked this conversation as resolved.
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;
Expand Down
Loading
Loading