UNPKG

vite-plugin-react18-pages

Version:

<p> <a href="https://www.npmjs.com/package/vite-plugin-react-pages" target="_blank" rel="noopener"><img src="https://img.shields.io/npm/v/vite-plugin-react-pages.svg" alt="npm package" /></a> </p>

205 lines (188 loc) 6.39 kB
import * as fs from "fs-extra"; import * as path from "path"; import type { RollupOutput } from "rollup"; import type { ResolvedConfig } from "vite"; import { build as viteBuild } from "vite"; import { CLIENT_PATH } from "../constants"; export async function ssrBuild( viteConfig: ResolvedConfig, ssrConfig: any, argv: any ) { // ssr build should not use hash router // if (viteOptions?.define?.['__HASH_ROUTER__']) // viteOptions!.define!['__HASH_ROUTER__'] = false const root = viteConfig.root; let outDir = viteConfig.build?.outDir ?? "dist"; outDir = path.resolve(root, outDir); await fs.emptyDir(outDir); const ssrOutDir = path.join(outDir, "ssr-tmp"); const clientOutDir = path.join(outDir, "client-tmp"); console.log("\n\npreparing vite pages ssr bundle..."); const ssrOutput = await viteBuild({ root, configFile: viteConfig.configFile, build: { ssr: true, cssCodeSplit: false, rollupOptions: { input: path.join(CLIENT_PATH, "ssr", "serverRender.js"), preserveEntrySignatures: "allow-extension", output: { format: "cjs", exports: "named", entryFileNames: "[name].js", }, }, outDir: ssrOutDir, minify: false, }, // @ts-ignore ssr: { external: ["react", "react-router-dom", "react-dom", "react-dom/server"], noExternal: [ // TODO: remove this "vite-pages-theme-basic", "vite-plugin-react18-pages", "vite-plugin-react18-pages/client", ], }, }); console.log("\n\nrendering html..."); const { renderToString, ssrData } = require(path.join( ssrOutDir, "serverRender.js" )); const pagePaths = Object.keys(ssrData); console.log("\n\npreparing vite pages client bundle..."); const _clientResult = await viteBuild({ root, configFile: viteConfig.configFile, build: { cssCodeSplit: false, rollupOptions: { input: path.join(CLIENT_PATH, "ssr", "clientRender.js"), preserveEntrySignatures: "allow-extension", }, assetsDir: "assets", outDir: clientOutDir, }, }); let clientResult: RollupOutput; if (Array.isArray(_clientResult)) { if (_clientResult.length !== 1) throw new Error(`expect viteBuild to have only one BuildResult`); clientResult = _clientResult[0]; } else { clientResult = _clientResult as RollupOutput; } const entryChunk = (() => { const _entryChunks = clientResult.output.filter((chunkOrAsset) => { return chunkOrAsset.type === "chunk" && chunkOrAsset.isEntry; }); if (_entryChunks.length !== 1) { throw new Error(`Expect one entryChunk. Got ${_entryChunks.length}.`); } return _entryChunks[0]; })(); const cssChunks = clientResult.output.filter((chunk) => { return chunk.type === "asset" && chunk.fileName.endsWith(".css"); }); const basePath = viteConfig.base ?? "/"; const htmlCode = await fs.readFile(path.join(root, "index.html"), "utf-8"); const RootElementInjectPoint = '<div id="root"></div>'; if (!htmlCode.includes(RootElementInjectPoint)) { throw new Error( `Your index.html should contain the RootElementInjectPoint: "${RootElementInjectPoint}" (it must appear exactly as-is)` ); } const EntryModuleInjectPoint = '<script type="module" src="/@pages-infra/main.js"></script>'; if (!htmlCode.includes(EntryModuleInjectPoint)) { throw new Error( `Your index.html should contain EntryModuleInjectPoint: "${EntryModuleInjectPoint}" (it must appear exactly as-is)` ); } const CSSInjectPoint = "</head>"; if (!htmlCode.includes(CSSInjectPoint)) { throw new Error( `Your index.html should contain CSSInjectPoint: "${CSSInjectPoint}" (it must appear exactly as-is)` ); } await Promise.all( pagePaths.map(async (pagePath) => { // currently not support pages with path params // .e.g /users/:userId if (pagePath.match(/\/:\w/)) return; const html = renderHTML(pagePath); // TODO: injectPreload // preload data module for this page // html = injectPreload(html, "path/to/page/data") const writePath = path.join( clientOutDir, pagePath.replace(/^\//, ""), "index.html" ); await fs.ensureDir(path.dirname(writePath)); await fs.writeFile(writePath, html); if (pagePath !== "/") { // should write to both /pagePath/index.html and /pagePath.html const writePath2 = path.join( clientOutDir, pagePath.replace(/^\//, "") + ".html" ); await fs.ensureDir(path.dirname(writePath2)); await fs.writeFile(writePath2, html); } }) ); const html404Path = path.join(clientOutDir, "404.html"); // pass in a pagePath that won't match any defined page // so the render result will be 404 page const html404 = renderHTML("/internal-404-page"); await fs.writeFile(html404Path, html404); // move 404 page to `/` if `/` doesn't exists if (!pagePaths.includes("/")) { await fs.copy(html404Path, path.join(clientOutDir, "index.html")); } await fs.copy(clientOutDir, outDir); await fs.remove(clientOutDir); await fs.remove(ssrOutDir); console.log("vite pages ssr build finished successfully."); return; function renderHTML(pagePath: string) { const ssrContent = renderToString(pagePath); const ssrInfo = { routePath: pagePath, }; let html = htmlCode.replace( RootElementInjectPoint, // let client know the current ssr page `<script>window._vitePagesSSR=${JSON.stringify(ssrInfo)};</script> <div id="root">${ssrContent}</div>` ); const cssInject = cssChunks .map((cssChunk) => { return `<link rel="stylesheet" href="${basePath}${cssChunk.fileName}" />`; }) .join("\n"); html = html.replace( CSSInjectPoint, `${cssInject} ${CSSInjectPoint}` ); html = html.replace( EntryModuleInjectPoint, `<script type="module" src="${basePath}${entryChunk.fileName}"></script>` ); return html; } } const injectPreload = (html: string, filePath: string) => { const tag = `<link rel="modulepreload" href="${filePath}" />`; if (/<\/head>/.test(html)) { return html.replace(/<\/head>/, `${tag}\n</head>`); } else { return tag + "\n" + html; } };