Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
86 changes: 78 additions & 8 deletions src/runtime/bake/production.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1318,23 +1318,88 @@ unsafe extern "C" {
) -> *mut JSPromise;
}

/// Inverse of the key spelling in `resolve_disk_key`, for the fetch hook to read
/// the file. Must stay a plain Win32 path: the module loader cuts specifiers at
/// the first `?`, so a `\\?\` prefixed one was read as `\\`.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[unsafe(no_mangle)]
extern "C" fn BakeToWindowsPath(input: BunString) -> BunString {
#[cfg(unix)]
#[cfg(not(windows))]
{
let _ = input;
panic!("This code should not be called on POSIX systems.");
}
#[cfg(not(unix))]
#[cfg(windows)]
{
let input_utf8 = input.to_utf8();
let input_slice = input_utf8.slice();
let mut output = bun_paths::w_path_buffer_pool::get();
let output_slice = strings::to_w_path_normalize_auto_extend(&mut output[..], input_slice);
BunString::clone_utf16(output_slice.as_slice())
let mut path = key_path_to_disk_path(input_utf8.slice()).to_vec();
resolve_path::slashes_to_windows_in_place(&mut path);
BunString::clone_utf8(&path)
}
}

/// `/C:/a/b.mjs` -> `C:/a/b.mjs`; anything else is returned as is.
#[cfg(windows)]
fn key_path_to_disk_path(key_path: &[u8]) -> &[u8] {
match key_path.strip_prefix(b"/") {
Some(rest) if strings::starts_with_windows_drive_letter(rest) => rest,
_ => key_path,
}
}

/// Drive (`C:\a`, `C:/a`) or UNC (`\\server\share\a`) path, as opposed to a
/// bundle output path like `/_bun/abc123.js`.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[cfg(windows)]
fn is_disk_path(path: &[u8]) -> bool {
resolve_path::windows_volume_name_len(path).0 > 0 && bun_paths::is_absolute(path)
}

/// Keys are posix-style paths (`bake:/_bun/abc123.js`), but a file imported from
/// outside the bundle is keyed by its disk path, which the fetch hook reads from
/// disk. A Windows disk path on either side is resolved here like
/// `path.win32.resolve(dirname(referrer), specifier)` and spelled the way `file:`
/// URLs spell it: `bake:/C:/a/b.mjs`, `bake://server/share/b.mjs`. The loader
/// resolves the returned key once more (referrer `bake:/`), so both spellings
/// have to come back out of here unchanged. `None` when neither side is on disk.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[cfg(windows)]
fn resolve_disk_key(
global: &JSGlobalObject,
referrer_key_path: &[u8],
specifier: &[u8],
) -> Option<BunString> {
let referrer = key_path_to_disk_path(referrer_key_path);
let specifier = key_path_to_disk_path(specifier);
if !is_disk_path(referrer) && !is_disk_path(specifier) {
return None;
}

let dir = bun_paths::Dirname::dirname(referrer).unwrap_or(referrer);
let mut buf = bun_paths::path_buffer_pool::get();
let Some(resolved) = resolve_path::join_abs_string_buf_checked::<platform::Windows>(
dir,
&mut buf[..],
&[specifier],
) else {
let _ = global.throw(format_args!(
"Cannot import {}: the resolved path is too long",
bun_core::fmt::quote(specifier),
));
return Some(BunString::dead());
};
let resolved_len = resolved.len();
let resolved = &mut buf[..resolved_len];
resolve_path::slashes_to_posix_in_place(resolved);

let slash = if strings::starts_with_windows_drive_letter(resolved) {
"/"
} else {
""
};
Some(BunString::create_format(format_args!(
"bake:{slash}{}",
BStr::new(resolved)
)))
}

#[unsafe(no_mangle)]
extern "C" fn BakeProdResolve(
global: &JSGlobalObject,
Expand Down Expand Up @@ -1363,10 +1428,15 @@ extern "C" fn BakeProdResolve(
}

debug_assert!(strings::has_prefix(referrer.slice(), b"bake:"));
let referrer_key_path = &referrer.slice()[5..];

#[cfg(windows)]
if let Some(key) = resolve_disk_key(global, referrer_key_path, specifier.slice()) {
return key;
}

// dirname semantics: returns None for the root / no-parent.
let after_scheme = &referrer.slice()[5..];
let dir = bun_paths::Dirname::dirname(after_scheme).unwrap_or(after_scheme);
let dir = bun_paths::Dirname::dirname(referrer_key_path).unwrap_or(referrer_key_path);

BunString::create_format(format_args!(
"bake:{}",
Expand Down
48 changes: 48 additions & 0 deletions test/bake/dev/production.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,4 +594,52 @@ export default function IndexPage() {
// Verify NO JavaScript imports are included in the HTML
expect(htmlContent).not.toContain('<script type="module"');
});

test("a route can import a file outside the bundle while rendering", async () => {
// import() of a path the bundler never saw is keyed under "bake:" like the bundled
// modules, misses the module map, and is read from disk by the regular loader. On
// Windows the specifier is a drive path (import.meta.dir is inlined as one), which
// used to be joined onto the referrer's bundle path as if it were relative, and the
// build failed with `EINVAL reading "\\"`.
const dir = await tempDirWithBakeDeps("bake-production-disk-import", {
"src/index.tsx": `export default { app: { framework: "react" } };`,
"extra/banner.mjs": `import { detail } from "../shared/detail.mjs";

export const banner = "read from disk while rendering";
export { detail };`,
"shared/detail.mjs": `export const detail = "resolved relative to the file on disk";`,
"pages/index.tsx": `import { join } from "node:path";

export default async function IndexPage() {
// Computed specifiers, so the bundler leaves these import()s to the runtime.
// The first is a normalized native path; the second still has the ".." in it
// and, on Windows, mixes separators. Both must name the same module.
const joined = await import(join(import.meta.dir, "..", "extra", "banner.mjs"));
const unnormalized = await import([import.meta.dir, "..", "extra", "banner.mjs"].join("/"));
return (
<ul>
<li>{joined.banner}</li>
<li>{joined.detail}</li>
<li>{joined === unnormalized ? "one module instance" : "two module instances"}</li>
</ul>
);
}`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--app", "./src/index.tsx"],
cwd: dir,
env: bunEnv,
stdout: "ignore",
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
Comment thread
claude[bot] marked this conversation as resolved.
expect(stderr).not.toContain("error:");
expect(exitCode).toBe(0);

const html = await Bun.file(path.join(dir, "dist", "index.html")).text();
expect(html).toContain("<li>read from disk while rendering</li>");
expect(html).toContain("<li>resolved relative to the file on disk</li>");
expect(html).toContain("<li>one module instance</li>");
});
});
Loading