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
10 changed files with 161 additions and 533 deletions

View File

@@ -36,7 +36,6 @@ pub const dns = @import("./api/bun/dns.zig");
pub const FFI = @import("./api/ffi.zig").FFI;
pub const HTMLRewriter = @import("./api/html_rewriter.zig");
pub const FileSystemRouter = @import("./api/filesystem_router.zig").FileSystemRouter;
pub const Directory = @import("./api/Directory.zig");
pub const Glob = @import("./api/glob.zig");
pub const H2FrameParser = @import("./api/bun/h2_frame_parser.zig").H2FrameParser;
pub const JSBundler = @import("./api/JSBundler.zig").JSBundler;

View File

@@ -17,7 +17,6 @@ pub const BunObject = struct {
pub const createParsedShellScript = toJSCallback(bun.shell.ParsedShellScript.createParsedShellScript);
pub const createShellInterpreter = toJSCallback(bun.shell.Interpreter.createShellInterpreter);
pub const deflateSync = toJSCallback(JSZlib.deflateSync);
pub const dir = toJSCallback(Bun.constructDirectory);
pub const file = toJSCallback(WebCore.Blob.constructBunFile);
pub const gunzipSync = toJSCallback(JSZlib.gunzipSync);
pub const gzipSync = toJSCallback(JSZlib.gzipSync);
@@ -51,7 +50,6 @@ pub const BunObject = struct {
// --- Lazy property callbacks ---
pub const CryptoHasher = toJSLazyPropertyCallback(Crypto.CryptoHasher.getter);
pub const CSRF = toJSLazyPropertyCallback(Bun.getCSRFObject);
pub const Directory = toJSLazyPropertyCallback(Bun.getDirectoryConstructor);
pub const FFI = toJSLazyPropertyCallback(Bun.FFIObject.getter);
pub const FileSystemRouter = toJSLazyPropertyCallback(Bun.getFileSystemRouter);
pub const Glob = toJSLazyPropertyCallback(Bun.getGlobConstructor);
@@ -117,7 +115,6 @@ pub const BunObject = struct {
// --- Lazy property callbacks ---
@export(&BunObject.CryptoHasher, .{ .name = lazyPropertyCallbackName("CryptoHasher") });
@export(&BunObject.CSRF, .{ .name = lazyPropertyCallbackName("CSRF") });
@export(&BunObject.Directory, .{ .name = lazyPropertyCallbackName("Directory") });
@export(&BunObject.FFI, .{ .name = lazyPropertyCallbackName("FFI") });
@export(&BunObject.FileSystemRouter, .{ .name = lazyPropertyCallbackName("FileSystemRouter") });
@export(&BunObject.MD4, .{ .name = lazyPropertyCallbackName("MD4") });
@@ -156,7 +153,6 @@ pub const BunObject = struct {
@export(&BunObject.createParsedShellScript, .{ .name = callbackName("createParsedShellScript") });
@export(&BunObject.createShellInterpreter, .{ .name = callbackName("createShellInterpreter") });
@export(&BunObject.deflateSync, .{ .name = callbackName("deflateSync") });
@export(&BunObject.dir, .{ .name = callbackName("dir") });
@export(&BunObject.file, .{ .name = callbackName("file") });
@export(&BunObject.gunzipSync, .{ .name = callbackName("gunzipSync") });
@export(&BunObject.gzipSync, .{ .name = callbackName("gzipSync") });
@@ -1273,16 +1269,6 @@ pub fn getGlobConstructor(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc
return jsc.API.Glob.js.getConstructor(globalThis);
}
pub fn getDirectoryConstructor(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
return api.Directory.js.getConstructor(globalThis);
}
/// Bun.dir(path) - creates a lazy Directory handle
pub fn constructDirectory(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const dir = try api.Directory.constructor(globalObject, callframe);
return dir.toJS(globalObject);
}
pub fn getS3ClientConstructor(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
return jsc.WebCore.S3Client.js.getConstructor(globalThis);
}

View File

@@ -1,30 +0,0 @@
import { define } from "../../codegen/class-definitions";
export default [
define({
name: "Directory",
construct: true,
finalize: true,
configurable: false,
klass: {},
JSType: "0b11101110",
proto: {
path: {
getter: "getPath",
cache: true,
},
name: {
getter: "getName",
cache: true,
},
files: {
fn: "files",
length: 0,
},
filesSync: {
fn: "filesSync",
length: 0,
},
},
}),
];

View File

@@ -1,298 +0,0 @@
const Directory = @This();
const std = @import("std");
const bun = @import("bun");
const jsc = bun.jsc;
const strings = bun.strings;
const Environment = bun.Environment;
pub const js = jsc.Codegen.JSDirectory;
pub const toJS = js.toJS;
pub const fromJS = js.fromJS;
pub const fromJSDirect = js.fromJSDirect;
const DirIterator = bun.DirIterator;
const Dirent = bun.jsc.Node.Dirent;
/// The path to the directory. This is stored as a string and the directory
/// is NOT opened until files() or filesSync() is called - this is the lazy
/// loading pattern similar to Bun.file().
path: bun.String,
/// Construct a Directory from JavaScript arguments.
/// Called when: `new Bun.Directory(path)` or `Bun.dir(path)`
pub fn constructor(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!*Directory {
const alloc = bun.default_allocator;
const arguments = callframe.arguments_old(1).slice();
if (arguments.len == 0) {
return globalObject.throwInvalidArguments("Expected directory path string", .{});
}
const path_arg = arguments[0];
if (!path_arg.isString()) {
return globalObject.throwInvalidArgumentTypeValue("path", "string", path_arg);
}
var path_string = try bun.String.fromJS(path_arg, globalObject);
if (path_string.isEmpty()) {
return globalObject.throwInvalidArguments("Path cannot be empty", .{});
}
// Store the path - we don't open the directory yet (lazy loading)
const dir = bun.handleOom(alloc.create(Directory));
dir.* = .{ .path = path_string };
return dir;
}
/// Called when the object is garbage collected
pub fn finalize(this: *Directory) callconv(.c) void {
const alloc = bun.default_allocator;
this.path.deref();
alloc.destroy(this);
}
/// Get the path property
pub fn getPath(this: *Directory, globalObject: *jsc.JSGlobalObject) jsc.JSValue {
return this.path.toJS(globalObject);
}
/// Get the name property (basename of the directory)
pub fn getName(this: *Directory, globalObject: *jsc.JSGlobalObject) jsc.JSValue {
const path_slice = this.path.toUTF8(bun.default_allocator);
defer path_slice.deinit();
const basename = std.fs.path.basename(path_slice.slice());
return bun.String.cloneUTF8(basename).toJS(globalObject);
}
/// Async version of files() - returns Promise<Dirent[]>
pub fn files(this: *Directory, globalObject: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const vm = globalObject.bunVM();
// Create a strong reference to the path for the async task
this.path.ref();
var task = FilesTask.new(.{
.path = this.path,
.vm = vm,
});
task.promise = jsc.JSPromise.Strong.init(globalObject);
task.any_task = jsc.AnyTask.New(FilesTask, &FilesTask.runFromJS).init(task);
task.ref.ref(vm);
jsc.WorkPool.schedule(&task.task);
return task.promise.value();
}
/// Sync version of files() - returns Dirent[]
pub fn filesSync(this: *Directory, globalObject: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const path_slice = this.path.toUTF8(bun.default_allocator);
defer path_slice.deinit();
return readDirectoryEntries(globalObject, path_slice.slice());
}
/// Read directory entries synchronously and return as JS array of Dirent objects
fn readDirectoryEntries(globalObject: *jsc.JSGlobalObject, path: []const u8) bun.JSError!jsc.JSValue {
const path_z = bun.default_allocator.dupeZ(u8, path) catch return globalObject.throw("Out of memory", .{});
defer bun.default_allocator.free(path_z);
// Open the directory
const flags = bun.O.DIRECTORY | bun.O.RDONLY;
const fd = switch (bun.sys.open(path_z, flags, 0)) {
.err => |err| {
const js_err = err.withPath(path).toJS(globalObject);
return globalObject.throwValue(js_err);
},
.result => |fd_result| fd_result,
};
defer fd.close();
// Use the directory iterator
var iterator = DirIterator.iterate(fd, .u8);
// Collect entries
var entries = std.ArrayListUnmanaged(Dirent){};
defer {
for (entries.items) |*item| {
item.deref();
}
entries.deinit(bun.default_allocator);
}
var dirent_path = bun.String.cloneUTF8(path);
defer dirent_path.deref();
var entry = iterator.next();
while (switch (entry) {
.err => |err| {
const js_err = err.withPath(path).toJS(globalObject);
return globalObject.throwValue(js_err);
},
.result => |ent| ent,
}) |current| : (entry = iterator.next()) {
const utf8_name = current.name.slice();
dirent_path.ref();
entries.append(bun.default_allocator, .{
.name = bun.String.cloneUTF8(utf8_name),
.path = dirent_path,
.kind = current.kind,
}) catch return globalObject.throw("Out of memory", .{});
}
// Convert to JS array
var array = try jsc.JSValue.createEmptyArray(globalObject, entries.items.len);
var previous_jsstring: ?*jsc.JSString = null;
for (entries.items, 0..) |*item, i| {
const js_dirent = try item.toJS(globalObject, &previous_jsstring);
try array.putIndex(globalObject, @truncate(i), js_dirent);
}
return array;
}
/// Async task for reading directory entries
const FilesTask = struct {
task: jsc.WorkPoolTask = .{ .callback = &workPoolCallback },
promise: jsc.JSPromise.Strong = .{},
vm: *jsc.VirtualMachine,
path: bun.String,
any_task: jsc.AnyTask = undefined,
ref: bun.Async.KeepAlive = .{},
result: Result = undefined,
const Result = union(enum) {
success: []Dirent,
err: bun.sys.Error,
};
pub const new = bun.TrivialNew(@This());
fn workPoolCallback(task_ptr: *jsc.WorkPoolTask) void {
const this: *FilesTask = @fieldParentPtr("task", task_ptr);
defer this.vm.enqueueTaskConcurrent(jsc.ConcurrentTask.create(this.any_task.task()));
const path_slice = this.path.toUTF8(bun.default_allocator);
defer path_slice.deinit();
const path_z = bun.default_allocator.dupeZ(u8, path_slice.slice()) catch {
this.result = .{ .err = bun.sys.Error.fromCode(.NOMEM, .open) };
return;
};
defer bun.default_allocator.free(path_z);
// Open the directory
const flags = bun.O.DIRECTORY | bun.O.RDONLY;
const fd = switch (bun.sys.open(path_z, flags, 0)) {
.err => |err| {
this.result = .{ .err = err };
return;
},
.result => |fd_result| fd_result,
};
defer fd.close();
// Use the directory iterator
var iterator = DirIterator.iterate(fd, .u8);
// Collect entries
var entries = std.ArrayListUnmanaged(Dirent){};
var dirent_path = bun.String.cloneUTF8(path_slice.slice());
defer dirent_path.deref();
var entry = iterator.next();
while (switch (entry) {
.err => |err| {
for (entries.items) |*item| {
item.deref();
}
entries.deinit(bun.default_allocator);
this.result = .{ .err = err };
return;
},
.result => |ent| ent,
}) |current| : (entry = iterator.next()) {
const utf8_name = current.name.slice();
dirent_path.ref();
entries.append(bun.default_allocator, .{
.name = bun.String.cloneUTF8(utf8_name),
.path = dirent_path,
.kind = current.kind,
}) catch {
for (entries.items) |*item| {
item.deref();
}
entries.deinit(bun.default_allocator);
this.result = .{ .err = bun.sys.Error.fromCode(.NOMEM, .open) };
return;
};
}
this.result = .{ .success = entries.toOwnedSlice(bun.default_allocator) catch &.{} };
}
pub fn runFromJS(this: *FilesTask) bun.JSTerminated!void {
defer this.deinit();
if (this.vm.isShuttingDown()) {
return;
}
const globalObject = this.vm.global;
const promise = this.promise.swap();
switch (this.result) {
.err => |err| {
const js_err = err.toJS(globalObject);
try promise.reject(globalObject, js_err);
},
.success => |entries| {
defer bun.default_allocator.free(entries);
// Convert to JS array
var array = jsc.JSValue.createEmptyArray(globalObject, entries.len) catch {
for (entries) |*item| {
@constCast(item).deref();
}
try promise.reject(globalObject, globalObject.createErrorInstance("Out of memory", .{}));
return;
};
var previous_jsstring: ?*jsc.JSString = null;
for (entries, 0..) |*item, i| {
const js_dirent = @constCast(item).toJS(globalObject, &previous_jsstring) catch {
for (entries[i..]) |*remaining| {
@constCast(remaining).deref();
}
// An exception is already pending from toJS, so we just return
return error.JSTerminated;
};
array.putIndex(globalObject, @truncate(i), js_dirent) catch {
for (entries[i + 1 ..]) |*remaining| {
@constCast(remaining).deref();
}
return error.JSTerminated;
};
}
try promise.resolve(globalObject, array);
},
}
}
fn deinit(this: *FilesTask) void {
this.ref.unref(this.vm);
this.path.deref();
this.promise.deinit();
bun.destroy(this);
}
};
comptime {
_ = js;
}

View File

@@ -5,7 +5,6 @@
#define FOR_EACH_GETTER(macro) \
macro(CSRF) \
macro(CryptoHasher) \
macro(Directory) \
macro(FFI) \
macro(FileSystemRouter) \
macro(Glob) \
@@ -45,7 +44,6 @@
macro(createParsedShellScript) \
macro(createShellInterpreter) \
macro(deflateSync) \
macro(dir) \
macro(file) \
macro(fs) \
macro(gc) \

View File

@@ -742,8 +742,6 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
deepEquals functionBunDeepEquals DontDelete|Function 2
deepMatch functionBunDeepMatch DontDelete|Function 2
deflateSync BunObject_callback_deflateSync DontDelete|Function 1
dir BunObject_callback_dir DontDelete|Function 1
Directory BunObject_lazyPropCb_wrap_Directory DontDelete|PropertyCallback
dns constructDNSObject ReadOnly|DontDelete|PropertyCallback
enableANSIColors BunObject_lazyPropCb_wrap_enableANSIColors DontDelete|PropertyCallback
env constructEnvObject ReadOnly|DontDelete|PropertyCallback

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

@@ -24,7 +24,6 @@ pub const Classes = struct {
pub const ExpectTypeOf = jsc.Expect.ExpectTypeOf;
pub const ScopeFunctions = jsc.Jest.bun_test.ScopeFunctions;
pub const DoneCallback = jsc.Jest.bun_test.DoneCallback;
pub const Directory = api.Directory;
pub const FileSystemRouter = api.FileSystemRouter;
pub const Glob = api.Glob;
pub const ShellInterpreter = api.Shell.Interpreter;

View File

@@ -1,162 +0,0 @@
import { describe, expect, test } from "bun:test";
import { tempDir } from "harness";
import * as fs from "node:fs";
import * as path from "node:path";
describe("Bun.dir()", () => {
test("creates a lazy Directory object", () => {
const dir = Bun.dir("/tmp");
expect(dir).toBeDefined();
expect(dir).toBeInstanceOf(Bun.Directory);
});
test("has path property", () => {
const dir = Bun.dir("/tmp");
expect(dir.path).toBe("/tmp");
});
test("has name property (basename)", () => {
const dir = Bun.dir("/some/nested/folder");
expect(dir.name).toBe("folder");
});
test("is lazy - doesn't open directory until files() is called", () => {
// This should NOT throw even though the path doesn't exist
const dir = Bun.dir("/this/path/does/not/exist");
expect(dir.path).toBe("/this/path/does/not/exist");
expect(dir.name).toBe("exist");
});
test("throws when path is not a string", () => {
// @ts-expect-error - intentionally passing wrong type
expect(() => Bun.dir(123)).toThrow();
// @ts-expect-error - intentionally passing wrong type
expect(() => Bun.dir(null)).toThrow();
});
test("throws when path is empty", () => {
expect(() => Bun.dir("")).toThrow();
});
});
describe("Bun.Directory constructor", () => {
test("can be constructed with new", () => {
const dir = new Bun.Directory("/tmp");
expect(dir).toBeInstanceOf(Bun.Directory);
expect(dir.path).toBe("/tmp");
});
});
describe("Directory.filesSync()", () => {
test("returns array of Dirent objects", () => {
using dir_path = tempDir("bun-dir-test", {
"file1.txt": "hello",
"file2.txt": "world",
});
// Create subdirectory manually
fs.mkdirSync(path.join(String(dir_path), "subdir"), { recursive: true });
const dir = Bun.dir(String(dir_path));
const entries = dir.filesSync();
expect(Array.isArray(entries)).toBe(true);
expect(entries.length).toBe(3);
// Check that entries are Dirent-like objects
for (const entry of entries) {
expect(typeof entry.name).toBe("string");
expect(typeof entry.isFile).toBe("function");
expect(typeof entry.isDirectory).toBe("function");
}
// Check specific entries
const names = entries.map(e => e.name).sort();
expect(names).toEqual(["file1.txt", "file2.txt", "subdir"]);
// Check file types
const file1 = entries.find(e => e.name === "file1.txt");
expect(file1?.isFile()).toBe(true);
expect(file1?.isDirectory()).toBe(false);
const subdir = entries.find(e => e.name === "subdir");
expect(subdir?.isDirectory()).toBe(true);
expect(subdir?.isFile()).toBe(false);
});
test("throws for non-existent directory", () => {
const dir = Bun.dir("/this/path/definitely/does/not/exist");
expect(() => dir.filesSync()).toThrow();
});
test("returns empty array for empty directory", () => {
using dir_path = tempDir("bun-dir-empty-test", {});
const dir = Bun.dir(String(dir_path));
const entries = dir.filesSync();
expect(Array.isArray(entries)).toBe(true);
expect(entries.length).toBe(0);
});
});
describe("Directory.files()", () => {
test("returns Promise that resolves to array of Dirent objects", async () => {
using dir_path = tempDir("bun-dir-async-test", {
"async1.txt": "async content 1",
"async2.txt": "async content 2",
});
const dir = Bun.dir(String(dir_path));
const promise = dir.files();
expect(promise).toBeInstanceOf(Promise);
const entries = await promise;
expect(Array.isArray(entries)).toBe(true);
expect(entries.length).toBe(2);
const names = entries.map(e => e.name).sort();
expect(names).toEqual(["async1.txt", "async2.txt"]);
});
test("rejects for non-existent directory", async () => {
const dir = Bun.dir("/this/path/definitely/does/not/exist/async");
await expect(dir.files()).rejects.toThrow();
});
test("can be called multiple times on same Directory", async () => {
using dir_path = tempDir("bun-dir-multi-test", {
"multi.txt": "content",
});
const dir = Bun.dir(String(dir_path));
// Call files() multiple times
const [entries1, entries2] = await Promise.all([dir.files(), dir.files()]);
expect(entries1.length).toBe(1);
expect(entries2.length).toBe(1);
expect(entries1[0].name).toBe("multi.txt");
expect(entries2[0].name).toBe("multi.txt");
});
});
describe("Directory Dirent properties", () => {
test("Dirent has parentPath/path property", () => {
using dir_path = tempDir("bun-dir-parent-test", {
"test.txt": "test",
});
const dir = Bun.dir(String(dir_path));
const entries = dir.filesSync();
expect(entries.length).toBe(1);
const entry = entries[0];
// Check path/parentPath
expect(entry.path).toBe(String(dir_path));
expect(entry.parentPath).toBe(String(dir_path));
});
});

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);
});