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
text/typescript
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;
}
};