UNPKG

vite-plugin-react-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>

257 lines (232 loc) 8.45 kB
import { build as viteBuild } from 'vite' import type { ResolvedConfig, Rollup } from 'vite' import { minify } from 'html-minifier-terser' import * as path from 'path' import fs from 'fs-extra' import { pathToFileURL } from 'node:url' import { CLIENT_PATH } from '../constants' import type { SSRPlugin } from '../../../clientTypes' import type { staticSiteGenerationConfig } from '../types' type RollupOutput = Rollup.RollupOutput const minifyOptions = { keepClosingSlash: true, removeRedundantAttributes: true, removeStyleLinkTypeAttributes: true, useShortDoctype: true, minifyCSS: true, } export async function ssrBuild( viteConfig: ResolvedConfig, argv: any, ssrConfig?: staticSiteGenerationConfig ) { // 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, // mode: "development", build: { ssr: true, cssCodeSplit: false, rollupOptions: { input: path.join(CLIENT_PATH, 'entries', 'ssg-server.mjs'), // preserveEntrySignatures: 'allow-extension', output: { entryFileNames: '[name].mjs', chunkFileNames: '[name]-[hash].mjs', }, onwarn(warning, defaultHandler) { // suppress warning like: /@react-pages/pages/guide/react/getting-started is dynamically imported by /@react-pages/pages but also statically imported by /@react-pages/ssrData, dynamic import will not move module into another chunk. if ( warning.plugin === 'vite:reporter' && warning.message.includes('/@react-pages/ssrData') && warning.message.includes( 'dynamic import will not move module into another chunk' ) ) return defaultHandler(warning) }, }, outDir: ssrOutDir, minify: false, }, ssr: { // `vite-pages-theme-doc/dist/index.js` have `import './index.css'` // so it needs to be bundled by vite before executed by node.js. // This is coupled to theme-doc, // but we don't want to ask users to put this in their vite config. // So let's put it here :) noExternal: ['vite-pages-theme-doc'], }, }) console.log('\n\nrendering html...') const ssrPluginPromises: Promise<SSRPlugin>[] = [] ;(global as any)['register_vite_pages_ssr_plugin'] = ( importSSRPlugin: () => Promise<SSRPlugin> ) => { ssrPluginPromises.push(importSSRPlugin()) } process.env.VITE_PAGES_IS_SSR = 'true' const { renderToString, ssrData } = await import( pathToFileURL(path.join(ssrOutDir, 'ssg-server.mjs')).toString() ) const ssrPlugins = await Promise.all(ssrPluginPromises) ssrPlugins.forEach((plugin, index) => { // validate ssr plugins if (!plugin?.id) { console.error('invalid ssr plugins:', ssrPlugins) throw new Error('invalid ssr plugin: no plugin id') } const idx = ssrPlugins.findIndex((p) => p.id === plugin.id) if (idx !== index) { console.error('invalid ssr plugins:', ssrPlugins) throw new Error(`duplicate ssr plugin: ${plugin.id}`) } }) 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, 'entries', 'ssg-client.mjs'), 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 = await 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 = await 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 async function renderHTML(pagePath: string) { const { contentText, styleText } = renderToString(pagePath, ssrPlugins) 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">${contentText}</div>` ) const cssInject = cssChunks.map((cssChunk) => { return `<link rel="stylesheet" href="${basePath}${cssChunk.fileName}" />` }) cssInject.push(styleText) html = html.replace( CSSInjectPoint, `${cssInject.join('\n')} ${CSSInjectPoint}` ) html = html.replace( EntryModuleInjectPoint, `<script type="module" src="${basePath}${entryChunk.fileName}"></script>` ) const minifyHtml = argv?.minifyHtml ?? ssrConfig?.minifyHtml ?? true if (minifyHtml) { const minifiedHtml = await minify(html, minifyOptions) return minifiedHtml } 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 } }