Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
ee1f9ec214 trigger ci
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-15 07:24:29 +00:00
Claude Bot
f2604e1b3d fix: include import type attribute in module cache key
Dynamic imports with different `with { type }` attributes were being
incorrectly cached together because the module cache key only included
the resolved path, not the type attribute.

For example:
  await import("./index.html");  // HTMLBundle
  await import("./index.html", { with: { type: "file" } });  // should be string

Both imports would return the same cached result (whichever was loaded
first) because they used the same cache key.

This fix extracts the type attribute before creating the cache key and
includes it in the key using a `#type=` suffix. This ensures that
imports with different type attributes are cached separately.
2025-12-14 22:03:15 +00:00
2 changed files with 161 additions and 23 deletions

View File

@@ -3237,37 +3237,46 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* j
return promise->rejectWithCaughtException(globalObject, scope);
}
if (queryString.len == 0) {
// Extract type attribute from import options BEFORE creating the cache key.
// This ensures that imports with different type attributes are cached separately.
// e.g., import("./foo.html") and import("./foo.html", { with: { type: "file" } })
// should be cached as different modules.
String typeAttributeForKey;
if (parameters && parameters.isObject()) {
auto* object = parameters.toObject(globalObject);
auto withObject = object->getIfPropertyExists(globalObject, vm.propertyNames->withKeyword);
if (!scope.exception() && withObject && withObject.isObject()) {
auto* with = jsCast<JSObject*>(withObject);
auto type = with->getIfPropertyExists(globalObject, vm.propertyNames->type);
if (!scope.exception() && type && type.isString()) {
typeAttributeForKey = type.toWTFString(globalObject);
parameters = JSC::JSScriptFetchParameters::create(vm, ScriptFetchParameters::create(typeAttributeForKey));
}
}
if (scope.exception()) [[unlikely]] {
moduleNameZ.deref();
sourceOriginZ.deref();
return JSC::JSInternalPromise::rejectedPromiseWithCaughtException(globalObject, scope);
}
}
// Build the cache key, including the type attribute if present.
// The format is: "resolved_path" or "resolved_path?query" or "resolved_path#type=attr"
// or "resolved_path?query#type=attr"
if (queryString.len == 0 && typeAttributeForKey.isEmpty()) {
resolvedIdentifier = JSC::Identifier::fromString(vm, resolved.result.value.toWTFString());
} else {
} else if (typeAttributeForKey.isEmpty()) {
resolvedIdentifier = JSC::Identifier::fromString(vm, makeString(resolved.result.value.toWTFString(BunString::ZeroCopy), Zig::toString(queryString)));
} else if (queryString.len == 0) {
resolvedIdentifier = JSC::Identifier::fromString(vm, makeString(resolved.result.value.toWTFString(BunString::ZeroCopy), "#type="_s, typeAttributeForKey));
} else {
resolvedIdentifier = JSC::Identifier::fromString(vm, makeString(resolved.result.value.toWTFString(BunString::ZeroCopy), Zig::toString(queryString), "#type="_s, typeAttributeForKey));
}
moduleNameZ.deref();
sourceOriginZ.deref();
}
// This gets passed through the "parameters" argument to moduleLoaderFetch.
// Therefore, we modify it in place.
if (parameters && parameters.isObject()) {
auto* object = parameters.toObject(globalObject);
auto withObject = object->getIfPropertyExists(globalObject, vm.propertyNames->withKeyword);
RETURN_IF_EXCEPTION(scope, {});
if (withObject) {
if (withObject.isObject()) {
auto* with = jsCast<JSObject*>(withObject);
auto type = with->getIfPropertyExists(globalObject, vm.propertyNames->type);
RETURN_IF_EXCEPTION(scope, {});
if (type) {
if (type.isString()) {
const auto typeString = type.toWTFString(globalObject);
parameters = JSC::JSScriptFetchParameters::create(vm, ScriptFetchParameters::create(typeString));
}
}
}
}
}
auto result = JSC::importModule(globalObject, resolvedIdentifier,
JSC::jsUndefined(), parameters, jsUndefined());
if (scope.exception()) [[unlikely]] {

View File

@@ -0,0 +1,129 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/19834
// HTML bundle imports are cached incorrectly without honoring `with { type }` value
test("issue #19834 - HTML imports with different `with { type }` should not be cached together", async () => {
using dir = tempDir("issue-19834", {
"index.html": `<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>`,
"test.ts": `
// First import as HTMLBundle (default behavior)
const htmlBundle = await import("./index.html").then(v => v.default);
// Second import with { type: "file" } should return a string path
const htmlFile = await import("./index.html", { with: { type: "file" } }).then(v => v.default);
// Output the types and values for verification
console.log("HTMLBundle type:", typeof htmlBundle);
console.log("HTMLBundle is object:", typeof htmlBundle === "object" && htmlBundle !== null);
console.log("HTMLBundle has index:", "index" in (htmlBundle ?? {}));
console.log("File type:", typeof htmlFile);
console.log("File is string:", typeof htmlFile === "string");
// The critical check: these should be different types
if (typeof htmlBundle === typeof htmlFile) {
console.log("BUG: Both imports returned the same type!");
process.exit(1);
}
console.log("SUCCESS: Different types returned for different import attributes");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
console.log("stdout:", stdout);
console.log("stderr:", stderr);
// When the bug is fixed:
// - htmlBundle should be an object (HTMLBundle with index property)
// - htmlFile should be a string (file path)
expect(stdout).toContain("HTMLBundle type: object");
expect(stdout).toContain("HTMLBundle is object: true");
expect(stdout).toContain("HTMLBundle has index: true");
expect(stdout).toContain("File type: string");
expect(stdout).toContain("File is string: true");
expect(stdout).toContain("SUCCESS: Different types returned for different import attributes");
expect(exitCode).toBe(0);
});
// Test the reverse order (file first, then bundle)
test("issue #19834 - reverse order: file import first, then HTMLBundle", async () => {
using dir = tempDir("issue-19834-reverse", {
"index.html": `<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>`,
"test.ts": `
// First import with { type: "file" } should return a string path
const htmlFile = await import("./index.html", { with: { type: "file" } }).then(v => v.default);
// Second import as HTMLBundle (default behavior)
const htmlBundle = await import("./index.html").then(v => v.default);
// Output the types and values for verification
console.log("File type:", typeof htmlFile);
console.log("File is string:", typeof htmlFile === "string");
console.log("HTMLBundle type:", typeof htmlBundle);
console.log("HTMLBundle is object:", typeof htmlBundle === "object" && htmlBundle !== null);
console.log("HTMLBundle has index:", "index" in (htmlBundle ?? {}));
// The critical check: these should be different types
if (typeof htmlBundle === typeof htmlFile) {
console.log("BUG: Both imports returned the same type!");
process.exit(1);
}
console.log("SUCCESS: Different types returned for different import attributes");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
console.log("stdout:", stdout);
console.log("stderr:", stderr);
// When the bug is fixed:
// - htmlFile should be a string (file path)
// - htmlBundle should be an object (HTMLBundle with index property)
expect(stdout).toContain("File type: string");
expect(stdout).toContain("File is string: true");
expect(stdout).toContain("HTMLBundle type: object");
expect(stdout).toContain("HTMLBundle is object: true");
expect(stdout).toContain("HTMLBundle has index: true");
expect(stdout).toContain("SUCCESS: Different types returned for different import attributes");
expect(exitCode).toBe(0);
});