From e7bb5baa1b236d351b8ce9bede5d1d97ffa64ce7 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 15 Dec 2023 11:45:21 +0900 Subject: [PATCH 1/3] feat(vite-node-miniflare): cache pre-bundle --- .../src/server/pre-bundle/plugin.ts | 23 +++--- .../src/server/pre-bundle/utils.ts | 79 +++++++++++++------ 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts b/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts index 1cb2f10f8..b7216b47f 100644 --- a/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts +++ b/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts @@ -3,16 +3,17 @@ import process from "node:process"; import { colors } from "@hiogawa/utils"; import { type Plugin, type ResolvedConfig } from "vite"; import { name as packageName } from "../../../package.json"; -import { preBundle } from "./utils"; +import { PreBundler } from "./utils"; -// TODO: cache +// TODO: include this plugin in main one vitePluginViteNodeMiniflare via preBundle options? export function vitePluginPreBundle(pluginOptions: { include: string[]; + force?: boolean; }): Plugin { - let alias: Record; - let config: ResolvedConfig; const name = `${packageName}/pre-bundle`; + let config: ResolvedConfig; + let preBundler: PreBundler; return { name, @@ -21,17 +22,21 @@ export function vitePluginPreBundle(pluginOptions: { config = config_; }, async buildStart(_options) { - config.logger.info( - ["", colors.cyan(`[${name}] pre-bundling...`)].join("\n") - ); const outDir = path.join( process.cwd(), "node_modules/.cache/@hiogawa/vite-node-miniflare/pre-bundle" ); - alias = await preBundle(pluginOptions.include, outDir); + preBundler = new PreBundler(pluginOptions.include, outDir); + if (preBundler.isCached()) { + return; + } + config.logger.info( + ["", colors.cyan(`[${name}] pre-bundling...`)].join("\n") + ); + await preBundler.run(); }, resolveId(source, _importer, options) { - return options.ssr ? alias[source] : undefined; + return options.ssr ? preBundler.alias[source] : undefined; }, }; } diff --git a/packages/vite-node-miniflare/src/server/pre-bundle/utils.ts b/packages/vite-node-miniflare/src/server/pre-bundle/utils.ts index 8a8402643..03039ce28 100644 --- a/packages/vite-node-miniflare/src/server/pre-bundle/utils.ts +++ b/packages/vite-node-miniflare/src/server/pre-bundle/utils.ts @@ -1,7 +1,7 @@ import childProcess from "node:child_process"; import fs from "node:fs"; import path from "node:path"; -import { createManualPromise } from "@hiogawa/utils"; +import { createManualPromise, wrapError } from "@hiogawa/utils"; // quick-and-dirty CJS pre-bundling // since `ssr.optimizeDeps` doesn't seem to work when running vite-node client on workered @@ -36,33 +36,60 @@ async function generateEntryCode(mod: string) { return `export { ${names.join(", ")} } from "${mod}"\n`; } -export async function preBundle(mods: string[], outDir: string) { - const esbuild = await import("esbuild"); +export class PreBundler { + entries: Record = {}; + alias: Record = {}; + hashValue: string; + hashPath: string; - const srcDir = path.join(outDir, ".tmp"); - const entries: Record = {}; - const alias: Record = {}; - for (const mod of mods) { - const entryCode = await generateEntryCode(mod); - const entry = path.join(mod, "index.js"); - const entryPath = path.join(srcDir, entry); - await fs.promises.mkdir(path.dirname(entryPath), { recursive: true }); - await fs.promises.writeFile(entryPath, entryCode); - entries[entry.slice(0, -3)] = entryPath; - alias[mod] = path.join(outDir, entry); + constructor(public mods: string[], public outDir: string) { + this.hashValue = JSON.stringify(mods); + this.hashPath = path.join(outDir, ".hash"); + + for (const mod of mods) { + this.entries[mod] = { + in: path.join(outDir, ".src", path.join(mod, "index.js")), + out: path.join(outDir, path.join(mod, "index")), // esbuild's need extension stripped + }; + this.alias[mod] = path.join(outDir, path.join(mod, "index.js")); + } } - await esbuild.build({ - entryPoints: entries, - format: "esm", - platform: "browser", - conditions: ["browser"], - bundle: true, - splitting: true, - outdir: outDir, - logLevel: "info", - tsconfigRaw: {}, - }); + isCached(): boolean { + const result = wrapError( + () => + fs.existsSync(this.hashPath) && + fs.readFileSync(this.hashPath, "utf-8") === this.hashValue + ); + return result.ok && result.value; + } + + async run() { + const esbuild = await import("esbuild"); - return alias; + // extract cjs module exports and generate source file to re-export as ESM + for (const [mod, entry] of Object.entries(this.entries)) { + const entryCode = await generateEntryCode(mod); + await fs.promises.mkdir(path.dirname(entry.in), { recursive: true }); + await fs.promises.writeFile(entry.in, entryCode); + } + + // bundle with code splitting + const result = await esbuild.build({ + entryPoints: Object.values(this.entries), + format: "esm", + platform: "browser", + conditions: ["browser"], + bundle: true, + splitting: true, + outdir: this.outDir, + logLevel: "info", + tsconfigRaw: {}, + }); + + // save bundle hash + await fs.promises.writeFile(this.hashPath, this.hashValue); + + return result; + } } From aaf07a0da75b3403c752baa50c45518c1fd22f0d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 15 Dec 2023 11:47:59 +0900 Subject: [PATCH 2/3] feat: support "force" option --- packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts b/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts index b7216b47f..af7761ad9 100644 --- a/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts +++ b/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts @@ -27,7 +27,7 @@ export function vitePluginPreBundle(pluginOptions: { "node_modules/.cache/@hiogawa/vite-node-miniflare/pre-bundle" ); preBundler = new PreBundler(pluginOptions.include, outDir); - if (preBundler.isCached()) { + if (!pluginOptions.force && preBundler.isCached()) { return; } config.logger.info( From 5745ad42a1e2e1bc47db27b21921b25680e93f4d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 15 Dec 2023 11:53:37 +0900 Subject: [PATCH 3/3] refactor: preBundle option to main plugin --- .../examples/react-router/vite.config.ts | 12 +++++------- .../examples/react/vite.config.ts | 12 +++++------- packages/vite-node-miniflare/src/server/plugin.ts | 15 +++++++++++++-- .../src/server/pre-bundle/plugin.ts | 3 +-- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/vite-node-miniflare/examples/react-router/vite.config.ts b/packages/vite-node-miniflare/examples/react-router/vite.config.ts index ed2bc54e6..795b5daa1 100644 --- a/packages/vite-node-miniflare/examples/react-router/vite.config.ts +++ b/packages/vite-node-miniflare/examples/react-router/vite.config.ts @@ -1,8 +1,5 @@ import globRoutesPlugin from "@hiogawa/vite-glob-routes"; -import { - vitePluginPreBundle, - vitePluginViteNodeMiniflare, -} from "@hiogawa/vite-node-miniflare"; +import { vitePluginViteNodeMiniflare } from "@hiogawa/vite-node-miniflare"; import react from "@vitejs/plugin-react"; import { Log } from "miniflare"; import { defineConfig } from "vite"; @@ -15,15 +12,16 @@ export default defineConfig({ }, plugins: [ globRoutesPlugin({ root: "/src/routes" }), - vitePluginPreBundle({ - include: ["react", "react/jsx-dev-runtime", "react-dom/server"], - }), vitePluginViteNodeMiniflare({ debug: true, entry: "./src/server/worker-entry-wrapper.ts", miniflareOptions(options) { options.log = new Log(); }, + preBundle: { + include: ["react", "react/jsx-dev-runtime", "react-dom/server"], + force: true, + }, }), react(), ], diff --git a/packages/vite-node-miniflare/examples/react/vite.config.ts b/packages/vite-node-miniflare/examples/react/vite.config.ts index 35c985c94..065078419 100644 --- a/packages/vite-node-miniflare/examples/react/vite.config.ts +++ b/packages/vite-node-miniflare/examples/react/vite.config.ts @@ -1,7 +1,4 @@ -import { - vitePluginPreBundle, - vitePluginViteNodeMiniflare, -} from "@hiogawa/vite-node-miniflare"; +import { vitePluginViteNodeMiniflare } from "@hiogawa/vite-node-miniflare"; import react from "@vitejs/plugin-react"; import { Log } from "miniflare"; import { defineConfig } from "vite"; @@ -13,15 +10,16 @@ export default defineConfig({ noExternal: true, }, plugins: [ - vitePluginPreBundle({ - include: ["react", "react/jsx-dev-runtime", "react-dom/server"], - }), vitePluginViteNodeMiniflare({ debug: true, entry: "./src/worker-entry.tsx", miniflareOptions(options) { options.log = new Log(); }, + preBundle: { + include: ["react", "react/jsx-dev-runtime", "react-dom/server"], + force: true, + }, }), react(), ], diff --git a/packages/vite-node-miniflare/src/server/plugin.ts b/packages/vite-node-miniflare/src/server/plugin.ts index 33e8da049..5545b8ca3 100644 --- a/packages/vite-node-miniflare/src/server/plugin.ts +++ b/packages/vite-node-miniflare/src/server/plugin.ts @@ -1,5 +1,6 @@ import * as httipAdapterNode from "@hattip/adapter-node/native-fetch"; import * as httipCompose from "@hattip/compose"; +import { typedBoolean } from "@hiogawa/utils"; import { Miniflare, type MiniflareOptions, @@ -8,6 +9,7 @@ import { import type { Plugin } from "vite"; import type { ViteNodeRunnerOptions, ViteNodeServerOptions } from "vite-node"; import { ViteNodeServer } from "vite-node/server"; +import { vitePluginPreBundle } from ".."; import { name as packageName } from "../../package.json"; import { setupViteNodeServerRpc } from "./vite-node"; @@ -18,12 +20,16 @@ export function vitePluginViteNodeMiniflare(pluginOptions: { miniflareOptions?: (options: MiniflareOptions) => void; viteNodeServerOptions?: (options: ViteNodeServerOptions) => void; viteNodeRunnerOptions?: (options: Partial) => void; -}): Plugin { + preBundle?: { + include: string[]; + force?: boolean; + }; +}): Plugin[] { // initialize miniflare lazily on first request and // dispose on server close (e.g. server restart on user vite config change) let miniflare: Miniflare | undefined; - return { + const middlewarePlugin: Plugin = { name: packageName, apply: "serve", async configureServer(server) { @@ -88,4 +94,9 @@ export function vitePluginViteNodeMiniflare(pluginOptions: { } }, }; + + return [ + middlewarePlugin, + pluginOptions.preBundle && vitePluginPreBundle(pluginOptions.preBundle), + ].filter(typedBoolean); } diff --git a/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts b/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts index af7761ad9..d7475239f 100644 --- a/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts +++ b/packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts @@ -5,8 +5,6 @@ import { type Plugin, type ResolvedConfig } from "vite"; import { name as packageName } from "../../../package.json"; import { PreBundler } from "./utils"; -// TODO: include this plugin in main one vitePluginViteNodeMiniflare via preBundle options? - export function vitePluginPreBundle(pluginOptions: { include: string[]; force?: boolean; @@ -18,6 +16,7 @@ export function vitePluginPreBundle(pluginOptions: { return { name, enforce: "pre", + apply: "serve", configResolved(config_) { config = config_; },