Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,5 @@
- Support real byte-based `node:inspector` HeapProfiler allocation sampling with allocation-site trees, live profiles, and major/minor GC inclusion flags (requires the OpenClaw WebKit allocation sampler).
- Preserve built-in error constructor syntax and returning arrow frames, and use JSC syntax-selected call and property-read stack positions. Retain runtime callee parentheses and computed access, and map call, bracket, and template delimiters back to their original source. Continues the source-position fixes from [oven-sh/bun#35179](https://github.com/oven-sh/bun/pull/35179), [oven-sh/bun#37396](https://github.com/oven-sh/bun/pull/37396), and [oven-sh/bun#41580](https://github.com/oven-sh/bun/pull/41580). Thanks @robobun!
- Add position-preserving `module.stripTypeScriptTypes()` strip mode for tooling that analyzes TypeScript exports. Adapts [oven-sh/bun#35517](https://github.com/oven-sh/bun/pull/35517); thanks @cirospaciari!

- Keep query and fragment imports of compiled embedded modules on their canonical module record, including Windows and non-ASCII filenames.
2 changes: 2 additions & 0 deletions docs/runtime/nodejs-compat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ Unreadable selected package metadata also throws `ERR_INVALID_PACKAGE_CONFIG`, r

`import.meta.resolve()` validates the package scope of existing `.js`, `.ts`, and extensionless files. Missing-file URLs and `require.resolve()` defer that scope validation. Loading an already-resolved CommonJS file through the ESM loader preserves the original resolution mode.

Modules embedded by `bun build --compile` share one module record across query and fragment spellings of their path or file URL. Files loaded from the filesystem keep separate ESM identities for distinct suffixes.

Async imports of CommonJS files preserve package-scope interop. A `.cjs` file inside a `"type": "module"` package keeps `module.exports` as its default export and exposes its `__esModule` property, while the file still executes as CommonJS.

`registerHooks()` supports synchronous resolve/load chaining, source transformation, and deregistration. `nextResolve` honors parent URLs and custom conditions, and hooks preserve query/fragment module identities. Dynamic-import attributes, including attributes returned by resolve hooks, reach the matching load hook.
Expand Down
7 changes: 7 additions & 0 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3456,6 +3456,13 @@ static String fileURLSuffix(const URL& url)

static String resolvedModuleKey(const String& resolved, const String& suffix)
{
if (!suffix.isEmpty() && Bun__hasStandaloneModuleGraph()) {
auto path = resolved.utf8();
size_t canonicalLength = 0;
// Embedded modules have one canonical record, including when a URL supplied the suffix.
if (Bun__standaloneModuleKey(std::bit_cast<const Latin1Character*>(path.data()), path.length(), &canonicalLength))
return resolved;
}
// Keep literal '#' paths keyed like require() and moduleKeyFromFileURL; only '?' splits a module key.
if (isAbsolutePath(resolved) && resolved.find('?') != WTF::notFound)
return makeString(URL::fileURLWithFileSystemPath(resolved).string(), suffix);
Expand Down
51 changes: 51 additions & 0 deletions test/js/bun/resolve/import-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,57 @@ test.concurrent("a module embedded by bun build --compile loads with a ?query or
expect(exitCode).toBe(0);
});

test.concurrent("embedded module suffixes share identity for non-ASCII names", async () => {
using dir = tempDir("embedded-unicode-suffix", {
"café-日本語.mjs": `globalThis.embeddedEvaluations = (globalThis.embeddedEvaluations ?? 0) + 1; export default "embedded";`,
"entry.mjs": `
const relative = "./café-日本語.mjs";
const url = new URL(relative, import.meta.url).href;
const modules = [];
for (const specifier of [url, url + "?q=1", url + "#a", url + "?q=1#a", relative + "?q=1", relative + "#a"]) {
modules.push(await import(specifier));
}
console.log(JSON.stringify({
values: modules.map(module => module.default),
same: modules.every(module => module === modules[0]),
evaluations: globalThis.embeddedEvaluations,
}));
`,
"elsewhere/.keep": "",
});
const exe = path.join(String(dir), isWindows ? "app.exe" : "app");
{
await using build = Bun.spawn({
cmd: [bunExe(), "build", "--compile", "entry.mjs", "café-日本語.mjs", "--outfile", exe],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]);
expect({ stdout: exitCode === 0 ? "" : stdout, stderr: exitCode === 0 ? "" : stderr, exitCode }).toEqual({
stdout: "",
stderr: "",
exitCode: 0,
});
}
await using proc = Bun.spawn({
cmd: [exe],
env: bunEnv,
cwd: path.join(String(dir), "elsewhere"),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({
values: ["embedded", "embedded", "embedded", "embedded", "embedded", "embedded"],
same: true,
evaluations: 1,
});
expect(exitCode).toBe(0);
});

// Each suffix is its own module, evaluated in import order. import.meta.url keeps the suffix,
// and a module that carries one can still import its neighbours.
test.concurrent("file URL queries and fragments preserve module identity and import.meta.url", async () => {
Expand Down
Loading