diff --git a/src/ast/runtime.rs b/src/ast/runtime.rs index 80fec8848aa6..bb2f3ab00fc2 100644 --- a/src/ast/runtime.rs +++ b/src/ast/runtime.rs @@ -175,10 +175,11 @@ pub struct Imports { pub __promiseAll: Ref, pub __MEMO_CACHE_SENTINEL: Ref, pub __EARLY_RETURN_SENTINEL: Ref, + pub __glob: Ref, } impl Imports { - pub const ALL: [&'static [u8]; 27] = [ + pub const ALL: [&'static [u8]; 28] = [ b"__name", b"__require", b"__export", @@ -206,12 +207,13 @@ impl Imports { b"__promiseAll", b"__MEMO_CACHE_SENTINEL", b"__EARLY_RETURN_SENTINEL", + b"__glob", ]; /// Rust stable cannot sort in `const`; precomputed here and verified by /// the test in `tests` below. #[cfg_attr(not(test), allow(dead_code))] - const ALL_SORTED: [&'static [u8]; 27] = [ + const ALL_SORTED: [&'static [u8]; 28] = [ b"$$typeof", b"__EARLY_RETURN_SENTINEL", b"__MEMO_CACHE_SENTINEL", @@ -222,6 +224,7 @@ impl Imports { b"__export", b"__exportDefault", b"__exportValue", + b"__glob", b"__jsonParse", b"__legacyDecorateClassTS", b"__legacyDecorateParamTS", @@ -243,34 +246,35 @@ impl Imports { /// When generating the list of runtime imports, we sort it for determinism. /// This is a lookup table so we don't need to resort the strings each time - pub const ALL_SORTED_INDEX: [usize; 27] = [ - 15, // __name - 24, // __require + pub const ALL_SORTED_INDEX: [usize; 28] = [ + 16, // __name + 25, // __require 7, // __export - 23, // __reExport + 24, // __reExport 9, // __exportValue 8, // __exportDefault - 14, // __merge - 11, // __legacyDecorateClassTS - 12, // __legacyDecorateParamTS - 13, // __legacyMetadataTS - 22, // __publicField - 18, // __privateIn - 17, // __privateGet - 16, // __privateAdd - 20, // __privateSet - 19, // __privateMethod + 15, // __merge + 12, // __legacyDecorateClassTS + 13, // __legacyDecorateParamTS + 14, // __legacyMetadataTS + 23, // __publicField + 19, // __privateIn + 18, // __privateGet + 17, // __privateAdd + 21, // __privateSet + 20, // __privateMethod 6, // __decoratorStart 5, // __decoratorMetadata - 25, // __runInitializers + 26, // __runInitializers 4, // __decorateElement 0, // $$typeof - 26, // __using + 27, // __using 3, // __callDispose - 10, // __jsonParse - 21, // __promiseAll + 11, // __jsonParse + 22, // __promiseAll 2, // __MEMO_CACHE_SENTINEL 1, // __EARLY_RETURN_SENTINEL + 10, // __glob ]; pub const NAME: &'static [u8] = b"bun:wrap"; @@ -306,6 +310,7 @@ impl Imports { 24 => self.__promiseAll, 25 => self.__MEMO_CACHE_SENTINEL, 26 => self.__EARLY_RETURN_SENTINEL, + 27 => self.__glob, _ => return None, }; r.to_nullable() @@ -341,6 +346,7 @@ impl Imports { 24 => Some(&mut self.__promiseAll), 25 => Some(&mut self.__MEMO_CACHE_SENTINEL), 26 => Some(&mut self.__EARLY_RETURN_SENTINEL), + 27 => Some(&mut self.__glob), _ => None, } } diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index 86fc7ff7c7e4..5c9257d245f3 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -2412,6 +2412,7 @@ pub mod parse_worker { // a worker-owned `Transpiler` that outlives the parse. // SAFETY: ARENA — `topts` outlives `opts` (worker-owned for the bundle pass). opts.allow_unresolved = unsafe { bun_collections::detach_ref(&topts.allow_unresolved) }; + opts.glob_resolver = Some(crate::options::parser_glob_resolver); // `Transpiler.macro_context` is `Option` // (same nominal type as `ParserOptions.macro_context`'s pointee). Reborrow // through the raw `*mut Transpiler` so the `&mut MacroContext` is disjoint diff --git a/src/bundler/options.rs b/src/bundler/options.rs index e709614c53eb..52c1ffac139b 100644 --- a/src/bundler/options.rs +++ b/src/bundler/options.rs @@ -77,6 +77,47 @@ pub fn validate_path( // `&transpiler.options.allow_unresolved` straight through. pub use bun_js_parser::options::AllowUnresolved; +/// `glob_resolver` for the parser's template-literal `require()` / `import()` +/// support: returns each match as a `./`- or `../`-prefixed relative specifier. +pub fn parser_glob_resolver(source_dir: &[u8], pattern: &[u8]) -> Vec> { + let mut out: Vec> = Vec::new(); + if !(pattern.starts_with(b"./") || pattern.starts_with(b"../")) { + return out; + } + + let walker = match bun_glob::BunGlobWalker::init_with_cwd( + pattern, source_dir, true, false, true, false, true, None, + ) { + Ok(Ok(w)) => w, + _ => return out, + }; + let mut walker = Box::new(walker); + let mut iter = bun_glob::walk::Iterator::new(&mut *walker); + if iter.init().map(|m| m.is_err()).unwrap_or(true) { + return out; + } + loop { + match iter.next() { + Ok(Ok(Some(m))) => { + let p: &[u8] = &m; + let mut s: Vec = p + .iter() + .map(|&b| if cfg!(windows) && b == b'\\' { b'/' } else { b }) + .collect(); + if !(s.starts_with(b"./") || s.starts_with(b"../")) { + s.splice(0..0, b"./".iter().copied()); + } + out.push(s.into_boxed_slice()); + } + Ok(Ok(None)) => break, + // A mid-walk failure would otherwise emit a partial map whose + // keys can never cover the runtime specifier. + Ok(Err(_)) | Err(_) => return Vec::new(), + } + } + out +} + // Canonical defs live in `bun_resolver::options` (lower tier; resolver is the // runtime consumer of `.patterns`/`.abs_paths`/`.node_modules`). Re-export so // `BundleOptions.external` and `Resolver.opts.external` are the SAME nominal diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 8fb72a09715a..5102d389a63a 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1550,6 +1550,7 @@ impl<'a> Transpiler<'a> { macro_context: None, warn_about_unbundled_modules: !target.is_bun(), allow_unresolved: &p_opts::AllowUnresolved::DEFAULT, + glob_resolver: None, module_type: to_parser_module_type(this_parse.module_type), output_format: p_opts::Format::Esm, transform_only: self.options.transform_only, diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 2a4761ab6141..49de1ed36256 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -563,6 +563,10 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { pub const_values: bun_ast::ast_result::ConstValuesMap, + /// `const x = ` initializers, consulted by + /// `append_dynamic_specifier_shape` so `require(x)` can glob. + pub glob_specifier_values: bun_ast::ast_result::ConstValuesMap, + // These are backed by stack fallback allocators in _parse, and are uninitialized until then. pub binary_expression_stack: ListManaged<'a, BinaryExpressionVisitor>, // Reusable stack for `SideEffects::simplify_unused_binary_comma_expr`; @@ -801,28 +805,322 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O arg: Expr, buf: &'b mut BumpVec<'a, u8>, ) -> Result<&'b [u8], crate::Error> { - if let Some(tmpl) = arg.data.e_template() { - if tmpl.tag.is_some() { - return Ok(b""); // tagged template — opaque + if self.append_dynamic_specifier_shape(arg, buf, 0)? { + Ok(buf.as_slice()) + } else { + buf.clear(); + Ok(b"") + } + } + + fn append_estring_rope( + &self, + s: &js_ast::e::EString, + buf: &mut BumpVec<'a, u8>, + ) -> Result { + let start = buf.len(); + let mut cur: Option<&js_ast::e::EString> = Some(s); + while let Some(seg) = cur { + let bytes = seg.string(self.arena)?; + if bytes.iter().any(|&b| b == 0) { + buf.truncate(start); + return Ok(false); + } + buf.extend_from_slice(bytes); + cur = seg.next.as_ref().map(|r| r.get()); + } + Ok(true) + } + + /// Appends `arg`'s static shape to `buf`; `false` means nothing was + /// appended (`buf` is truncated back to its entry length). + fn append_dynamic_specifier_shape( + &mut self, + arg: Expr, + buf: &mut BumpVec<'a, u8>, + depth: u32, + ) -> Result { + if depth >= 32 { + return Ok(false); + } + let start = buf.len(); + let ok = match &arg.data { + js_ast::ExprData::EString(s) => self.append_estring_rope(s, buf)?, + js_ast::ExprData::ETemplate(tmpl) => 'tmpl: { + if tmpl.tag.is_some() { + break 'tmpl false; + } + match &tmpl.head { + js_ast::e::TemplateContents::Cooked(head) => { + if !self.append_estring_rope(head, buf)? { + break 'tmpl false; + } + } + js_ast::e::TemplateContents::Raw(_) => break 'tmpl false, + } + let mut ok = true; + for part in tmpl.parts().iter() { + let value = part.value; + if !self.append_dynamic_specifier_shape(value, buf, depth + 1)? { + buf.push(0); + } + match &part.tail { + js_ast::e::TemplateContents::Cooked(tail) => { + if !self.append_estring_rope(tail, buf)? { + ok = false; + break; + } + } + js_ast::e::TemplateContents::Raw(_) => { + ok = false; + break; + } + } + } + ok } - match &tmpl.head { - js_ast::e::TemplateContents::Cooked(head) => { - buf.extend_from_slice(head.string(self.arena)?); + js_ast::ExprData::EBinary(bin) if bin.op == js_ast::Op::Code::BinAdd => { + let (l, r) = (bin.left, bin.right); + let ok_l = self.append_dynamic_specifier_shape(l, buf, depth + 1)?; + if !ok_l { + buf.push(0); } - js_ast::e::TemplateContents::Raw(_) => return Ok(b""), // shouldn't happen post-visit but be safe + let ok_r = self.append_dynamic_specifier_shape(r, buf, depth + 1)?; + if !ok_r { + buf.push(0); + } + ok_l || ok_r } - for part in tmpl.parts().iter() { - buf.push(0); // \x00 placeholder per interpolation - match &part.tail { - js_ast::e::TemplateContents::Cooked(tail) => { - buf.extend_from_slice(tail.string(self.arena)?); + js_ast::ExprData::EIdentifier(id) => { + let ref_ = id.ref_; + if ref_.source_index() == self.source.index.0 + && self.symbols[ref_.inner_index() as usize].kind + == js_ast::symbol::Kind::Constant + { + if let Some(init) = self.glob_specifier_values.remove(&ref_) { + let r = self.append_dynamic_specifier_shape(init, buf, depth + 1); + self.glob_specifier_values.put(ref_, init).expect("oom"); + return r; } - js_ast::e::TemplateContents::Raw(_) => return Ok(b""), // raw tail — treat as opaque } + false + } + _ => false, + }; + if !ok { + buf.truncate(start); + } + Ok(ok) + } + + fn glob_shape_is_eligible(shape: &[u8]) -> bool { + if !(shape.starts_with(b"./") || shape.starts_with(b"../")) { + return false; + } + if !shape.iter().any(|b| *b == 0) { + return false; + } + match shape.last() { + Some(&0) | Some(&b'/') => return false, + _ => {} + } + for &b in shape { + if matches!(b, b'*' | b'?' | b'[' | b'{' | b'!' | b'\\') { + return false; + } + } + true + } + + /// `\x00` placeholders become `**/*` between slashes, `*` otherwise + /// (matches esbuild; keeps `./mods/${x}.js` bounded to one directory). + fn glob_shape_to_pattern(shape: &[u8], out: &mut Vec) { + out.clear(); + let mut i = 0; + while i < shape.len() { + let b = shape[i]; + if b != 0 { + out.push(b); + i += 1; + continue; } - return Ok(buf.as_slice()); + while i < shape.len() && shape[i] == 0 { + i += 1; + } + let left_slash = out.last() == Some(&b'/'); + let right_slash = shape.get(i) == Some(&b'/'); + if left_slash && right_slash { + out.extend_from_slice(b"**/*"); + } else { + out.push(b'*'); + } + } + } + + /// Record `const x =