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
3 changes: 2 additions & 1 deletion src/ast/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ impl Target {
// `bake_graph()` stays in bun_bake (would back-edge into tier-6).
// `out_extensions()` stays in bun_bundler (allocator-heavy, only used there).

const MAIN_FIELD_NAMES: [&'static str; 4] = [
/// Every main field that the default list of some target names.
pub const MAIN_FIELD_NAMES: [&'static str; 4] = [
"browser",
"module",
"main",
Expand Down
2 changes: 2 additions & 0 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2045,6 +2045,8 @@ pub mod bv2_impl {
// Re-project the resolver subset now that `target`/`conditions` etc.
// have been overwritten for the browser.
boxed.sync_resolver_opts();
// The list above is the default of a browser, whatever `--main-fields` gave the server.
boxed.resolver.opts.main_fields_is_default = true;
boxed.resolver.env_loader = NonNull::new(this_env.cast());

// Park the owning Box first, then derive both the published `NonNull`
Expand Down
10 changes: 4 additions & 6 deletions src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,10 @@ pub(crate) use bun_ast::Target;
pub use bun_ast::target::TARGET_MAP;

pub(crate) const TARGET_MAIN_FIELD_NAMES: [&[u8]; 4] = [
b"browser",
b"module",
b"main",
// https://github.com/jsforum/jsforum/issues/5
// Older packages might use jsnext:main in place of module
b"jsnext:main",
Target::MAIN_FIELD_NAMES[0].as_bytes(),
Target::MAIN_FIELD_NAMES[1].as_bytes(),
Target::MAIN_FIELD_NAMES[2].as_bytes(),
Target::MAIN_FIELD_NAMES[3].as_bytes(),
];

// Note that this means if a package specifies "module" and "main", the ES6
Expand Down
86 changes: 68 additions & 18 deletions src/resolver/package_json.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bun_ast as js_ast;
use bun_ast::Target;
use bun_collections::{ArrayHashMap, StringArrayHashMap};
use bun_core::Output;
use bun_core::StackCheck;
Expand Down Expand Up @@ -45,7 +46,8 @@ pub type MacroMap = StringArrayHashMap<MacroImportReplacementMap>;
// borrow kept alive by `PackageJSON::source_contents` (the owning field).
type ScriptsMap = StringArrayHashMap<&'static [u8]>;

type MainFieldMap = StringMap;
// A slot per `Target::MAIN_FIELD_NAMES`: a string value in the source buffer or the tape, or empty.
type MainFields = [js_ast::E::Str; Target::MAIN_FIELD_NAMES.len()];

#[derive(Default)]
pub struct DependencyMap {
Expand Down Expand Up @@ -85,7 +87,10 @@ pub struct PackageJSON {
/// field is what keeps that borrow — and the map values above — alive.)
pub(crate) source_contents: Box<[u8]>,
pub(crate) json_tape: Option<Box<js_ast::E::JsonTape>>,
pub(crate) main_fields: MainFieldMap,
/// Not only those of the resolver that parses: every resolver shares this, and has its own list.
pub(crate) main_fields: MainFields,
/// The rows of the root object in `json_tape`. `main_field` finds any other name there.
pub(crate) root_rows: core::ops::Range<u32>,
pub module_type: ModuleType,
pub version: Box<[u8]>,

Expand Down Expand Up @@ -145,7 +150,8 @@ impl Default for PackageJSON {
source: bun_ast::Source::default(),
source_contents: Box::default(),
json_tape: None,
main_fields: MainFieldMap::default(),
main_fields: MainFields::default(),
root_rows: 0..0,
module_type: ModuleType::default(),
version: Box::default(),
scripts: None,
Expand Down Expand Up @@ -210,6 +216,30 @@ impl PackageJSON {
})
}

/// The file that the main field `name` gives. A resolver asks for the names of its own list.
pub(crate) fn main_field(&self, name: &[u8]) -> Option<&[u8]> {
let file = match default_main_field(name) {
Some(slot) => self.main_fields[slot].slice(),
None => self.main_field_outside_defaults(name),
};
(!file.is_empty()).then_some(file)
}

/// Only `--main-fields` gives a resolver such a name, so it has no slot.
#[cold]
#[inline(never)]
fn main_field_outside_defaults(&self, name: &[u8]) -> &[u8] {
let Some(tape) = self.json_tape.as_deref() else {
return b"";
};
string_property(tape.properties(self.root_rows.clone()), name).slice()
}

/// Returns true if `main_field` finds one of `Target::MAIN_FIELD_NAMES`.
pub(crate) fn has_default_main_field(&self) -> bool {
self.main_fields.iter().any(|file| !file.is_empty())
}

/// Normalize path separators to forward slashes for glob matching
/// This is needed because glob patterns use forward slashes but Windows uses backslashes
fn normalize_path_for_glob(path: &[u8]) -> Result<Vec<u8>, bun_alloc::AllocError> {
Expand All @@ -219,6 +249,24 @@ impl PackageJSON {
}
}

/// The slot of `name` in `PackageJSON::main_fields`.
fn default_main_field(name: &[u8]) -> Option<usize> {
Target::MAIN_FIELD_NAMES
.iter()
.position(|default| default.as_bytes() == name)
}

/// The value of the first of `rows` that has the key `name`, when it is a string. Else empty.
fn string_property(rows: &[js_ast::E::PropertyJSON], name: &[u8]) -> js_ast::E::Str {
match rows.iter().find(|row| row.key.slice() == name) {
Some(js_ast::E::PropertyJSON {
value: js_ast::E::JsonValue::String(value),
..
}) => *value,
_ => js_ast::E::Str::EMPTY,
}
}

#[derive(Default)]
pub enum SideEffects {
/// either `package.json` is missing "sideEffects", it is true, or some
Expand Down Expand Up @@ -506,12 +554,13 @@ impl PackageJSON {
};
let json: js_ast::Expr = parsed_json.root;

if !json.is_object() {
let js_ast::ExprData::EObjectJSON(root) = &json.data else {
// Invalid package.json in node_modules is noisy.
// Let's just ignore it.
// (allocator.free dropped — entry.contents owned by `entry`)
return None;
}
};
let root = root.get();

let mut package_json = PackageJSON {
name: Box::default(),
Expand All @@ -524,7 +573,8 @@ impl PackageJSON {
json_tape: None,
module_type: ModuleType::Unknown,
browser_map: BrowserMap::default(),
main_fields: MainFieldMap::default(),
main_fields: MainFields::default(),
root_rows: root.rows(),
scripts: None,
config: None,
package_manager_package_id: INVALID_PACKAGE_ID,
Expand Down Expand Up @@ -594,18 +644,14 @@ impl PackageJSON {
}

// Read the "main" fields
for main in r.opts.main_fields.iter() {
if let Some(main_json) = json.as_property(main) {
let expr: &js_ast::Expr = &main_json.expr;

if let Some(str) = expr.as_utf8_string_literal() {
if !str.is_empty() {
package_json
.main_fields
.put(main, Box::from(str))
.expect("unreachable");
}
}
for (file, name) in package_json
.main_fields
.iter_mut()
.zip(Target::MAIN_FIELD_NAMES)
{
// The lookup of the "browser" property below stores the one that is a string.
if name != "browser" {
*file = string_property(root.properties(), name.as_bytes());
}
}

Expand Down Expand Up @@ -682,6 +728,10 @@ impl PackageJSON {
}
}
}
} else if let Some(file) = browser_prop.expr.as_utf8_string_literal()
&& let Some(slot) = default_main_field(b"browser")
{
package_json.main_fields[slot] = js_ast::E::Str::new(file);
}
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/resolver/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5691,8 +5691,7 @@ impl<'a> Resolver<'a> {
// Try using the main field(s) from "package.json"
if let Some(pkg_json) = dir_info.package_json() {
package_json = Some(std::ptr::from_ref(pkg_json));
if pkg_json.main_fields.count() > 0 {
let main_field_values = &pkg_json.main_fields;
if pkg_json.has_default_main_field() || !self.opts.main_fields_is_default {
// BACKREF: `RawSlice` detaches the `&self.opts.main_fields`
// borrow so the loop body can take `&mut self`. Backing
// `Box<[Box<[u8]>]>` heap buffer is owned by `self.opts` and
Expand All @@ -5713,7 +5712,7 @@ impl<'a> Resolver<'a> {

for key in main_field_keys.iter() {
let key: &[u8] = key;
let field_rel_path = match main_field_values.get(key) {
let field_rel_path = match pkg_json.main_field(key) {
Some(v) => v,
None => {
if let Some(debug) = self.debug_logs.as_mut() {
Expand Down Expand Up @@ -5758,7 +5757,7 @@ impl<'a> Resolver<'a> {
let mut auto_main_result = MatchResult::default();
let mut auto_main_found = false;

if let Some(main_rel_path) = main_field_values.get(b"main".as_slice()) {
if let Some(main_rel_path) = pkg_json.main_field(b"main") {
if !main_rel_path.is_empty() {
auto_main_found = self
.load_from_main_field(
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/bake/config/framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ impl Framework {
}
Self::resolve_helper(
&self.built_in_modules,
client,
server,
&mut fsr.entry_server,
&mut had_errors,
b"server side entrypoint",
Expand Down
19 changes: 12 additions & 7 deletions src/runtime/bake/config/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,24 @@ impl Framework {
let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::borrowing(arena);
let _ast_scope = ast_memory_allocator.enter();

let target = match renderer {
Graph::Client => bun_schema::api::Target::browser,
Graph::Server | Graph::Ssr => bun_schema::api::Target::bun,
Comment thread
robobun marked this conversation as resolved.
Resolved
};
Comment thread
robobun marked this conversation as resolved.
Resolved

// The caller (`DevServer::init`) hands us an uninitialized slot, so
// use `MaybeUninit::write` (no drop of prior bytes) then reborrow as
// `&mut Transpiler` for the field assignments below.
let out: &mut bun_bundler::Transpiler = out.write(bun_bundler::Transpiler::init(
arena,
log,
// `TransformOptions::default()`: every `Option` is `None`, every
// slice empty, every scalar zero/false.
bun_schema::api::TransformOptions::default(),
bun_schema::api::TransformOptions {
target: Some(target),
..Default::default()
},
None,
)?);
Comment thread
robobun marked this conversation as resolved.
Resolved

out.options.target = match renderer {
Graph::Client => bun_ast::Target::Browser,
Graph::Server | Graph::Ssr => bun_ast::Target::Bun,
};
out.options.public_path = match renderer {
Graph::Client => dev_server::CLIENT_PREFIX.as_bytes().into(),
Graph::Server | Graph::Ssr => Box::default(),
Expand Down Expand Up @@ -104,6 +106,9 @@ impl Framework {
if bundler_options.env != bun_schema::api::DotEnvBehavior::_none {
out.options.env.behavior = bundler_options.env;
out.options.env.prefix = bundler_options.env_prefix.clone().unwrap_or_default();
} else {
// `init` turns inlining on for the bun target.
out.options.env.behavior = bun_schema::api::DotEnvBehavior::disable;
}
// The resolver crate carries a FORWARD_DECL subset of
// `BundleOptions`, so re-project via the dedicated helper rather than
Expand Down
8 changes: 7 additions & 1 deletion src/runtime/bake/dev_server/directory_watch_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,16 @@ pub(crate) struct Dep {
pub(crate) source_file_path: bun_ptr::RawSlice<u8>,
/// The specifier that failed. Allocated memory.
pub(crate) specifier: Box<[u8]>,
/// The graph of the importer. Its resolver tries the import again.
pub(crate) graph: Graph,
}
impl Default for Dep {
fn default() -> Self {
Dep {
next: None,
source_file_path: bun_ptr::RawSlice::EMPTY,
specifier: Box::default(),
graph: Graph::Client,
}
}
}
Expand Down Expand Up @@ -213,7 +216,7 @@ impl DirectoryWatchStore {
}
};

match self.insert(dir, owned_file_path, specifier) {
match self.insert(dir, owned_file_path, specifier, renderer) {
Ok(()) => Ok(()),
Err(DirectoryWatchInsertError::Ignore) => Ok(()), // ignoring watch errors.
Err(DirectoryWatchInsertError::OutOfMemory) => Err(bun_alloc::AllocError),
Expand All @@ -227,6 +230,7 @@ impl DirectoryWatchStore {
dir_name_to_watch: &[u8],
file_path: bun_ptr::RawSlice<u8>,
specifier: &[u8],
graph: Graph,
) -> Result<(), DirectoryWatchInsertError> {
debug_assert!(!specifier.is_empty());
// TODO: watch the parent dir too.
Expand Down Expand Up @@ -282,6 +286,7 @@ impl DirectoryWatchStore {
next: prev_first,
source_file_path: file_path,
specifier: specifier_cloned,
graph,
});
self.watches.values_mut()[gop_index].first_dep = dep;
return Ok(());
Expand Down Expand Up @@ -374,6 +379,7 @@ impl DirectoryWatchStore {
next: None,
source_file_path: file_path,
specifier: specifier_cloned,
graph,
});
self.watches.values_mut()[gop_index] = Entry {
dir: fd,
Expand Down
18 changes: 15 additions & 3 deletions src/runtime/bake/dev_server/hot_reload_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,28 @@ impl HotReloadEvent {
while let Some(index) = it {
// Note: reshaped for borrowck — re-index per iteration instead of
// holding `dep` ref across resolver call + appendFile + freeDependencyIndex.
let (source_file_path, specifier, next) = {
let (source_file_path, specifier, graph, next) = {
let dep = &dev.directory_watchers.dependencies[index as usize];
(dep.source_file_path, &raw const *dep.specifier, dep.next)
(
dep.source_file_path,
&raw const *dep.specifier,
dep.graph,
dep.next,
)
};
it = next;

let transpiler = match graph {
Graph::Client => &mut dev.client_transpiler,
Graph::Server | Graph::Ssr => &mut dev.server_transpiler,
};
// SAFETY: both transpilers are initialized in DevServer::init
// before any HotReloadEvent can fire.
let transpiler = unsafe { transpiler.assume_init_mut() };
// `specifier` points into the dep's owned `Box<[u8]>`, which is
// not mutated until after `resolve` returns.
// SAFETY: see `Dep` doc — neither slice is mutated mid-resolve.
let resolved = unsafe { dev.server_transpiler.assume_init_mut() }
let resolved = transpiler
.resolver
.resolve(
bun_paths::resolve_path::dirname::<bun_paths::platform::Auto>(
Expand Down
Loading
Loading