feat: remove cloudflare adapter dependency to nodejs_compat

- adjusted bindings extraction to not rely on node inspect anymore
- added new vite export that uses nodejs apis
This commit is contained in:
dswbx
2025-11-11 13:33:04 +01:00
parent 3f55c0f2c5
commit 4859b99954
11 changed files with 119 additions and 31 deletions
+11 -2
View File
@@ -1,3 +1,4 @@
/** biome-ignore-all lint/suspicious/noConsole: . */
import { $ } from "bun"; import { $ } from "bun";
import * as tsup from "tsup"; import * as tsup from "tsup";
import pkg from "./package.json" with { type: "json" }; import pkg from "./package.json" with { type: "json" };
@@ -267,7 +268,7 @@ async function buildAdapters() {
tsup.build(baseConfig("react-router")), tsup.build(baseConfig("react-router")),
tsup.build( tsup.build(
baseConfig("bun", { baseConfig("bun", {
external: [/^bun\:.*/], external: [/^bun:.*/],
}), }),
), ),
tsup.build(baseConfig("astro")), tsup.build(baseConfig("astro")),
@@ -286,6 +287,14 @@ async function buildAdapters() {
external: [/bknd/, "wrangler", "node:process"], 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({ tsup.build({
...baseConfig("vite"), ...baseConfig("vite"),
@@ -322,7 +331,7 @@ async function buildAdapters() {
entry: ["src/adapter/sqlite/bun.ts"], entry: ["src/adapter/sqlite/bun.ts"],
outDir: "dist/adapter/sqlite", outDir: "dist/adapter/sqlite",
metafile: false, metafile: false,
external: [/^bun\:.*/], external: [/^bun:.*/],
}), }),
]); ]);
} }
+5
View File
@@ -214,6 +214,11 @@
"import": "./dist/adapter/cloudflare/proxy.js", "import": "./dist/adapter/cloudflare/proxy.js",
"require": "./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": { "./adapter": {
"types": "./dist/types/adapter/index.d.ts", "types": "./dist/types/adapter/index.d.ts",
"import": "./dist/adapter/index.js" "import": "./dist/adapter/index.js"
+31 -14
View File
@@ -1,5 +1,3 @@
import { inspect } from "node:util";
export type BindingTypeMap = { export type BindingTypeMap = {
D1Database: D1Database; D1Database: D1Database;
KVNamespace: KVNamespace; KVNamespace: KVNamespace;
@@ -10,21 +8,40 @@ export type BindingTypeMap = {
export type GetBindingType = keyof BindingTypeMap; export type GetBindingType = keyof BindingTypeMap;
export type BindingMap<T extends GetBindingType> = { key: string; value: BindingTypeMap[T] }; export type BindingMap<T extends GetBindingType> = { 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<T extends GetBindingType>(env: any, type: T): BindingMap<T>[] { export function getBindings<T extends GetBindingType>(env: any, type: T): BindingMap<T>[] {
const bindings: BindingMap<T>[] = []; const bindings: BindingMap<T>[] = [];
if (!(type in bindingMatchers)) {
console.error(`No binding matcher for type ${type}`);
return [];
}
const matcher = bindingMatchers[type];
for (const key in env) { for (const key in env) {
try { const value = env[key];
if ( if (typeof value !== "object" || value === null) continue;
(env[key] as any).constructor.name === type || if (!matcher(value)) continue;
String(env[key]) === `[object ${type}]` ||
inspect(env[key]).includes(type) bindings.push({
) { key,
bindings.push({ value,
key, } as any);
value: env[key] as BindingTypeMap[T],
});
}
} catch (e) {}
} }
return bindings; return bindings;
} }
@@ -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"]);
});
});
+6 -2
View File
@@ -16,10 +16,14 @@ export {
type GetBindingType, type GetBindingType,
type BindingMap, type BindingMap,
} from "./bindings"; } from "./bindings";
export { constants, makeConfig, type CloudflareContext } from "./config"; export {
constants,
makeConfig,
type CloudflareContext,
registerAsyncsExecutionContext,
} from "./config";
export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter"; export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter";
export { registries } from "bknd"; export { registries } from "bknd";
export { devFsVitePlugin, devFsWrite } from "./vite";
// for compatibility with old code // for compatibility with old code
export function d1<DB extends D1Database | D1DatabaseSession = D1Database>( export function d1<DB extends D1Database | D1DatabaseSession = D1Database>(
@@ -16,6 +16,7 @@ export function registerMedia(
env: Record<string, any>, env: Record<string, any>,
registries: typeof $registries = $registries, registries: typeof $registries = $registries,
) { ) {
if (registries.media.has("r2")) return;
const r2_bindings = getBindings(env, "R2Bucket"); const r2_bindings = getBindings(env, "R2Bucket");
registries.media.register( registries.media.register(
@@ -172,7 +173,7 @@ export class StorageR2Adapter extends StorageAdapter {
await this.bucket.delete(this.getKey(key)); await this.bucket.delete(this.getKey(key));
} }
getObjectUrl(key: string): string { getObjectUrl(): string {
throw new Error("Method getObjectUrl not implemented."); throw new Error("Method getObjectUrl not implemented.");
} }
@@ -183,7 +184,7 @@ export class StorageR2Adapter extends StorageAdapter {
return key; return key;
} }
toJSON(secrets?: boolean) { toJSON() {
return { return {
type: this.getName(), type: this.getName(),
config: {}, config: {},
+1 -2
View File
@@ -1,3 +1,4 @@
// biome-ignore-all lint: .
import type { Plugin } from "vite"; import type { Plugin } from "vite";
import { writeFile as nodeWriteFile } from "node:fs/promises"; import { writeFile as nodeWriteFile } from "node:fs/promises";
import { resolve } from "node:path"; import { resolve } from "node:path";
@@ -48,7 +49,6 @@ export function devFsVitePlugin({
// Skip our own debug output // Skip our own debug output
if (output.includes("[dev-fs-plugin]") || output.includes("[dev-fs-polyfill]")) { if (output.includes("[dev-fs-plugin]") || output.includes("[dev-fs-polyfill]")) {
// @ts-ignore // @ts-ignore
// biome-ignore lint/style/noArguments: <explanation>
return originalStdoutWrite.apply(process.stdout, arguments); return originalStdoutWrite.apply(process.stdout, arguments);
} }
@@ -178,7 +178,6 @@ export function devFsVitePlugin({
} }
// @ts-ignore // @ts-ignore
// biome-ignore lint:
return originalStdoutWrite.apply(process.stdout, arguments); return originalStdoutWrite.apply(process.stdout, arguments);
}; };
+4 -1
View File
@@ -73,8 +73,11 @@ export const env = <
onValid?: (valid: R) => void; onValid?: (valid: R) => void;
}, },
): R extends undefined ? Fallback : R => { ): R extends undefined ? Fallback : R => {
let source = opts?.source ?? {};
try {
source = process?.env ?? {};
} catch (e) {}
try { try {
const source = opts?.source ?? process.env;
const c = envs[key]; const c = envs[key];
const g = source[c.key]; const g = source[c.key];
const v = c.validate(g) as any; const v = c.validate(g) as any;
@@ -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: 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 ```ts
import { devFsVitePlugin } from "bknd/adapter/cloudflare"; import { devFsVitePlugin } from "bknd/adapter/cloudflare/vite";
export default defineConfig({ export default defineConfig({
plugins: [devFsVitePlugin()], // [!code highlight] 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. Now to use this polyfill, you can use the `devFsWrite` function to write files to the filesystem.
```ts ```ts
import { devFsWrite } from "bknd/adapter/cloudflare"; // [!code highlight] import { devFsWrite } from "bknd/adapter/cloudflare/vite"; // [!code highlight]
import { syncTypes } from "bknd/plugins"; import { syncTypes } from "bknd/plugins";
export default { export default {
+4 -4
View File
@@ -6,15 +6,15 @@
"scripts": { "scripts": {
"deploy": "wrangler deploy", "deploy": "wrangler deploy",
"dev": "wrangler dev", "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", "typegen": "wrangler types && npm run bknd-typegen",
"predev": "npm run typegen" "postinstall": "npm run typegen"
}, },
"dependencies": { "dependencies": {
"bknd": "file:../../app" "bknd": "file:../../app"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.9.2", "typescript": "^5.9.3",
"wrangler": "^4.34.0" "wrangler": "^4.46.0"
} }
} }
+1 -2
View File
@@ -2,8 +2,7 @@
"$schema": "node_modules/wrangler/config-schema.json", "$schema": "node_modules/wrangler/config-schema.json",
"name": "bknd-cf-worker-example", "name": "bknd-cf-worker-example",
"main": "src/index.ts", "main": "src/index.ts",
"compatibility_date": "2025-08-03", "compatibility_date": "2025-11-05",
"compatibility_flags": ["nodejs_compat"],
"workers_dev": true, "workers_dev": true,
"minify": true, "minify": true,
"assets": { "assets": {