Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
9fc8c99917 Add macro to TargetMatcher for consistency
Addresses CodeRabbit review comment - the 'macro' target should be in the
TargetMatcher along with other targets rather than handled as a special case.

This maintains the same behavior as before (macro -> bun_macro) but keeps
all target string matching in one place for consistency.
2025-10-29 23:54:01 +00:00
Claude Bot
46ac3d36fe Add --target=cloudflare support to bundler
Implements Cloudflare Workers target with export conditions matching Wrangler's behavior:
- workerd (first priority - Cloudflare's runtime)
- worker (second priority - generic web worker)
- browser (third priority - browser-compatible code)

This matches the official Wrangler bundler behavior as documented in:
https://github.com/cloudflare/workers-sdk/blob/main/packages/wrangler/src/deployment-bundle/bundle.ts#L51-L88

Like node and bun targets, cloudflare is a server-side target that:
- Uses isServerSide() = true
- Prefers module field over main in package.json
- Uses .esm output format (not .esm_ascii like bun target)

Changes:
- Added cloudflare to options.Target enum
- Added cloudflare to Api.Target enum
- Configured default export conditions for cloudflare target (workerd, worker, browser)
- Updated all switch statements to handle cloudflare case
- Refactored CLI argument parser to use ComptimeStringMap instead of ExactSizeMatcher
- Added comprehensive test suite in test/bundler/bundler_cloudflare.test.ts

Tests verify:
- workerd condition has highest priority
- Falls back to worker then browser conditions
- Falls back to default when no cloudflare-related conditions match
- Handles nested exports with conditions
- Works with multiple packages using different conditions
- Prefers module field like other server-side targets

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 23:24:18 +00:00
7 changed files with 264 additions and 12 deletions

View File

@@ -741,6 +741,9 @@ pub const api = struct {
/// bun_macro
bun_macro,
/// cloudflare
cloudflare,
_,
pub fn jsonStringify(self: @This(), writer: anytype) !void {

View File

@@ -375,6 +375,7 @@ pub noinline fn computeChunks(
.bun => "bun",
.node => "node",
.bun_macro => "macro",
.cloudflare => "cloudflare",
.bake_server_components_ssr => "ssr",
};
}

View File

@@ -911,7 +911,13 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
}
}
const TargetMatcher = strings.ExactSizeMatcher(8);
const TargetMatcher = bun.ComptimeStringMap(Api.Target, .{
.{ "browser", .browser },
.{ "node", .node },
.{ "bun", .bun },
.{ "cloudflare", .cloudflare },
.{ "macro", .bun_macro },
});
if (args.option("--target")) |_target| brk: {
if (comptime cmd == .BuildCommand) {
if (args.flag("--compile")) {
@@ -927,13 +933,7 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
}
}
opts.target = opts.target orelse switch (TargetMatcher.match(_target)) {
TargetMatcher.case("browser") => Api.Target.browser,
TargetMatcher.case("node") => Api.Target.node,
TargetMatcher.case("macro") => if (cmd == .BuildCommand) Api.Target.bun_macro else Api.Target.bun,
TargetMatcher.case("bun") => Api.Target.bun,
else => CLI.invalidTarget(&diag, _target),
};
opts.target = opts.target orelse TargetMatcher.get(_target) orelse CLI.invalidTarget(&diag, _target);
if (opts.target.? == .bun) {
ctx.debug.run_in_bun = opts.target.? == .bun;

View File

@@ -78,7 +78,7 @@ pub const Targets = struct {
}
}
return switch (target) {
.node, .bun => runtimeDefault(),
.node, .bun, .cloudflare => runtimeDefault(),
.browser, .bun_macro, .bake_server_components_ssr => browserDefault(),
};
}

View File

@@ -356,6 +356,7 @@ pub const Target = enum {
bun,
bun_macro,
node,
cloudflare,
/// This is used by bake.Framework.ServerComponents.separate_ssr_graph
bake_server_components_ssr,
@@ -366,6 +367,7 @@ pub const Target = enum {
.{ "bun_macro", .bun_macro },
.{ "macro", .bun_macro },
.{ "node", .node },
.{ "cloudflare", .cloudflare },
});
pub fn fromJS(global: *jsc.JSGlobalObject, value: jsc.JSValue) bun.JSError!?Target {
@@ -381,12 +383,13 @@ pub const Target = enum {
.browser => .browser,
.bun, .bake_server_components_ssr => .bun,
.bun_macro => .bun_macro,
.cloudflare => .cloudflare,
};
}
pub inline fn isServerSide(this: Target) bool {
return switch (this) {
.bun_macro, .node, .bun, .bake_server_components_ssr => true,
.bun_macro, .node, .bun, .bake_server_components_ssr, .cloudflare => true,
else => false,
};
}
@@ -416,7 +419,7 @@ pub const Target = enum {
return switch (target) {
.browser => .client,
.bake_server_components_ssr => .ssr,
.bun_macro, .bun, .node => .server,
.bun_macro, .bun, .node, .cloudflare => .server,
};
}
@@ -448,6 +451,7 @@ pub const Target = enum {
.browser => .browser,
.bun => .bun,
.bun_macro => .bun_macro,
.cloudflare => .cloudflare,
else => .browser,
};
}
@@ -495,6 +499,7 @@ pub const Target = enum {
array.set(Target.bun, &listd);
array.set(Target.bun_macro, &listd);
array.set(Target.bake_server_components_ssr, &listd);
array.set(Target.cloudflare, &listd);
// Original comment:
// The neutral target is for people that don't want esbuild to try to
@@ -528,6 +533,11 @@ pub const Target = enum {
"bun",
"node",
});
array.set(Target.cloudflare, &.{
"workerd",
"worker",
"browser",
});
break :brk array;
};

View File

@@ -660,7 +660,7 @@ pub const Transpiler = struct {
var writer = js_printer.BufferPrinter.init(buffer_writer);
output_file.size = switch (transpiler.options.target) {
.browser, .node => try transpiler.print(
.browser, .node, .cloudflare => try transpiler.print(
result,
*js_printer.BufferPrinter,
&writer,

View File

@@ -0,0 +1,238 @@
import { describe } from "bun:test";
import { itBundled } from "./expectBundled";
describe("bundler", () => {
itBundled("cloudflare/ExportsWorkerdCondition", {
files: {
"/Users/user/project/src/entry.js": `import 'pkg'; console.log('done');`,
"/Users/user/project/node_modules/pkg/package.json": /* json */ `
{
"exports": {
"workerd": "./workerd.js",
"worker": "./worker.js",
"browser": "./browser.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg/workerd.js": `console.log('workerd')`,
"/Users/user/project/node_modules/pkg/worker.js": `console.log('worker')`,
"/Users/user/project/node_modules/pkg/browser.js": `console.log('browser')`,
"/Users/user/project/node_modules/pkg/default.js": `console.log('default')`,
},
target: "cloudflare",
outfile: "/Users/user/project/out.js",
run: {
stdout: "workerd\ndone",
},
});
itBundled("cloudflare/ExportsWorkerFallback", {
files: {
"/Users/user/project/src/entry.js": `import 'pkg'; console.log('done');`,
"/Users/user/project/node_modules/pkg/package.json": /* json */ `
{
"exports": {
"worker": "./worker.js",
"browser": "./browser.js",
"node": "./node.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg/worker.js": `console.log('worker')`,
"/Users/user/project/node_modules/pkg/browser.js": `console.log('browser')`,
"/Users/user/project/node_modules/pkg/node.js": `console.log('node')`,
"/Users/user/project/node_modules/pkg/default.js": `console.log('default')`,
},
target: "cloudflare",
outfile: "/Users/user/project/out.js",
run: {
stdout: "worker\ndone",
},
});
itBundled("cloudflare/ExportsBrowserFallback", {
files: {
"/Users/user/project/src/entry.js": `import 'pkg'; console.log('done');`,
"/Users/user/project/node_modules/pkg/package.json": /* json */ `
{
"exports": {
"browser": "./browser.js",
"node": "./node.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg/browser.js": `console.log('browser')`,
"/Users/user/project/node_modules/pkg/node.js": `console.log('node')`,
"/Users/user/project/node_modules/pkg/default.js": `console.log('default')`,
},
target: "cloudflare",
outfile: "/Users/user/project/out.js",
run: {
stdout: "browser\ndone",
},
});
itBundled("cloudflare/ExportsFallbackToDefault", {
files: {
"/Users/user/project/src/entry.js": `import 'pkg'; console.log('done');`,
"/Users/user/project/node_modules/pkg/package.json": /* json */ `
{
"exports": {
"node": "./node.js",
"deno": "./deno.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg/node.js": `console.log('node')`,
"/Users/user/project/node_modules/pkg/deno.js": `console.log('deno')`,
"/Users/user/project/node_modules/pkg/default.js": `console.log('default')`,
},
target: "cloudflare",
outfile: "/Users/user/project/out.js",
run: {
stdout: "default\ndone",
},
});
itBundled("cloudflare/ExportsPriorityOrder", {
files: {
"/Users/user/project/src/entry.js": `import 'pkg'; console.log('done');`,
"/Users/user/project/node_modules/pkg/package.json": /* json */ `
{
"exports": {
"workerd": "./workerd.js",
"worker": "./worker.js",
"browser": "./browser.js",
"node": "./node.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg/workerd.js": `console.log('workerd')`,
"/Users/user/project/node_modules/pkg/worker.js": `console.log('worker')`,
"/Users/user/project/node_modules/pkg/browser.js": `console.log('browser')`,
"/Users/user/project/node_modules/pkg/node.js": `console.log('node')`,
"/Users/user/project/node_modules/pkg/default.js": `console.log('default')`,
},
target: "cloudflare",
outfile: "/Users/user/project/out.js",
run: {
stdout: "workerd\ndone",
},
});
itBundled("cloudflare/MainFieldsPreferModule", {
files: {
"/Users/user/project/src/entry.js": /* js */ `
import fn from 'demo-pkg'
console.log(fn())
`,
"/Users/user/project/node_modules/demo-pkg/package.json": /* json */ `
{
"main": "./main.js",
"module": "./main.esm.js"
}
`,
"/Users/user/project/node_modules/demo-pkg/main.js": /* js */ `
module.exports = function() {
return 'cjs'
}
`,
"/Users/user/project/node_modules/demo-pkg/main.esm.js": /* js */ `
export default function() {
return 'esm'
}
`,
},
target: "cloudflare",
run: {
stdout: "esm",
},
});
itBundled("cloudflare/IsServerSide", {
files: {
"/Users/user/project/src/entry.js": /* js */ `
// Cloudflare should behave like a server-side target
console.log(typeof process !== 'undefined' ? 'server' : 'client')
`,
},
target: "cloudflare",
run: {
stdout: "server",
},
});
itBundled("cloudflare/MultiplePackagesWithDifferentConditions", {
files: {
"/Users/user/project/src/entry.js": /* js */ `
import 'pkg1'
import 'pkg2'
import 'pkg3'
console.log('done')
`,
"/Users/user/project/node_modules/pkg1/package.json": /* json */ `
{
"exports": {
"workerd": "./workerd.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg1/workerd.js": `console.log('pkg1:workerd')`,
"/Users/user/project/node_modules/pkg1/default.js": `console.log('pkg1:default')`,
"/Users/user/project/node_modules/pkg2/package.json": /* json */ `
{
"exports": {
"worker": "./worker.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg2/worker.js": `console.log('pkg2:worker')`,
"/Users/user/project/node_modules/pkg2/default.js": `console.log('pkg2:default')`,
"/Users/user/project/node_modules/pkg3/package.json": /* json */ `
{
"exports": {
"browser": "./browser.js",
"default": "./default.js"
}
}
`,
"/Users/user/project/node_modules/pkg3/browser.js": `console.log('pkg3:browser')`,
"/Users/user/project/node_modules/pkg3/default.js": `console.log('pkg3:default')`,
},
target: "cloudflare",
outfile: "/Users/user/project/out.js",
run: {
stdout: "pkg1:workerd\npkg2:worker\npkg3:browser\ndone",
},
});
itBundled("cloudflare/NestedExportsWithConditions", {
files: {
"/Users/user/project/src/entry.js": `import { foo } from 'pkg/sub'; console.log(foo);`,
"/Users/user/project/node_modules/pkg/package.json": /* json */ `
{
"exports": {
"./sub": {
"workerd": "./sub-workerd.js",
"default": "./sub-default.js"
}
}
}
`,
"/Users/user/project/node_modules/pkg/sub-workerd.js": `export const foo = 'workerd'`,
"/Users/user/project/node_modules/pkg/sub-default.js": `export const foo = 'default'`,
},
target: "cloudflare",
outfile: "/Users/user/project/out.js",
run: {
stdout: "workerd",
},
});
});