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
6 changes: 5 additions & 1 deletion src/runtime/bake/bake_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ impl UserOptions {
}
};

if let Some(plugin_array) = config.get(global, "plugins")? {
if let Some(plugin_array) = get_optional_value(config, global, b"plugins")? {
bundler_options.parse_plugin_array(plugin_array, global)?;
}

Expand Down Expand Up @@ -312,6 +312,10 @@ impl SplitBundlerOptions {
plugin_array: JSValue,
global: &JSGlobalObject,
) -> JsResult<()> {
if !plugin_array.is_array() {
return Err(global.throw_invalid_arguments(format_args!("plugins must be an array")));
}
Comment thread
robobun marked this conversation as resolved.

// Create the Plugin and assign it to `opts.plugin` BEFORE iterating,
// so `plugins: []` still leaves `self.plugin = Some(_)`.
let plugin: NonNull<Plugin> = match self.plugin {
Expand Down
83 changes: 83 additions & 0 deletions test/bake/dev/plugins.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Plugin tests concern plugins in development mode.
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { devTest, minimalFramework } from "../bake-harness";

// Note: more in depth testing of plugins is done in test/bundler/bundler_plugin.test.ts
Expand Down Expand Up @@ -110,6 +112,87 @@ devTest("onResolve + onLoad virtual file", {
]);
},
});

// `app.plugins` and `framework.plugins` share one parser. It used to iterate whatever
// it was handed: a non-array was either accepted and ignored (123, an array-like) or
// rejected for its first "element" ("abc", a single plugin object passed without the
// surrounding array).
test.concurrent("app.plugins and framework.plugins must be arrays", async () => {
using dir = tempDir("bake-plugins-not-array", {
"server.ts": `export function render() { return new Response("unused"); }`,
"check.ts": `
const framework = {
fileSystemRouterTypes: [{ root: "routes", style: "nextjs-pages", serverEntryPoint: "./server.ts" }],
};
let setupCalls = 0;
const plugin = { name: "counted", setup() { setupCalls++; } };
const values = {
"string": "abc",
"single plugin object": plugin,
"number": 123,
"array-like": { length: 0 },
"array": [plugin],
"empty array": [],
"null": null,
};
const sites = {
"app.plugins": plugins => ({ framework, plugins }),
"framework.plugins": plugins => ({ framework: { ...framework, plugins } }),
};

const results = {};
for (const [site, app] of Object.entries(sites)) {
for (const [name, plugins] of Object.entries(values)) {
try {
const server = Bun.serve({
port: 0,
development: true,
app: app(plugins),
fetch: () => new Response(""),
});
server.stop(true);
results[site + " = " + name] = "accepted";
} catch (e) {
results[site + " = " + name] = e.message;
}
}
}
results.setupCalls = setupCalls;
console.log(JSON.stringify(results));
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "check.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]);

expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({
"app.plugins = string": "plugins must be an array",
"app.plugins = single plugin object": "plugins must be an array",
"app.plugins = number": "plugins must be an array",
"app.plugins = array-like": "plugins must be an array",
"app.plugins = array": "accepted",
"app.plugins = empty array": "accepted",
"app.plugins = null": "accepted",
"framework.plugins = string": "plugins must be an array",
"framework.plugins = single plugin object": "plugins must be an array",
"framework.plugins = number": "plugins must be an array",
"framework.plugins = array-like": "plugins must be an array",
"framework.plugins = array": "accepted",
"framework.plugins = empty array": "accepted",
"framework.plugins = null": "accepted",
// once per site, from the two "array" cases
setupCalls: 2,
});
expect(exitCode).toBe(0);
});

// devTest("onLoad with watchFile", {
// framework: minimalFramework,
// pluginFile: `
Expand Down
25 changes: 24 additions & 1 deletion test/bake/dev/production.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { existsSync } from "fs";
import { bunEnv, bunExe } from "harness";
import { bunEnv, bunExe, tempDir } from "harness";
import path from "path";
import { tempDirWithBakeDeps } from "../bake-harness";

Expand Down Expand Up @@ -329,6 +329,29 @@ export default function GettingStarted() {
}
});

// Same options parser as `Bun.serve({ app })` (dev/plugins.test.ts covers the value
// matrix); this used to build with the option silently ignored.
test("rejects a non-array plugins option", async () => {
using dir = tempDir("bake-production-plugins-not-array", {
"server.ts": `export function render() { return new Response("unused"); }`,
"bun.app.ts": `
export default {
app: {
framework: {
fileSystemRouterTypes: [{ root: "routes", style: "nextjs-pages", serverEntryPoint: "./server.ts" }],
},
plugins: 123,
},
};
`,
});

const { exitCode, stderr } = await Bun.$`${bunExe()} build --app`.cwd(String(dir)).env(bunEnv).throws(false);

expect(stderr.toString()).toContain("TypeError: plugins must be an array");
expect(exitCode).toBe(1);
});

test("client-side component with default import should work", async () => {
const dir = await tempDirWithBakeDeps("bake-production-client-import", {
"src/index.tsx": `export default { app: { framework: "react" } };`,
Expand Down
Loading