From 9436b8bac5b6ea53be8a501d4908d0e790e9765c Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 29 Aug 2025 06:44:20 +0200 Subject: [PATCH] cloudflare: enable fs with @cloudflare/vite-plugin (#239) * init dev vite write plugin * reorganized cf proxy, fixed dev write --- app/build.ts | 8 ++ app/package.json | 5 + app/src/adapter/cloudflare/index.ts | 2 +- app/src/adapter/cloudflare/proxy.ts | 5 +- app/src/adapter/cloudflare/vite.ts | 135 +++++++++++++++++++++++++++ app/src/cli/commands/types/types.ts | 1 - examples/cloudflare-worker/config.ts | 14 +++ 7 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 app/src/adapter/cloudflare/vite.ts diff --git a/app/build.ts b/app/build.ts index 7586c66..2d0caa9 100644 --- a/app/build.ts +++ b/app/build.ts @@ -263,6 +263,14 @@ async function buildAdapters() { external: ["wrangler", "node:process"], }), ), + tsup.build( + baseConfig("cloudflare/proxy", { + entry: ["src/adapter/cloudflare/proxy.ts"], + outDir: "dist/adapter/cloudflare", + metafile: false, + external: [/bknd/, "wrangler", "node:process"], + }), + ), tsup.build({ ...baseConfig("vite"), diff --git a/app/package.json b/app/package.json index 2e316f7..434e6ff 100644 --- a/app/package.json +++ b/app/package.json @@ -197,6 +197,11 @@ "import": "./dist/adapter/cloudflare/index.js", "require": "./dist/adapter/cloudflare/index.js" }, + "./adapter/cloudflare/proxy": { + "types": "./dist/types/adapter/cloudflare/proxy.d.ts", + "import": "./dist/adapter/cloudflare/proxy.js", + "require": "./dist/adapter/cloudflare/proxy.js" + }, "./adapter": { "types": "./dist/types/adapter/index.d.ts", "import": "./dist/adapter/index.js" diff --git a/app/src/adapter/cloudflare/index.ts b/app/src/adapter/cloudflare/index.ts index 88f7413..eaa5253 100644 --- a/app/src/adapter/cloudflare/index.ts +++ b/app/src/adapter/cloudflare/index.ts @@ -15,7 +15,7 @@ export { export { constants } from "./config"; export { StorageR2Adapter, registerMedia } from "./storage/StorageR2Adapter"; export { registries } from "bknd"; -export { withPlatformProxy } from "./proxy"; +export { devFsPlugin, devFsWrite } from "./vite"; // for compatibility with old code export function d1( diff --git a/app/src/adapter/cloudflare/proxy.ts b/app/src/adapter/cloudflare/proxy.ts index 60e49ea..ddbd4b3 100644 --- a/app/src/adapter/cloudflare/proxy.ts +++ b/app/src/adapter/cloudflare/proxy.ts @@ -4,7 +4,7 @@ import { registerMedia, type CloudflareBkndConfig, type CloudflareEnv, -} from "."; +} from "bknd/adapter/cloudflare"; import type { PlatformProxy } from "wrangler"; import process from "node:process"; @@ -41,12 +41,13 @@ export function withPlatformProxy( beforeBuild: async (app, registries) => { if (!use_proxy) return; const env = await getEnv(); - registerMedia(env, registries); + registerMedia(env, registries as any); await config?.beforeBuild?.(app, registries); }, bindings: async (env) => { return (await config?.bindings?.(await getEnv(env))) || {}; }, + // @ts-ignore app: async (_env) => { const env = await getEnv(_env); diff --git a/app/src/adapter/cloudflare/vite.ts b/app/src/adapter/cloudflare/vite.ts new file mode 100644 index 0000000..53ba999 --- /dev/null +++ b/app/src/adapter/cloudflare/vite.ts @@ -0,0 +1,135 @@ +import type { Plugin } from "vite"; +import { writeFile as nodeWriteFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +/** + * Vite plugin that provides Node.js filesystem access during development + * by injecting a polyfill into the SSR environment + */ +export function devFsPlugin({ + verbose = false, + configFile = "bknd.config.ts", +}: { + verbose?: boolean; + configFile?: string; +}): Plugin { + let isDev = false; + let projectRoot = ""; + + return { + name: "dev-fs-plugin", + enforce: "pre", + configResolved(config) { + isDev = config.command === "serve"; + projectRoot = config.root; + }, + configureServer(server) { + if (!isDev) return; + + // Intercept stdout to watch for our write requests + const originalStdoutWrite = process.stdout.write; + process.stdout.write = function (chunk: any, encoding?: any, callback?: any) { + const output = chunk.toString(); + + // Check if this output contains our special write request + if (output.includes("{{DEV_FS_WRITE_REQUEST}}")) { + try { + // Extract the JSON from the log line + const match = output.match(/{{DEV_FS_WRITE_REQUEST}} ({.*})/); + if (match) { + const writeRequest = JSON.parse(match[1]); + if (writeRequest.type === "DEV_FS_WRITE_REQUEST") { + if (verbose) { + console.debug("[dev-fs-plugin] Intercepted write request via stdout"); + } + + // Process the write request immediately + (async () => { + try { + const fullPath = resolve(projectRoot, writeRequest.filename); + await nodeWriteFile(fullPath, writeRequest.data); + if (verbose) { + console.debug("[dev-fs-plugin] File written successfully!"); + } + } catch (error) { + console.error("[dev-fs-plugin] Error writing file:", error); + } + })(); + + // Don't output the raw write request to console + return true; + } + } + } catch (error) { + // Not a valid write request, continue with normal output + } + } + + // @ts-ignore + // biome-ignore lint: + return originalStdoutWrite.apply(process.stdout, arguments); + }; + + // Restore stdout when server closes + server.httpServer?.on("close", () => { + process.stdout.write = originalStdoutWrite; + }); + }, + // @ts-ignore + transform(code, id, options) { + // Only transform in SSR mode during development + if (!isDev || !options?.ssr) return; + + // Check if this is the bknd config file + if (id.includes(configFile)) { + if (verbose) { + console.debug("[dev-fs-plugin] Transforming", configFile); + } + + // Inject our filesystem polyfill at the top of the file + const polyfill = ` +// Dev-fs polyfill injected by vite-plugin-dev-fs +if (typeof globalThis !== 'undefined') { + globalThis.__devFsPolyfill = { + writeFile: async (filename, data) => { + ${verbose ? "console.debug('dev-fs polyfill: Intercepting write request for', filename);" : ""} + + // Use console logging as a communication channel + // The main process will watch for this specific log pattern + const writeRequest = { + type: 'DEV_FS_WRITE_REQUEST', + filename: filename, + data: data, + timestamp: Date.now() + }; + + // Output as a specially formatted console message + console.log('{{DEV_FS_WRITE_REQUEST}}', JSON.stringify(writeRequest)); + ${verbose ? "console.debug('dev-fs polyfill: Write request sent via console');" : ""} + + return Promise.resolve(); + } + }; +} +`; + return polyfill + code; + } + }, + }; +} + +// Write function that uses the dev-fs polyfill injected by our Vite plugin +export async function devFsWrite(filename: string, data: string): Promise { + try { + // Check if the dev-fs polyfill is available (injected by our Vite plugin) + if (typeof globalThis !== "undefined" && (globalThis as any).__devFsPolyfill) { + return (globalThis as any).__devFsPolyfill.writeFile(filename, data); + } + + // Fallback to Node.js fs for other environments (Node.js, Bun) + const { writeFile } = await import("node:fs/promises"); + return writeFile(filename, data); + } catch (error) { + console.error("[dev-fs-write] Error writing file:", error); + } +} diff --git a/app/src/cli/commands/types/types.ts b/app/src/cli/commands/types/types.ts index 3d53618..c2e8a78 100644 --- a/app/src/cli/commands/types/types.ts +++ b/app/src/cli/commands/types/types.ts @@ -24,7 +24,6 @@ async function action({ const app = await makeAppFromEnv({ server: "node", }); - await app.build(); const et = new EntityTypescript(app.em); diff --git a/examples/cloudflare-worker/config.ts b/examples/cloudflare-worker/config.ts index 4207e27..e352f16 100644 --- a/examples/cloudflare-worker/config.ts +++ b/examples/cloudflare-worker/config.ts @@ -1,7 +1,21 @@ import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare"; +import { syncTypes } from "bknd/plugins"; +import { writeFile } from "node:fs/promises"; + +const isDev = !import.meta.env.PROD; export default { d1: { session: true, }, + options: { + plugins: [ + syncTypes({ + enabled: isDev, + write: async (et) => { + await writeFile("bknd-types.d.ts", et.toString()); + }, + }), + ], + }, } satisfies CloudflareBkndConfig;