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
13 changes: 0 additions & 13 deletions src/bundler/Chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,6 @@ impl IntermediateOutput {
let unique_key_for_additional_files =
graph.input_files.items_unique_key_for_additional_file();
let mut relative_platform_buf = bun_paths::path_buffer_pool::get();
let mut file_path_buf = bun_paths::path_buffer_pool::get();
match self {
IntermediateOutput::Pieces(pieces) => {
let entry_point_chunks_for_scb = linker_graph.files.items_entry_point_chunk_index();
Expand Down Expand Up @@ -943,18 +942,6 @@ impl IntermediateOutput {
_ => unreachable!(),
};

// normalize windows paths to '/'
// The source slices are reachable only
// through `&Graph` / `&[Chunk]` here; materialising `&mut` from a
// shared-provenance pointer is UB regardless of whether the write
// happens. Copy into a pooled scratch buffer and normalise that.
let file_path: &[u8] = {
let n = file_path.len();
let dst = &mut file_path_buf[..n];
dst.copy_from_slice(file_path);
bun_paths::resolve_path::platform_to_posix_in_place::<u8>(dst);
dst
};
let cheap_normalizer = cheap_prefix_normalizer(
import_prefix,
if use_outdir_relative_path {
Expand Down
24 changes: 17 additions & 7 deletions src/bundler/HTMLImportManifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ use bun_collections::VecExt;
use bun_core::strings;
use bun_io::{FmtAdapter, Write};
use bun_js_printer::Encoding;
use bun_paths::resolve_path::relative_normalized;
use bun_paths::path_buffer_pool;
use bun_paths::resolve_path::{platform, platform_to_posix_in_place};
use bun_resolver::fs::FileSystem;

use crate::Graph::Graph;
Expand Down Expand Up @@ -197,6 +198,7 @@ pub fn write<W: Write + ?Sized>(
// Use the server-side public path here.
let public_path: &[u8] = &options.public_path;
let mut temp_buffer: Vec<u8> = Vec::new();
let mut input_buf = path_buffer_pool::get();

for ch in chunks.iter() {
if ch.entry_point.source_index() == browser_source_index && ch.entry_point.is_entry_point()
Expand Down Expand Up @@ -253,11 +255,16 @@ pub fn write<W: Write + ?Sized>(
let input: &[u8] = if !ch.entry_point.is_entry_point() {
b""
} else {
let path_for_key = relative_normalized::<bun_paths::platform::Posix, false>(
// `root_dir` and `source.path.text` are native absolute paths, so
// compute the relative path with host semantics and emit it as posix.
let len = bun_paths::resolve_path::relative_platform_buf::<platform::Auto, true>(
&mut **input_buf,
root_dir,
sources[ch.entry_point.source_index() as usize].path.text,
);
strings::remove_leading_dot_slash(path_for_key)
)
.len();
platform_to_posix_in_place::<u8>(&mut input_buf[..len]);
strings::remove_leading_dot_slash(&input_buf[..len])
};

let path: &[u8] = if inject_compiler_filesystem_prefix {
Expand Down Expand Up @@ -308,11 +315,14 @@ pub fn write<W: Write + ?Sized>(
}
first = false;

let path_for_key = relative_normalized::<bun_paths::platform::Posix, false>(
let len = bun_paths::resolve_path::relative_platform_buf::<platform::Auto, true>(
&mut **input_buf,
root_dir,
sources[source_index.get() as usize].path.text,
);
let path_for_key = strings::remove_leading_dot_slash(path_for_key);
)
.len();
platform_to_posix_in_place::<u8>(&mut input_buf[..len]);
let path_for_key = strings::remove_leading_dot_slash(&input_buf[..len]);

let path: &[u8] = if inject_compiler_filesystem_prefix {
temp_buffer.clear();
Expand Down
4 changes: 4 additions & 0 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4155,6 +4155,10 @@ pub mod bv2_impl {
template
.print(&mut v, !self.transpiler.options.compile)
.expect("oom");
// Same invariant as `Chunk.final_rel_path`: every consumer
// (chunk assembly, manifest JSON, standalone graph, HTTP
// routes) wants `/`, and Windows openat normalises `/` itself.
bun_paths::resolve_path::platform_to_posix_in_place::<u8>(&mut v);
v.into_boxed_slice()
};

Expand Down
16 changes: 2 additions & 14 deletions src/runtime/bake/production.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,7 @@ pub(super) fn write_sourcemap_to_disk(
let source_map_index = file.source_map_index;
debug_assert!(bundled_outputs[source_map_index as usize].output_kind == OutputKind::Sourcemap);

let without_prefix = if strings::has_prefix(&file.dest_path, b"./")
|| (cfg!(windows) && strings::has_prefix(&file.dest_path, b".\\"))
{
&file.dest_path[2..]
} else {
&file.dest_path[..]
};
let without_prefix = strings::remove_leading_dot_slash(&file.dest_path);

let mut key = Vec::with_capacity(6 + without_prefix.len());
write!(&mut key, "bake:/{}", BStr::new(without_prefix)).expect("infallible: in-memory write");
Expand Down Expand Up @@ -754,13 +748,7 @@ pub(super) fn build_with_vm(

match file.output_kind {
OutputKind::EntryPoint | OutputKind::Chunk => {
let without_prefix = if strings::has_prefix(&file.dest_path, b"./")
|| (cfg!(windows) && strings::has_prefix(&file.dest_path, b".\\"))
{
&file.dest_path[2..]
} else {
&file.dest_path[..]
};
let without_prefix = strings::remove_leading_dot_slash(&file.dest_path);

if let Some(entry_point_index) = file.entry_point_index {
if (entry_point_index as usize) < module_keys.len() {
Expand Down
10 changes: 0 additions & 10 deletions src/standalone_graph/StandaloneModuleGraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,16 +775,6 @@ pub(crate) fn to_bytes(

let dest_path = bun_core::strings::remove_leading_dot_slash(&output_file.dest_path);

// Windows: store the key with `/`. The template printer emits native
// `\` into `dest_path`, but `find_assume_standalone_path` normalizes
// lookups to `/`, so a `\` key would miss (ENOENT). `src/bundler/Chunk.rs`
// only normalizes a scratch copy, so we re-normalize here.
#[cfg(windows)]
let mut dest_path_buf = PathBuffer::uninit();
#[cfg(windows)]
let dest_path: &[u8] =
path::resolve_path::platform_to_posix_buf::<u8>(dest_path, &mut dest_path_buf);

let bytecode: StringPointer = 'brk: {
if output_file.bytecode_index != u32::MAX {
// Bytecode alignment for JSC bytecode cache deserialization.
Expand Down
47 changes: 46 additions & 1 deletion test/bundler/html-import-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { tempDirWithFiles } from "harness";
import { tempDir, tempDirWithFiles } from "harness";
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { itBundled } from "./expectBundled";
Expand Down Expand Up @@ -360,6 +360,51 @@ console.log("About manifest:", aboutHtml);
},
});

// The manifest is meant to be passed straight to a static-file server, so its
// `input` keys must be project-relative and every path must use `/` regardless
// of the host (no leaked build-machine absolute paths, no backslashes).
test("html-import/manifest-paths-are-posix-relative", async () => {
using cwd = tempDir("html-manifest-paths", {
"server.ts": `import m from "./page/index.html"; console.log(JSON.stringify(m));`,
"page/index.html": `<!doctype html><link rel="stylesheet" href="./s.css"><script type="module" src="./a.ts"></script>`,
"page/a.ts": `import icon from "./icon.txt" with { type: "file" }; console.log(icon);`,
"page/s.css": `body { color: red }`,
"page/icon.txt": `hi`,
});
const dir = String(cwd);

const out = join(dir, "out");
const r = await Bun.build({ entrypoints: [join(dir, "server.ts")], outdir: out, target: "bun", root: dir });
expect(r.success).toBe(true);
const js = readFileSync(join(out, "server.js"), "utf8");
const m = js.match(/__jsonParse\("(.+?)"\)/s)!;
const manifest = JSON.parse(JSON.parse('"' + m[1] + '"')) as {
index: string;
files: Array<{ input?: string; path: string; loader: string }>;
};

const absolute = /^(?:[A-Za-z]:|[\\/]|\.\.[\\/])/;
expect(manifest.index).not.toContain("\\");
expect(manifest.index).not.toMatch(absolute);

// Entry chunks are keyed by the entry HTML source, relative to `root`;
// the file-loader asset is keyed by its own source path.
expect(manifest.files.map(f => [f.loader, f.input])).toEqual([
["js", "page/index.html"],
["html", "page/index.html"],
["css", "page/index.html"],
["file", "page/icon.txt"],
]);
for (const f of manifest.files) {
expect(f.path).not.toContain("\\");
expect(f.path).not.toMatch(absolute);
}
// file-loader asset path comes from `additional_output_files[].dest_path`,
// which was the one emitted with native `\` before the fix.
const asset = manifest.files.find(f => f.loader === "file")!;
expect(asset.path).toStartWith("./");
});

// The HTML chunk's etag must change when only a referenced JS/CSS chunk
// changes; otherwise the browser 304s to a body that points at chunks the
// server no longer has.
Expand Down
Loading