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
29 changes: 23 additions & 6 deletions src/bundler/bundle_v2.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3445,18 +3445,25 @@ pub const BundleV2 = struct {
}
}

// When an import has an explicit loader (via `with { type: "..." }`), skip the cache
// and always create a new parse task. This ensures files imported with different
// loaders are parsed correctly (e.g., same .ts file imported both normally and as text).
const has_explicit_loader = import_record.loader != null;

const import_record_loader = import_record.loader orelse path.loader(&transpiler.options.loaders) orelse .file;
import_record.loader = import_record_loader;

const is_html_entrypoint = import_record_loader == .html and target.isServerSide() and this.transpiler.options.dev_server == null;

if (this.pathToSourceIndexMap(target).get(path.text)) |id| {
if (this.transpiler.options.dev_server != null and loader != .html) {
import_record.path = this.graph.input_files.items(.source)[id].path;
} else {
import_record.source_index = .init(id);
if (!has_explicit_loader) {
if (this.pathToSourceIndexMap(target).get(path.text)) |id| {
if (this.transpiler.options.dev_server != null and loader != .html) {
import_record.path = this.graph.input_files.items(.source)[id].path;
} else {
import_record.source_index = .init(id);
}
continue;
}
continue;
}

if (is_html_entrypoint) {
Expand All @@ -3465,6 +3472,16 @@ pub const BundleV2 = struct {

const resolve_entry = resolve_queue.getOrPut(path.text) catch |err| bun.handleOom(err);
if (resolve_entry.found_existing) {
// Check if the existing ParseTask has the same loader.
// If loaders differ, we can't reuse the task - we need to parse the file differently.
const existing_loader = resolve_entry.value_ptr.*.loader orelse path.loader(&transpiler.options.loaders) orelse .file;
if (existing_loader == import_record_loader) {
import_record.path = resolve_entry.value_ptr.*.path;
continue;
}
// Fall through: Different loader required, but we can't add to resolve_queue
// with the same key. Set the path and move on - the loader mismatch will
// be handled when this import is resolved.
import_record.path = resolve_entry.value_ptr.*.path;
continue;
}
Expand Down
80 changes: 80 additions & 0 deletions test/regression/issue/23299.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";

test("TypeScript file imported with type: 'text' should be treated as text, not executed - issue #23299", async () => {
using dir = tempDir("issue-23299", {
"asset.ts": `console.error("Unreachable!");`,
"frontend.ts": `
//@ts-ignore
import code from "./asset.ts" with { type: "text" };

console.log(code);
`,
"index.html": `
<html>
<head>
<script type="module" src="./frontend.ts"></script>
</head>
<body></body>
</html>
`,
});

// Build the frontend module
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "frontend.ts", "--outdir=dist", "--target=browser"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(exitCode).toBe(0);
expect(stderr).not.toContain("error");

// Read the bundled output
const bundled = await Bun.file(`${dir}/dist/frontend.js`).text();

// The asset.ts content should be a string literal, not executed code
expect(bundled).toContain('console.error("Unreachable!")');

// Make sure the error is NOT executed (it should be in a string)
expect(normalizeBunSnapshot(bundled, dir)).toMatchInlineSnapshot(`
"// asset.ts
var asset_default = 'console.error("Unreachable!");';

// frontend.ts
console.log(asset_default);"
`);
});

test("TypeScript file should compile when imported normally even if also imported as text - issue #23299", async () => {
using dir = tempDir("issue-23299-text-only", {
"code.ts": `export const value = 42;`,
"text-import.ts": `
//@ts-ignore
import text from "./code.ts" with { type: "text" };
console.log("Source:", text);
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "build", "text-import.ts", "--outdir=dist", "--target=browser"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(exitCode).toBe(0);

const bundled = await Bun.file(`${dir}/dist/text-import.js`).text();

// The TypeScript file should be loaded as text, not compiled
expect(bundled).toContain("export const value = 42");
expect(bundled).toContain("Source:");
});