import { build as viteBuild, InlineConfig } from 'vite'; import type { RollupOutput } from 'rollup'; import { CLIENT_ENTRY_PATH, SERVER_ENTRY_PATH } from './constants'; import path, { join, dirname } from 'path'; import fs from 'fs-extra'; // import ora from 'ora'; import { SiteConfig } from 'types'; import { createVitePlugins } from './vitePlugins'; import { Route } from './plugin-routes'; import { HelmetData } from 'react-helmet-async'; const CLIENT_OUTPUT = 'build'; export async function bundle(root: string, config: SiteConfig) { const resolveViteConfig = async (isServer: boolean): Promise => ({ mode: 'production', root, plugins: await createVitePlugins(config, undefined, isServer), ssr: { noExternal: ['react-router-dom', 'loadsh-es', 'react-helmet-async'] }, build: { minify: false, ssr: isServer, outDir: isServer ? path.join(root, '.temp') : path.join(root, CLIENT_OUTPUT), rollupOptions: { input: isServer ? SERVER_ENTRY_PATH : CLIENT_ENTRY_PATH, output: { format: 'esm' }, onwarn(warning, warn) { if (warning.message.includes('dynamically imported')) { return; } } } } }); // const spinner = ora(); // spinner.start(`Building client + server bundles...`); try { const [clientBundle, serverBundle] = await Promise.all([ // client build viteBuild(await resolveViteConfig(false)), // server build viteBuild(await resolveViteConfig(true)) ]); const publicDir = join(root, 'markdown/public'); if (fs.pathExistsSync(publicDir)) { await fs.copy(publicDir, join(root, CLIENT_OUTPUT)); } return [clientBundle, serverBundle] as [RollupOutput, RollupOutput]; } catch (e) { console.log(e); } } export async function renderPage( render: (url: string, helmetContext: object) => string, routes: Route[], root: string, clientBundle: RollupOutput ) { console.log('Rendering page in server side...'); const clientChunk = clientBundle.output.find((chunk) => chunk.type === 'chunk' && chunk.isEntry); const styleAssets = clientBundle.output.filter( (chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css') ); return Promise.all( [ ...routes, { path: '/404' } ].map(async (route) => { const helmetContext = { context: {} } as HelmetData; const routePath = route.path; const appHtml = await render(routePath, helmetContext); const { helmet } = helmetContext.context; const html = ` ${helmet?.title?.toString() || ''} ${helmet?.meta?.toString() || ''} ${helmet?.link?.toString() || ''} ${helmet?.style?.toString() || ''} ${styleAssets.map((item) => ``).join('\n')}
${appHtml}
`.trim(); const fileName = routePath.endsWith('/') ? `${routePath}index.html` : `${routePath}.html`; await fs.ensureDir(join(root, 'build', dirname(fileName))); await fs.writeFile(join(root, 'build', fileName), html); }) ); } export async function build(root: string = process.cwd(), config: SiteConfig) { // 1. bundle: generate client + server bundles const [clientBundle] = await bundle(root, config); // 2. put client bundle to build folder const serverEntryPath = join(root, '.temp', 'renderServer.js'); const { render, routes } = await import(serverEntryPath); // 3. render page try { await renderPage(render, routes, root, clientBundle); } catch (e) { console.log('Render page error.\n', e); } }