diff --git a/src/bake/bake.zig b/src/bake/bake.zig index cd621eb3de4..237d5934f99 100644 --- a/src/bake/bake.zig +++ b/src/bake/bake.zig @@ -68,14 +68,17 @@ pub const UserOptions = struct { } if (try config.getOptional(global, "bundlerOptions", JSValue)) |js_options| { + if (!js_options.isObject()) { + return global.throwInvalidArguments("'bundlerOptions' must be an object", .{}); + } if (try js_options.getOptional(global, "server", JSValue)) |server_options| { - bundler_options.server = try BuildConfigSubset.fromJS(global, server_options); + bundler_options.server = try BuildConfigSubset.fromJS(global, server_options, "server"); } if (try js_options.getOptional(global, "client", JSValue)) |client_options| { - bundler_options.client = try BuildConfigSubset.fromJS(global, client_options); + bundler_options.client = try BuildConfigSubset.fromJS(global, client_options, "client"); } if (try js_options.getOptional(global, "ssr", JSValue)) |ssr_options| { - bundler_options.ssr = try BuildConfigSubset.fromJS(global, ssr_options); + bundler_options.ssr = try BuildConfigSubset.fromJS(global, ssr_options, "ssr"); } } @@ -202,9 +205,13 @@ const BuildConfigSubset = struct { minify_identifiers: ?bool = null, minify_whitespace: ?bool = null, - pub fn fromJS(global: *jsc.JSGlobalObject, js_options: JSValue) bun.JSError!BuildConfigSubset { + pub fn fromJS(global: *jsc.JSGlobalObject, js_options: JSValue, comptime property_name: []const u8) bun.JSError!BuildConfigSubset { var options = BuildConfigSubset{}; + if (!js_options.isObject()) { + return global.throwInvalidArguments("'bundlerOptions." ++ property_name ++ "' must be an object", .{}); + } + if (try js_options.getOptional(global, "sourcemap", JSValue)) |val| brk: { if (try bun.schema.api.SourceMapMode.fromJS(global, val)) |sourcemap| { options.source_map = sourcemap; @@ -215,13 +222,17 @@ const BuildConfigSubset = struct { } if (try js_options.getOptional(global, "minify", JSValue)) |minify_options| brk: { - if (minify_options.isBoolean() and minify_options.asBoolean()) { + if (minify_options.isBoolean()) { options.minify_syntax = minify_options.asBoolean(); options.minify_identifiers = minify_options.asBoolean(); options.minify_whitespace = minify_options.asBoolean(); break :brk; } + if (!minify_options.isObject()) { + return global.throwInvalidArguments("'bundlerOptions." ++ property_name ++ ".minify' must be a boolean or an object", .{}); + } + if (try minify_options.getBooleanLoose(global, "whitespace")) |value| { options.minify_whitespace = value; } diff --git a/test/bake/bundler-options-validation.test.ts b/test/bake/bundler-options-validation.test.ts new file mode 100644 index 00000000000..e2a6952c8b4 --- /dev/null +++ b/test/bake/bundler-options-validation.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from "bun:test"; + +describe("Bun.serve app.bundlerOptions validation", () => { + test("non-object bundlerOptions throws", () => { + expect(() => + Bun.serve({ + // @ts-expect-error + app: { bundlerOptions: 1225 }, + }), + ).toThrow("'bundlerOptions' must be an object"); + }); + + for (const key of ["server", "client", "ssr"] as const) { + test(`non-object bundlerOptions.${key} throws`, () => { + expect(() => + Bun.serve({ + // @ts-expect-error + app: { bundlerOptions: { [key]: 1225 } }, + }), + ).toThrow(`'bundlerOptions.${key}' must be an object`); + }); + + test(`non-object non-boolean bundlerOptions.${key}.minify throws`, () => { + expect(() => + Bun.serve({ + // @ts-expect-error + app: { bundlerOptions: { [key]: { minify: 1225 } } }, + }), + ).toThrow(`'bundlerOptions.${key}.minify' must be a boolean or an object`); + }); + + for (const minify of [true, false, { whitespace: true }]) { + test(`bundlerOptions.${key}.minify = ${JSON.stringify(minify)} is accepted`, () => { + expect(() => + Bun.serve({ + // @ts-expect-error + app: { bundlerOptions: { [key]: { minify } } }, + }), + ).not.toThrow(/must be/); + }); + } + } +});