diff --git a/CHANGELOG.md b/CHANGELOG.md index c35d4ef96b58..f3e73f501a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/runtime/nodejs-compat.mdx b/docs/runtime/nodejs-compat.mdx index 68611635b4dd..3c53bc1cfa80 100644 --- a/docs/runtime/nodejs-compat.mdx +++ b/docs/runtime/nodejs-compat.mdx @@ -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. diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 5e47c42b2d71..0036edda3316 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -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(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); diff --git a/test/js/bun/resolve/import-query.test.ts b/test/js/bun/resolve/import-query.test.ts index 346ac87402b5..30970488d043 100644 --- a/test/js/bun/resolve/import-query.test.ts +++ b/test/js/bun/resolve/import-query.test.ts @@ -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 () => {