fix(core): make packed SDK boot on workerd (#44703)
This commit is contained in:
@@ -89,6 +89,12 @@ jobs:
|
||||
working-directory: packages/codemode
|
||||
run: bun run script/publish.ts --dry-run
|
||||
|
||||
- name: Verify packed workerd SDK
|
||||
if: runner.os == 'Linux'
|
||||
timeout-minutes: 15
|
||||
working-directory: packages/sdk
|
||||
run: bun run verify:package
|
||||
|
||||
- name: Verify compiled service lifecycle
|
||||
if: always()
|
||||
timeout-minutes: 10
|
||||
|
||||
@@ -32,3 +32,38 @@ const result = await Bun.build({
|
||||
},
|
||||
})
|
||||
if (!result.success) throw new AggregateError(result.logs, "Failed to build Core")
|
||||
|
||||
// Bun's Node target eagerly creates its shared require helper, so every split
|
||||
// entry evaluates import.meta.url even when it never requires a module. Keep
|
||||
// the helper lazy until Bun stops hoisting it into workerd-reachable chunks.
|
||||
// https://github.com/oven-sh/bun/issues/12615
|
||||
const eagerRequire = "var __require = /* @__PURE__ */ createRequire(import.meta.url);"
|
||||
const lazyRequire = `var __require = (specifier) => createRequire(import.meta.url ?? "file:///worker.js")(specifier);
|
||||
__require.resolve = (specifier, options) => createRequire(import.meta.url ?? "file:///worker.js").resolve(specifier, options);`
|
||||
const rewritten = await Promise.all(
|
||||
result.outputs.map(async (output) => {
|
||||
if (!output.path.endsWith(".js")) return false
|
||||
const source = await output.text()
|
||||
|
||||
const generatedUses = source
|
||||
.replace(/import\s*\{[^}]*\b__require\b[^}]*\}\s*from\s*["'][^"']+["'];/g, "")
|
||||
.replace(/export\s*\{[^}]*\b__require\b[^}]*\};/g, "")
|
||||
.replace(eagerRequire, "")
|
||||
if (/\bnew\s+__require\s*\(/.test(generatedUses))
|
||||
throw new Error(`Unsupported generated require constructor in ${output.path}`)
|
||||
const unsupported = generatedUses
|
||||
.replace(/\b__require\.resolve\s*\(/g, "")
|
||||
.replace(/\b__require\s*\(/g, "")
|
||||
if (/\b__require\b/.test(unsupported)) throw new Error(`Unsupported generated require usage in ${output.path}`)
|
||||
|
||||
if (!source.includes(eagerRequire)) return false
|
||||
if (source.indexOf(eagerRequire) !== source.lastIndexOf(eagerRequire))
|
||||
throw new Error(`Multiple eager require helpers in ${output.path}`)
|
||||
const rewrittenSource = source.replace(eagerRequire, lazyRequire)
|
||||
if (rewrittenSource.includes(eagerRequire)) throw new Error(`Failed to rewrite eager require helper in ${output.path}`)
|
||||
await Bun.write(output.path, rewrittenSource)
|
||||
return true
|
||||
}),
|
||||
)
|
||||
if (rewritten.filter(Boolean).length !== 1)
|
||||
throw new Error("Expected exactly one eager require helper; Bun may have fixed #12615 and made this shim removable")
|
||||
|
||||
@@ -572,7 +572,7 @@ function cacheKey(source: string) {
|
||||
}
|
||||
|
||||
export function bodyDigest(text: string) {
|
||||
return new Bun.CryptoHasher("sha256").update(text).digest("hex")
|
||||
return Hash.sha256(text)
|
||||
}
|
||||
|
||||
export const layer = (options?: Options) =>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * as SessionMessage from "./message.js"
|
||||
export * as SessionMessage from "@opencode-ai/schema/session-message"
|
||||
export * from "@opencode-ai/schema/session-message"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as SessionSchema from "./schema.js"
|
||||
export * as SessionSchema from "@opencode-ai/schema/session"
|
||||
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"scripts": {
|
||||
"build": "bun run script/build.ts",
|
||||
"test": "bun test --timeout 5000",
|
||||
"typecheck": "tsgo -b"
|
||||
"typecheck": "tsgo -b",
|
||||
"verify:package": "bun run script/verify-package.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { mkdtemp, rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const root = fileURLToPath(new URL("../../..", import.meta.url))
|
||||
const names = ["schema", "codemode", "ai", "util", "protocol", "client", "plugin", "core", "simulation", "server", "sdk"]
|
||||
const temporary = await mkdtemp(join(tmpdir(), "opencode-sdk-package-"))
|
||||
const archives = new Map<string, string>()
|
||||
|
||||
try {
|
||||
for (const name of names) {
|
||||
const directory = join(root, "packages", name)
|
||||
await $`bun run build`.cwd(directory)
|
||||
const original = await Bun.file(join(directory, "package.json")).text()
|
||||
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- package manifests are validated by their package builds.
|
||||
const pkg = JSON.parse(original) as {
|
||||
name: string
|
||||
dependencies?: Record<string, string>
|
||||
exports?: Record<string, string | { import: string; types: string }>
|
||||
imports?: Record<string, Record<string, string>>
|
||||
}
|
||||
const archive = join(temporary, `${name}.tgz`)
|
||||
|
||||
if (pkg.dependencies) {
|
||||
const unpacked = Object.keys(pkg.dependencies).filter(
|
||||
(dependency) => dependency.startsWith("@opencode-ai/") && !archives.has(dependency),
|
||||
)
|
||||
if (unpacked.length > 0) throw new Error(`${pkg.name} has unpacked workspace dependencies: ${unpacked.join(", ")}`)
|
||||
pkg.dependencies = Object.fromEntries(
|
||||
Object.entries(pkg.dependencies).map(([dependency, version]) => {
|
||||
const local = archives.get(dependency)
|
||||
return [dependency, local ? `file:${local}` : version]
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (pkg.exports) {
|
||||
pkg.exports = Object.fromEntries(
|
||||
Object.entries(pkg.exports).map(([key, value]) => {
|
||||
if (typeof value !== "string") return [key, value]
|
||||
return [key, { import: output(name, value), types: output(name, value, true) }]
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (pkg.imports) {
|
||||
pkg.imports = Object.fromEntries(
|
||||
Object.entries(pkg.imports).map(([key, conditions]) => [
|
||||
key,
|
||||
Object.fromEntries(
|
||||
Object.entries(conditions).map(([condition, value]) => [condition, output(name, value, condition === "types")]),
|
||||
),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
await Bun.write(join(directory, "package.json"), JSON.stringify(pkg, null, 2) + "\n")
|
||||
try {
|
||||
await $`bun pm pack --filename ${archive} --ignore-scripts --quiet`.cwd(directory)
|
||||
} finally {
|
||||
await Bun.write(join(directory, "package.json"), original)
|
||||
}
|
||||
archives.set(pkg.name, archive)
|
||||
}
|
||||
|
||||
const consumer = join(temporary, "consumer")
|
||||
await Bun.write(
|
||||
join(consumer, "package.json"),
|
||||
JSON.stringify({ name: "opencode-sdk-consumer", private: true, type: "module" }),
|
||||
)
|
||||
await Promise.all([
|
||||
Bun.write(
|
||||
join(consumer, "wrangler.jsonc"),
|
||||
JSON.stringify({
|
||||
name: "opencode-sdk-packed-consumer",
|
||||
main: "worker.js",
|
||||
compatibility_date: "2026-07-15",
|
||||
compatibility_flags: ["nodejs_compat"],
|
||||
durable_objects: { bindings: [{ name: "OPENCODE", class_name: "OpenCodeDO" }] },
|
||||
migrations: [{ tag: "v1", new_sqlite_classes: ["OpenCodeDO"] }],
|
||||
}),
|
||||
),
|
||||
Bun.write(
|
||||
join(consumer, "worker.js"),
|
||||
`import { bodyDigest } from "@opencode-ai/core/models-dev"
|
||||
import { OpenCodeWorkerd } from "@opencode-ai/sdk/workerd"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export class OpenCodeDO {
|
||||
constructor(state) {
|
||||
this.state = state
|
||||
}
|
||||
|
||||
fetch() {
|
||||
if (bodyDigest("packed-workerd") !== "5fc174bf63e8dd108ebb6c53d85e7bbc4525b2f4c1c43280364cdbfd9b37aaf5") {
|
||||
throw new Error("Packed workerd SHA-256 mismatch")
|
||||
}
|
||||
const storage = this.state.storage
|
||||
return Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const sdk = yield* OpenCodeWorkerd.create({
|
||||
storage,
|
||||
app: { version: "packed-workerd" },
|
||||
config: { content: "{}" },
|
||||
})
|
||||
return Response.json(yield* sdk.health.get())
|
||||
}).pipe(Effect.scoped),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
fetch(request, env) {
|
||||
return env.OPENCODE.get(env.OPENCODE.idFromName("packed-consumer")).fetch(request)
|
||||
},
|
||||
}
|
||||
`,
|
||||
),
|
||||
Bun.write(
|
||||
join(consumer, "boot.mjs"),
|
||||
`import { Miniflare } from "miniflare"
|
||||
|
||||
const miniflare = new Miniflare({
|
||||
compatibilityDate: "2026-07-15",
|
||||
compatibilityFlags: ["nodejs_compat"],
|
||||
modules: true,
|
||||
scriptPath: new URL("./dist/worker.js", import.meta.url).pathname,
|
||||
durableObjects: { OPENCODE: { className: "OpenCodeDO", useSQLite: true } },
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await miniflare.dispatchFetch("http://opencode.local/health")
|
||||
if (response.status !== 200) throw new Error(
|
||||
"Packed workerd health returned " + response.status + ": " + await response.text(),
|
||||
)
|
||||
const body = await response.json()
|
||||
if (body.healthy !== true || body.version !== "packed-workerd") {
|
||||
throw new Error("Unexpected packed workerd health: " + JSON.stringify(body))
|
||||
}
|
||||
} finally {
|
||||
await miniflare.dispose()
|
||||
}
|
||||
`,
|
||||
),
|
||||
])
|
||||
|
||||
const sdk = archives.get("@opencode-ai/sdk")
|
||||
if (!sdk) throw new Error("Packed SDK archive was not created")
|
||||
await $`npm install --ignore-scripts --no-audit --no-fund --package-lock=false ${sdk} wrangler@4.110.0`.cwd(consumer)
|
||||
await $`node_modules/.bin/wrangler deploy --dry-run --config wrangler.jsonc --outdir dist`.cwd(consumer)
|
||||
|
||||
const transpiler = new Bun.Transpiler({ loader: "js" })
|
||||
const bundled = await Bun.file(join(consumer, "dist/worker.js")).text()
|
||||
if (/createRequire\s*\(\s*import\.meta\.url\s*\)/.test(bundled)) {
|
||||
throw new Error("Packed workerd bundle contains Bun's eager Node require initializer")
|
||||
}
|
||||
const bunGlobals = Array.from(new Set(bundled.match(/\bBun\.[A-Za-z_$][\w$]*/g) ?? []))
|
||||
if (bunGlobals.length > 0) throw new Error(`Packed workerd bundle references Bun globals: ${bunGlobals.join(", ")}`)
|
||||
const leaked = [
|
||||
...transpiler.scanImports(bundled)
|
||||
.filter((imported) => imported.kind !== "dynamic-import")
|
||||
.map((imported) => imported.path),
|
||||
...Array.from(bundled.matchAll(/\brequire\(\s*["']([^"']+)["']\s*\)/g), (match) => match[1]),
|
||||
]
|
||||
.filter((specifier) => specifier === "bun" || specifier.startsWith("bun:"))
|
||||
if (leaked.length > 0) throw new Error(`Packed workerd bundle statically imports Bun builtins: ${leaked.join(", ")}`)
|
||||
|
||||
await $`node boot.mjs`.cwd(consumer)
|
||||
console.log("packed SDK consumer OK")
|
||||
} finally {
|
||||
await rm(temporary, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
function output(name: string, value: string, types = false) {
|
||||
const root = name === "core" && types ? "./dist/types/" : "./dist/"
|
||||
return value.replace("./src/", root).replace(/\.ts$/, types ? ".d.ts" : ".js")
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
"default": "./src/global-roots.ts"
|
||||
},
|
||||
"#runtime-import": {
|
||||
"workerd": "./src/runtime/import.bun.ts",
|
||||
"workerd": "./src/runtime/import.workerd.ts",
|
||||
"bun": "./src/runtime/import.bun.ts",
|
||||
"node": "./src/runtime/import.node.ts",
|
||||
"default": "./src/runtime/import.bun.ts"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const unavailable = () => new Error("Dynamic module loading is unavailable on workerd")
|
||||
|
||||
export function importModule(_specifier: string): Promise<unknown> {
|
||||
return Promise.reject(unavailable())
|
||||
}
|
||||
|
||||
export function resolveModule(_specifier: string, _directory: string): string {
|
||||
throw unavailable()
|
||||
}
|
||||
Reference in New Issue
Block a user