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
10 changes: 7 additions & 3 deletions src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ pub mod EntryPoint {
pub(crate) use crate::entry_point::Kind;
}
use crate::bundled_ast::Flags as AstFlags;
use crate::generic_path_with_pretty_initialized;
use crate::{generic_path_with_pretty_initialized, pretty_path_base_dir};
type DeclaredSymbolList = bun_ast::DeclaredSymbolList;

impl<'a> LinkerContext<'a> {
Expand All @@ -382,8 +382,12 @@ impl<'a> LinkerContext<'a> {
path: &bun_paths::fs::Path<'static>,
arena: &Bump,
) -> Result<bun_paths::fs::Path<'static>, BunError> {
let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir;
generic_path_with_pretty_initialized(path, self.options.target, top_level_dir, arena)
generic_path_with_pretty_initialized(
path,
self.options.target,
pretty_path_base_dir(self.dev_server.as_ref()),
arena,
)
}

pub(crate) fn should_include_part(&self, source_index: crate::IndexInt, part: &Part) -> bool {
Expand Down
39 changes: 24 additions & 15 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub use bv2_impl::dispatch;
pub use bv2_impl::{
CompileResult, CompileResultForSourceMap, CompileResultForSourceMapColumns, ContentHasher,
EventLoop, ImportTracker, PartRange, StableRef, WrapKind, generic_path_with_pretty_initialized,
target_from_hashbang,
pretty_path_base_dir, target_from_hashbang,
};
pub use bv2_impl::{DevServerInput, DevServerOutput, ImportTrackerIterator, ImportTrackerStatus};
// Flatten the impl-body module into this file's namespace so external callers
Expand Down Expand Up @@ -2489,12 +2489,11 @@ pub mod bv2_impl {

if path.pretty.as_ptr() == path.text.as_ptr() {
// TODO: outbase
let base_dir = pretty_path_base_dir(self.dev_server.as_ref());
let rel = bun_paths::resolve_path::relative_platform::<
bun_paths::resolve_path::platform::Loose,
false,
>(
bun_resolver::fs::FileSystem::get().top_level_dir, path.text
);
>(base_dir, path.text);
// SAFETY: arena outlives the bundle pass; raw-pointer detour erases the
// `&self` lifetime so the resulting `&'static [u8]` doesn't pin `self`.
path.pretty =
Expand Down Expand Up @@ -5761,7 +5760,7 @@ pub mod bv2_impl {
let out = generic_path_with_pretty_initialized(
path,
target,
self.transpiler.fs().top_level_dir,
pretty_path_base_dir(self.dev_server.as_ref()),
bump,
)?;
Ok(out)
Expand Down Expand Up @@ -6455,12 +6454,6 @@ pub mod bv2_impl {
import_record.source_index = Index::INVALID;

if let Some(entry) = dev_server.is_file_cached(path.text, bake_graph) {
let rel = bun_paths::resolve_path::relative_platform::<
bun_paths::resolve_path::platform::Loose,
false,
>(
self.transpiler.fs().top_level_dir, path.text
);
if loader == Loader::Html && entry.kind == bake_types::CacheKind::Asset
{
// Overload `path.text` to point to the final URL
Expand All @@ -6486,8 +6479,6 @@ pub mod bv2_impl {
};
import_record.path.is_disabled = false;
} else {
import_record.path.text = path.text;
import_record.path.pretty = rel;
import_record.path = path_as_static(
&self
.path_with_pretty_initialized(path, target)
Expand Down Expand Up @@ -7565,10 +7556,28 @@ pub mod bv2_impl {
None
}

/// The directory `Path.pretty` is computed relative to.
///
/// In a dev server bundle the pretty path is the module id the HMR runtimes
/// load modules by, and the dev server derives the ids it asks them to load
/// from its own root (`DevServer::relative_path`), which `app.root` can
/// point somewhere other than the cwd. Both sides have to agree on the
/// directory, so dev server bundles relativize against that root; every
/// other build relativizes against the cwd.
pub fn pretty_path_base_dir(dev_server: Option<&dispatch::DevServerHandle>) -> &[u8] {
match dev_server {
// SAFETY: points at `DevServer.root`, which is written once and
// outlives every bundle the dev server runs (see the interface
// definition in lib.rs).
Some(dev_server) => unsafe { &*dev_server.root() },
None => bun_resolver::fs::FileSystem::get().top_level_dir,
}
}

pub fn generic_path_with_pretty_initialized(
path: &bun_paths::fs::Path<'static>,
target: options::Target,
top_level_dir: &[u8],
base_dir: &[u8],
bump: &bun_alloc::Arena,
) -> crate::Result<bun_paths::fs::Path<'static>> {
use crate::bun_fs::PathResolverExt as _;
Expand All @@ -7590,7 +7599,7 @@ pub mod bv2_impl {
let rel = bun_paths::resolve_path::relative_platform_buf::<
bun_paths::resolve_path::platform::Loose,
false,
>(&mut **buf2, top_level_dir, path.text);
>(&mut **buf2, base_dir, path.text);
let mut path_clone: crate::bun_fs::Path<'_> = *path;
if target == options::Target::ServerComponentsSsr {
let mut fbs = bun_io::FixedBufferStream::new_mut(&mut buf.0[..]);
Expand Down
9 changes: 8 additions & 1 deletion src/bundler/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ pub use HTMLImportManifest::html_import_manifest;
pub use bun_core::cheap_prefix_normalizer;
pub use bundle_v2::{
CompileResult, CompileResultForSourceMap, ContentHasher, EventLoop, ImportTracker, PartRange,
StableRef, WrapKind, generic_path_with_pretty_initialized, target_from_hashbang,
StableRef, WrapKind, generic_path_with_pretty_initialized, pretty_path_base_dir,
target_from_hashbang,
};
pub use chunk::{
CrossChunkImport, CrossChunkImportItem, CrossChunkImportItemList, bun_renamer,
Expand Down Expand Up @@ -332,6 +333,12 @@ bun_dispatch::link_interface! {
fn current_bundle_start_data() -> *mut ();
fn register_barrel_with_deferrals(path: &[u8]) -> Result<(), crate::Error>;
fn register_barrel_export(barrel_path: &[u8], alias: &[u8]);
// `DevServer.root`: the directory the dev server's module ids are
// relative to. Written once when the dev server is created and never
// again (so chunk workers may read it too), and alive for as long as
// the dev server is, which is longer than any bundle it runs. Read
// through `pretty_path_base_dir`.
fn root() -> *const [u8];
}
}
// SAFETY: the handle is `{ kind, owner: *mut () }`; the raw pointer is what
Expand Down
10 changes: 2 additions & 8 deletions src/bundler/linker_context/generateCodeForFileInChunkJS.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use bun_js_printer::renamer;
use bun_js_printer::{self as js_printer, PrintResult, PrintResultSuccess};

use crate::analyze_transpiled_module::ModuleInfo;
use crate::generic_path_with_pretty_initialized;
use crate::linker_context_mod::{StmtList, StmtListWhich};
use crate::options::Format as OutputFormat;
use crate::{Chunk, Index, LinkerContext, Part, PartRange, WrapKind};
Expand Down Expand Up @@ -189,13 +188,8 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>(
source_ref.path.text.as_ptr(),
source_ref.path.pretty.as_ptr(),
) {
let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir;
let new_path = bun_core::handle_oom(generic_path_with_pretty_initialized(
&source_ref.path,
c.options.target,
top_level_dir,
arena,
));
let new_path =
bun_core::handle_oom(c.path_with_pretty_initialized(&source_ref.path, arena));
source_storage = bun_ast::Source {
path: new_path,
// SAFETY: `source_ref` is `&'static Source`, so re-borrowing its
Expand Down
17 changes: 8 additions & 9 deletions src/runtime/bake/DevServer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,11 @@ pub struct DevServer {
/// To validate the DevServer has not been collected, this can be checked.
/// When freed, this is set to `undefined`. UAF here also trips ASAN.
pub(crate) magic: Magic,
/// Absolute path to project root directory. For the HMR
/// runtime, its module IDs are strings relative to this.
/// Absolute path to project root directory (`app.root`, the cwd unless
/// configured otherwise). The HMR runtimes' module IDs are paths relative
/// to this, both the ones the bundler prints into the bundles
/// (`bun_bundler::pretty_path_base_dir`) and the ones this server asks the
/// runtimes to load (`relative_path`).
pub(crate) root: Box<[u8]>,
/// Unique identifier for this DevServer instance. Used to identify it
/// when using the debugger protocol.
Expand Down Expand Up @@ -610,13 +613,9 @@ pub(crate) fn init(options: Options) -> JsResult<Box<DevServer>> {
let global = options.vm.global();

let generic_action = "while initializing development server";
// FileSystem is a process-lifetime singleton; `init` interns the path into
// the `DirnameStore` (process-lifetime arena) so no caller-side leak is
// needed for the `'static` it stores.
let _fs = match bun_resolver::fs::FileSystem::init(Some(options.root.as_bytes())) {
Ok(fs) => fs,
Err(err) => return Err(global.throw_error(err, generic_action)),
};
// The process-wide `FileSystem` was initialized with the cwd when the VM
// started, so `top_level_dir` is the cwd, not `options.root`. Everything
// that needs to be relative to the project root goes through `dev.root`.
let top_level_dir: &'static [u8] = bun_resolver::fs::FileSystem::get().top_level_dir;

// `.bun_watcher = undefined` → `Watcher.init(DevServer, dev, fs, ...)`
Expand Down
1 change: 1 addition & 0 deletions src/runtime/bake/dev_server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,7 @@ bun_bundler::link_impl_DevServerHandle! {
};
let _ = gop.value_ptr.get_or_put(alias);
},
root() => core::ptr::from_ref::<[u8]>(&(*this).root),
}
}

Expand Down
57 changes: 57 additions & 0 deletions test/bake/dev/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,3 +865,60 @@ devTest("barrel optimization: namespace re-export cycle through a star-exported
await c.expectMessage("result: object Y KEEP DEEP OTHER");
},
});
// The dev server addresses modules by their path relative to `app.root`, so the
// bundler has to compute the module ids it prints relative to that same
// directory rather than to the cwd. Routes are served from a directory below the
// cwd here, so the two differ.
devTest("app.root that is not the cwd", {
files: {
"bun.app.ts": `
import path from "node:path";
export default {
app: {
root: path.join(process.cwd(), "app"),
framework: {
fileSystemRouterTypes: [
{
root: "app/routes",
style: "nextjs-pages",
serverEntryPoint: "./app/server.ts",
clientEntryPoint: "./app/client.ts",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
],
},
},
};
`,
"app/server.ts": `
export function render(req, meta) {
const scripts = meta.modules.map(src => '<script type="module" src="' + src + '"></script>').join("");
return new Response("<!DOCTYPE html><html><body><p>" + meta.pageModule.default() + "</p>" + scripts + "</body></html>", {
headers: { "Content-Type": "text/html" },
});
}
`,
"app/client.ts": `
console.log("client loaded");
`,
"app/message.ts": `
export const message = "Hello";
`,
"app/routes/index.ts": `
import { message } from "../message";
export default () => message;
`,
},
async test(dev) {
await dev.fetch("/").expect.toInclude("<p>Hello</p>");

await using c = await dev.client("/");
await c.expectMessage("client loaded");

// Server-side changes make connected clients reload the page.
await c.expectReload(async () => {
await dev.write("app/message.ts", `export const message = "Updated";`);
});
await c.expectMessage("client loaded");
await dev.fetch("/").expect.toInclude("<p>Updated</p>");
},
});
Loading