diff --git a/examples/react-server/src/features/utils/plugin.ts b/examples/react-server/src/features/utils/plugin.ts new file mode 100644 index 00000000..22bed0d4 --- /dev/null +++ b/examples/react-server/src/features/utils/plugin.ts @@ -0,0 +1,126 @@ +import { parseAstAsync, type Plugin } from "vite"; +import fs from "node:fs"; +import path from "node:path"; + +export function vitePluginSilenceDirectiveBuildWarning(): Plugin { + return { + name: vitePluginSilenceDirectiveBuildWarning.name, + apply: "build", + enforce: "post", + config: (config, _env) => ({ + build: { + rollupOptions: { + onwarn(warning, defaultHandler) { + if ( + warning.code === "SOURCEMAP_ERROR" && + warning.message.includes("(1:0)") + ) { + return; + } + if ( + warning.code === "MODULE_LEVEL_DIRECTIVE" && + (warning.message.includes(`"use client"`) || + warning.message.includes(`"use server"`)) + ) { + return; + } + if (config.build?.rollupOptions?.onwarn) { + config.build.rollupOptions.onwarn(warning, defaultHandler); + } else { + defaultHandler(warning); + } + }, + }, + }, + }), + }; +} + +// +// plugin utils +// + +export function createVirtualPlugin(name: string, load: Plugin["load"]) { + name = "virtual:" + name; + return { + name: `virtual-${name}`, + resolveId(source, _importer, _options) { + return source === name ? "\0" + name : undefined; + }, + load(id, options) { + if (id === "\0" + name) { + return (load as any).apply(this, [id, options]); + } + }, + } satisfies Plugin; +} + +// +// code transform utils +// + +export async function parseExports(code: string) { + const ast = await parseAstAsync(code); + const exportNames = new Set(); + let writableCode = code; + for (const node of ast.body) { + // named exports + if (node.type === "ExportNamedDeclaration") { + if (node.declaration) { + if ( + node.declaration.type === "FunctionDeclaration" || + node.declaration.type === "ClassDeclaration" + ) { + /** + * export function foo() {} + */ + exportNames.add(node.declaration.id.name); + } else if (node.declaration.type === "VariableDeclaration") { + /** + * export const foo = 1, bar = 2 + */ + if (node.declaration.kind === "const") { + const start = (node.declaration as any).start; + writableCode = replaceCode(writableCode, start, start + 5, "let "); + } + for (const decl of node.declaration.declarations) { + if (decl.id.type === "Identifier") { + exportNames.add(decl.id.name); + } + } + } + } + } + } + return { + exportNames, + writableCode, + }; +} + +function replaceCode( + code: string, + start: number, + end: number, + content: string, +) { + return code.slice(0, start) + content + code.slice(end); +} + +// +// fs utils +// + +export async function traverseFiles( + dir: string, + callback: (filepath: string, e: fs.Dirent) => void, +) { + const entries = await fs.promises.readdir(dir, { withFileTypes: true }); + for (const e of entries) { + const filepath = path.join(e.path, e.name); + callback(filepath, e); + if (e.isDirectory()) { + await traverseFiles(filepath, callback); + } + } +} diff --git a/examples/react-server/vite.config.ts b/examples/react-server/vite.config.ts index e7fbc2fb..fd0cab14 100644 --- a/examples/react-server/vite.config.ts +++ b/examples/react-server/vite.config.ts @@ -4,12 +4,19 @@ import { type PluginOption, type Plugin, createServerModuleRunner, - parseAstAsync, } from "vite"; import { createDebug, tinyassert, typedBoolean } from "@hiogawa/utils"; import { __global } from "./src/global"; import react from "@vitejs/plugin-react"; import { vitePluginSsrMiddleware } from "../react-ssr/vite.config"; +import { + createVirtualPlugin, + parseExports, + traverseFiles, + vitePluginSilenceDirectiveBuildWarning, +} from "./src/features/utils/plugin"; +import fs from "node:fs"; +import path from "node:path"; const debug = createDebug("app"); @@ -23,7 +30,7 @@ export default defineConfig((_env) => ({ preview: new URL("./dist/server/index.js", import.meta.url).toString(), }), vitePluginReactServer(), - vitePluginSilenceUseClientBuildWarning(), + vitePluginSilenceDirectiveBuildWarning(), vitePluginServerAction(), ], @@ -170,20 +177,20 @@ function vitePluginUseClient(): PluginOption { }, }; - return [ - transformPlugin, - /* - [output] + /* + [output] - export default { - "": () => import(""), - ... - } + export default { + "": () => import(""), + ... + } - */ - createVirtualPlugin("client-reference", function () { + */ + const virtualPlugin: Plugin = createVirtualPlugin( + "client-reference", + function () { tinyassert(this.environment?.name !== "react-server"); - tinyassert(!this.meta.watchMode); + tinyassert(this.environment?.mode === "build"); return [ `export default {`, [...manager.clientReferences].map( @@ -191,42 +198,10 @@ function vitePluginUseClient(): PluginOption { ), `}`, ].join("\n"); - }), - ]; -} + }, + ); -function vitePluginSilenceUseClientBuildWarning(): Plugin { - return { - name: vitePluginSilenceUseClientBuildWarning.name, - apply: "build", - enforce: "post", - config: (config, _env) => ({ - build: { - rollupOptions: { - onwarn(warning, defaultHandler) { - if ( - warning.code === "SOURCEMAP_ERROR" && - warning.message.includes("(1:0)") - ) { - return; - } - if ( - warning.code === "MODULE_LEVEL_DIRECTIVE" && - (warning.message.includes(`"use client"`) || - warning.message.includes(`"use server"`)) - ) { - return; - } - if (config.build?.rollupOptions?.onwarn) { - config.build.rollupOptions.onwarn(warning, defaultHandler); - } else { - defaultHandler(warning); - } - }, - }, - }, - }), - }; + return [transformPlugin, virtualPlugin]; } function vitePluginServerAction(): PluginOption { @@ -283,10 +258,11 @@ function vitePluginServerAction(): PluginOption { } */ - const virtualServerReference: Plugin = { - apply: "build", - ...createVirtualPlugin("server-reference", async function () { + const virtualServerReference = createVirtualPlugin( + "server-reference", + async function () { tinyassert(this.environment?.name === "react-server"); + tinyassert(this.environment.mode === "build"); const files: string[] = []; await traverseFiles(path.resolve("./src"), (file, e) => { if (e.isFile()) { @@ -305,96 +281,8 @@ function vitePluginServerAction(): PluginOption { ...ids.map((id) => `"${id}": () => import("${id}"),\n`), "}", ].join("\n"); - }), - }; - - return [transformPlugin, virtualServerReference]; -} - -import fs from "node:fs"; -import path from "node:path"; - -async function traverseFiles( - dir: string, - callback: (filepath: string, e: fs.Dirent) => void, -) { - const entries = await fs.promises.readdir(dir, { withFileTypes: true }); - for (const e of entries) { - const filepath = path.join(e.path, e.name); - callback(filepath, e); - if (e.isDirectory()) { - await traverseFiles(filepath, callback); - } - } -} - -// -// plugin utils -// - -function createVirtualPlugin(name: string, load: Plugin["load"]) { - name = "virtual:" + name; - return { - name: `virtual-${name}`, - resolveId(source, _importer, _options) { - return source === name ? "\0" + name : undefined; - }, - load(id, options) { - if (id === "\0" + name) { - return (load as any).apply(this, [id, options]); - } }, - } satisfies Plugin; -} - -// -// ast utils -// + ); -async function parseExports(code: string) { - const ast = await parseAstAsync(code); - const exportNames = new Set(); - let writableCode = code; - for (const node of ast.body) { - // named exports - if (node.type === "ExportNamedDeclaration") { - if (node.declaration) { - if ( - node.declaration.type === "FunctionDeclaration" || - node.declaration.type === "ClassDeclaration" - ) { - /** - * export function foo() {} - */ - exportNames.add(node.declaration.id.name); - } else if (node.declaration.type === "VariableDeclaration") { - /** - * export const foo = 1, bar = 2 - */ - if (node.declaration.kind === "const") { - const start = (node.declaration as any).start; - writableCode = replaceCode(writableCode, start, start + 5, "let "); - } - for (const decl of node.declaration.declarations) { - if (decl.id.type === "Identifier") { - exportNames.add(decl.id.name); - } - } - } - } - } - } - return { - exportNames, - writableCode, - }; -} - -function replaceCode( - code: string, - start: number, - end: number, - content: string, -) { - return code.slice(0, start) + content + code.slice(end); + return [transformPlugin, virtualServerReference]; }