Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vite-node-miniflare): cache pre-bundle #139

Merged
merged 3 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(),
],
Expand Down
12 changes: 5 additions & 7 deletions packages/vite-node-miniflare/examples/react/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(),
],
Expand Down
15 changes: 13 additions & 2 deletions packages/vite-node-miniflare/src/server/plugin.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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";

Expand All @@ -18,12 +20,16 @@ export function vitePluginViteNodeMiniflare(pluginOptions: {
miniflareOptions?: (options: MiniflareOptions) => void;
viteNodeServerOptions?: (options: ViteNodeServerOptions) => void;
viteNodeRunnerOptions?: (options: Partial<ViteNodeRunnerOptions>) => 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) {
Expand Down Expand Up @@ -88,4 +94,9 @@ export function vitePluginViteNodeMiniflare(pluginOptions: {
}
},
};

return [
middlewarePlugin,
pluginOptions.preBundle && vitePluginPreBundle(pluginOptions.preBundle),
].filter(typedBoolean);
}
24 changes: 14 additions & 10 deletions packages/vite-node-miniflare/src/server/pre-bundle/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,39 @@ 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";

// TODO: cache
import { PreBundler } from "./utils";

export function vitePluginPreBundle(pluginOptions: {
include: string[];
force?: boolean;
}): Plugin {
let alias: Record<string, string>;
let config: ResolvedConfig;
const name = `${packageName}/pre-bundle`;
let config: ResolvedConfig;
let preBundler: PreBundler;

return {
name,
enforce: "pre",
apply: "serve",
configResolved(config_) {
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 (!pluginOptions.force && 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;
},
};
}
79 changes: 53 additions & 26 deletions packages/vite-node-miniflare/src/server/pre-bundle/utils.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<string, { in: string; out: string }> = {};
alias: Record<string, string> = {};
hashValue: string;
hashPath: string;

const srcDir = path.join(outDir, ".tmp");
const entries: Record<string, string> = {};
const alias: Record<string, string> = {};
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;
}
}