Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
a687ed057f fix: allow onLoad plugins to pass through to next plugin
When an onLoad plugin returns an object without a 'contents' field,
it now passes control to the next matching plugin instead of throwing
an error. This allows plugins to observe/analyze files without claiming
ownership, fixing issues where plugins like tailwind need to scan HTML
files but other plugins also need to process them.

Fixes #22330

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 09:03:41 +00:00
2 changed files with 180 additions and 0 deletions

View File

@@ -558,6 +558,11 @@ export function runOnLoadPlugins(
}
}
// Allow plugins to return an object without contents to continue to the next plugin
if (contents === undefined) {
continue;
}
if (!(typeof contents === "string") && !$isTypedArrayView(contents)) {
throw new TypeError('onLoad plugins must return an object with "contents" as a string or Uint8Array');
}

View File

@@ -0,0 +1,175 @@
import { test, expect } from "bun:test";
import { bunExe, bunEnv, tempDirWithFiles } from "harness";
import path from "path";
test("onLoad plugins can pass through to next plugin by returning undefined contents", async () => {
const dir = tempDirWithFiles("plugin-passthrough", {
"index.html": `<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Test</h1>
</body>
</html>`,
"styles.css": `body { color: red; }`,
"build.js": `
let observerCalled = false;
let processorCalled = false;
const observerPlugin = {
name: "observer",
setup(build) {
build.onLoad({ filter: /\\.html$/ }, async (args) => {
observerCalled = true;
console.log("[observer] called");
// Return object without contents to pass through
return {};
});
}
};
const processorPlugin = {
name: "processor",
setup(build) {
build.onLoad({ filter: /\\.html$/ }, async (args) => {
processorCalled = true;
console.log("[processor] called");
return {
contents: await Bun.file(args.path).text(),
loader: "html"
};
});
}
};
const result = await Bun.build({
entrypoints: ["./index.html"],
outdir: "./dist",
plugins: [observerPlugin, processorPlugin]
});
if (!result.success) {
console.error("Build failed:", result.logs);
process.exit(1);
}
if (!observerCalled) {
console.error("Observer plugin was not called");
process.exit(1);
}
if (!processorCalled) {
console.error("Processor plugin was not called");
process.exit(1);
}
console.log("SUCCESS: Both plugins were called");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build.js"],
env: bunEnv,
cwd: 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(stdout).toContain("[observer] called");
expect(stdout).toContain("[processor] called");
expect(stdout).toContain("SUCCESS: Both plugins were called");
});
test("onLoad plugin returning contents stops subsequent plugins", async () => {
const dir = tempDirWithFiles("plugin-blocking", {
"index.html": `<!DOCTYPE html>
<html>
<body>
<h1>Test</h1>
</body>
</html>`,
"build.js": `
let firstCalled = false;
let secondCalled = false;
const firstPlugin = {
name: "first",
setup(build) {
build.onLoad({ filter: /\\.html$/ }, async (args) => {
firstCalled = true;
console.log("[first] called and returning contents");
return {
contents: await Bun.file(args.path).text(),
loader: "html"
};
});
}
};
const secondPlugin = {
name: "second",
setup(build) {
build.onLoad({ filter: /\\.html$/ }, async (args) => {
secondCalled = true;
console.log("[second] called");
return {
contents: "should not get here",
loader: "html"
};
});
}
};
const result = await Bun.build({
entrypoints: ["./index.html"],
outdir: "./dist",
plugins: [firstPlugin, secondPlugin]
});
if (!result.success) {
console.error("Build failed:", result.logs);
process.exit(1);
}
if (!firstCalled) {
console.error("First plugin was not called");
process.exit(1);
}
if (secondCalled) {
console.error("ERROR: Second plugin should not have been called");
process.exit(1);
}
console.log("SUCCESS: Only first plugin was called");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build.js"],
env: bunEnv,
cwd: 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(stdout).toContain("[first] called and returning contents");
expect(stdout).not.toContain("[second] called");
expect(stdout).toContain("SUCCESS: Only first plugin was called");
});