diff --git a/app/build.ts b/app/build.ts index 3de4e2aa..a66dbeae 100644 --- a/app/build.ts +++ b/app/build.ts @@ -1,3 +1,4 @@ +/** biome-ignore-all lint/suspicious/noConsole: . */ import { $ } from "bun"; import * as tsup from "tsup"; import pkg from "./package.json" with { type: "json" }; @@ -267,7 +268,7 @@ async function buildAdapters() { tsup.build(baseConfig("react-router")), tsup.build( baseConfig("bun", { - external: [/^bun\:.*/], + external: [/^bun:.*/], }), ), tsup.build(baseConfig("astro")), @@ -286,6 +287,14 @@ async function buildAdapters() { external: [/bknd/, "wrangler", "node:process"], }), ), + tsup.build( + baseConfig("cloudflare/vite", { + target: "esnext", + entry: ["src/adapter/cloudflare/vite.ts"], + outDir: "dist/adapter/cloudflare", + metafile: false, + }), + ), tsup.build({ ...baseConfig("vite"), @@ -322,7 +331,7 @@ async function buildAdapters() { entry: ["src/adapter/sqlite/bun.ts"], outDir: "dist/adapter/sqlite", metafile: false, - external: [/^bun\:.*/], + external: [/^bun:.*/], }), ]); } diff --git a/app/package.json b/app/package.json index 52268cf4..33ec5755 100644 --- a/app/package.json +++ b/app/package.json @@ -214,6 +214,11 @@ "import": "./dist/adapter/cloudflare/proxy.js", "require": "./dist/adapter/cloudflare/proxy.js" }, + "./adapter/cloudflare/vite": { + "types": "./dist/types/adapter/cloudflare/vite.d.ts", + "import": "./dist/adapter/cloudflare/vite.js", + "require": "./dist/adapter/cloudflare/vite.js" + }, "./adapter": { "types": "./dist/types/adapter/index.d.ts", "import": "./dist/adapter/index.js" diff --git a/app/src/adapter/cloudflare/bindings.ts b/app/src/adapter/cloudflare/bindings.ts index 891081ef..ebeff51b 100644 --- a/app/src/adapter/cloudflare/bindings.ts +++ b/app/src/adapter/cloudflare/bindings.ts @@ -1,5 +1,3 @@ -import { inspect } from "node:util"; - export type BindingTypeMap = { D1Database: D1Database; KVNamespace: KVNamespace; @@ -10,21 +8,40 @@ export type BindingTypeMap = { export type GetBindingType = keyof BindingTypeMap; export type BindingMap = { key: string; value: BindingTypeMap[T] }; +export const bindingMatchers = { + // D1Database instance doesn't stringify to [object D1Database] (yet) + D1Database: (value: object): value is D1Database => { + if (typeof value !== "object" || value === null) return false; + if (String(value) === "[object D1Database]") return true; + if (Object.keys(value).length !== 2) return false; + return ["alwaysPrimarySession", "fetcher"].every((key) => key in value); + }, + KVNamespace: (value: object): value is KVNamespace => + String(value) === "[object KvNamespace]" && Object.keys(value).length === 0, + DurableObjectNamespace: (value: object): value is DurableObjectNamespace => + String(value) === "[object DurableObjectNamespace]" && Object.keys(value).length === 0, + R2Bucket: (value: object): value is R2Bucket => + String(value) === "[object R2Bucket]" && Object.keys(value).length === 0, +}; + export function getBindings(env: any, type: T): BindingMap[] { const bindings: BindingMap[] = []; + if (!(type in bindingMatchers)) { + console.error(`No binding matcher for type ${type}`); + return []; + } + + const matcher = bindingMatchers[type]; + for (const key in env) { - try { - if ( - (env[key] as any).constructor.name === type || - String(env[key]) === `[object ${type}]` || - inspect(env[key]).includes(type) - ) { - bindings.push({ - key, - value: env[key] as BindingTypeMap[T], - }); - } - } catch (e) {} + const value = env[key]; + if (typeof value !== "object" || value === null) continue; + if (!matcher(value)) continue; + + bindings.push({ + key, + value, + } as any); } return bindings; } diff --git a/app/src/adapter/cloudflare/bindings.vitest.ts b/app/src/adapter/cloudflare/bindings.vitest.ts new file mode 100644 index 00000000..77babb83 --- /dev/null +++ b/app/src/adapter/cloudflare/bindings.vitest.ts @@ -0,0 +1,51 @@ +import { describe, afterAll, test, expect, beforeAll } from "vitest"; +import { Miniflare } from "miniflare"; +import { getBindings, type GetBindingType } from "./bindings"; + +let mf: Miniflare | undefined; + +beforeAll(() => { + mf = new Miniflare({ + modules: true, + script: `import { DurableObject } from "cloudflare:workers"; + export default { async fetch() { return new Response(null); } } + export class TestObject extends DurableObject {} + export class TestObject2 extends DurableObject {}`, + r2Buckets: ["BUCKET"], + d1Databases: ["DB"], + kvNamespaces: ["KV"], + durableObjects: { + DO_SQLITE: { className: "TestObject", useSQLite: true }, + DO_KV: { className: "TestObject2" }, + }, + }); +}); +afterAll(() => { + mf?.dispose(); +}); + +describe("bindings", () => { + test("extracts", async () => { + const env = { + ...(await mf!.getBindings()), + some: "string", + another: 123, + array: [1, 2, 3], + }; + + const $getBindingKeys = (type: GetBindingType) => { + const bindings = getBindings(env, type); + const keys = bindings?.map((b) => b.key); + if (!keys || keys.length === 0) { + console.error(`No ${type} binding found`, { bindings }); + throw new Error(`No ${type} binding found`); + } + return keys; + }; + + expect($getBindingKeys("D1Database")).toEqual(["DB"]); + expect($getBindingKeys("KVNamespace")).toEqual(["KV"]); + expect($getBindingKeys("DurableObjectNamespace")).toEqual(["DO_SQLITE", "DO_KV"]); + expect($getBindingKeys("R2Bucket")).toEqual(["BUCKET"]); + }); +}); diff --git a/app/src/adapter/cloudflare/index.ts b/app/src/adapter/cloudflare/index.ts index abc0719e..3fd83dda 100644 --- a/app/src/adapter/cloudflare/index.ts +++ b/app/src/adapter/cloudflare/index.ts @@ -16,10 +16,14 @@ export { type GetBindingType, type BindingMap, } from "./bindings"; -export { constants, makeConfig, type CloudflareContext } from "./config"; +export { + constants, + makeConfig, + type CloudflareContext, + registerAsyncsExecutionContext, +} from "./config"; export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter"; export { registries } from "bknd"; -export { devFsVitePlugin, devFsWrite } from "./vite"; // for compatibility with old code export function d1( diff --git a/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts b/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts index 756e562d..c090b13f 100644 --- a/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts +++ b/app/src/adapter/cloudflare/storage/StorageR2Adapter.ts @@ -16,6 +16,7 @@ export function registerMedia( env: Record, registries: typeof $registries = $registries, ) { + if (registries.media.has("r2")) return; const r2_bindings = getBindings(env, "R2Bucket"); registries.media.register( @@ -172,7 +173,7 @@ export class StorageR2Adapter extends StorageAdapter { await this.bucket.delete(this.getKey(key)); } - getObjectUrl(key: string): string { + getObjectUrl(): string { throw new Error("Method getObjectUrl not implemented."); } @@ -183,7 +184,7 @@ export class StorageR2Adapter extends StorageAdapter { return key; } - toJSON(secrets?: boolean) { + toJSON() { return { type: this.getName(), config: {}, diff --git a/app/src/adapter/cloudflare/vite.ts b/app/src/adapter/cloudflare/vite.ts index 1b7640bf..139f03a4 100644 --- a/app/src/adapter/cloudflare/vite.ts +++ b/app/src/adapter/cloudflare/vite.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint: . import type { Plugin } from "vite"; import { writeFile as nodeWriteFile } from "node:fs/promises"; import { resolve } from "node:path"; @@ -48,7 +49,6 @@ export function devFsVitePlugin({ // Skip our own debug output if (output.includes("[dev-fs-plugin]") || output.includes("[dev-fs-polyfill]")) { // @ts-ignore - // biome-ignore lint/style/noArguments: return originalStdoutWrite.apply(process.stdout, arguments); } @@ -178,7 +178,6 @@ export function devFsVitePlugin({ } // @ts-ignore - // biome-ignore lint: return originalStdoutWrite.apply(process.stdout, arguments); }; diff --git a/app/src/core/env.ts b/app/src/core/env.ts index 77710736..f71c1a0c 100644 --- a/app/src/core/env.ts +++ b/app/src/core/env.ts @@ -73,8 +73,11 @@ export const env = < onValid?: (valid: R) => void; }, ): R extends undefined ? Fallback : R => { + let source = opts?.source ?? {}; + try { + source = process?.env ?? {}; + } catch (e) {} try { - const source = opts?.source ?? process.env; const c = envs[key]; const g = source[c.key]; const v = c.validate(g) as any; diff --git a/docs/content/docs/(documentation)/integration/(runtimes)/cloudflare.mdx b/docs/content/docs/(documentation)/integration/(runtimes)/cloudflare.mdx index 1157e062..69911fe1 100644 --- a/docs/content/docs/(documentation)/integration/(runtimes)/cloudflare.mdx +++ b/docs/content/docs/(documentation)/integration/(runtimes)/cloudflare.mdx @@ -164,7 +164,7 @@ The [Cloudflare Vite Plugin](https://developers.cloudflare.com/workers/vite-plug To fix this, bknd exports a Vite plugin that provides filesystem access during development. You can use it by adding the following to your `vite.config.ts` file: ```ts -import { devFsVitePlugin } from "bknd/adapter/cloudflare"; +import { devFsVitePlugin } from "bknd/adapter/cloudflare/vite"; export default defineConfig({ plugins: [devFsVitePlugin()], // [!code highlight] @@ -174,7 +174,7 @@ export default defineConfig({ Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem. ```ts -import { devFsWrite } from "bknd/adapter/cloudflare"; // [!code highlight] +import { devFsWrite } from "bknd/adapter/cloudflare/vite"; // [!code highlight] import { syncTypes } from "bknd/plugins"; export default { diff --git a/examples/cloudflare-worker/package.json b/examples/cloudflare-worker/package.json index 5169a197..43cb22df 100644 --- a/examples/cloudflare-worker/package.json +++ b/examples/cloudflare-worker/package.json @@ -6,15 +6,15 @@ "scripts": { "deploy": "wrangler deploy", "dev": "wrangler dev", - "bknd-typegen": "PROXY=1 npx bknd types", + "bknd-typegen": "PROXY=1 node_modules/.bin/bknd types", "typegen": "wrangler types && npm run bknd-typegen", - "predev": "npm run typegen" + "postinstall": "npm run typegen" }, "dependencies": { "bknd": "file:../../app" }, "devDependencies": { - "typescript": "^5.9.2", - "wrangler": "^4.34.0" + "typescript": "^5.9.3", + "wrangler": "^4.46.0" } } diff --git a/examples/cloudflare-worker/wrangler.json b/examples/cloudflare-worker/wrangler.json index fd92cd83..d72dd4c3 100644 --- a/examples/cloudflare-worker/wrangler.json +++ b/examples/cloudflare-worker/wrangler.json @@ -2,8 +2,7 @@ "$schema": "node_modules/wrangler/config-schema.json", "name": "bknd-cf-worker-example", "main": "src/index.ts", - "compatibility_date": "2025-08-03", - "compatibility_flags": ["nodejs_compat"], + "compatibility_date": "2025-11-05", "workers_dev": true, "minify": true, "assets": {