UNPKG

rasengan

Version:

The modern React Framework

243 lines (242 loc) 11.4 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { ManifestManager } from '../build/manifest.js'; import fs from 'node:fs'; import path from 'node:path'; import { render, } from '../../entries/server/entry.server.js'; import { generateRoutes, getAllRoutesPath, preloadMatches, } from '../../routing/utils/index.js'; import { createStaticHandler, createStaticRouter, StaticRouterProvider, } from 'react-router'; import createRasenganRequest, { convertSecondsToMinutes, createFakeRasenganRequest, filterRoutesForPrerender, logRenderedPagesGrouped, } from './utils.js'; import { extractHeadersFromRRContext, extractMetaFromRRContext, isRedirectResponse, isStaticRedirectFromConfig, } from '../dev/utils.js'; import { handleRedirectRequest } from '../dev/handlers.js'; import { resolvePath } from '../../core/config/utils/path.js'; import ora from 'ora'; import { loadModuleSSR } from '../../core/config/utils/load-modules.js'; import chalk from 'chalk'; // Spinner const spinner = (text) => ora({ text, spinner: 'dots', color: 'blue', }); /** * This function is responsible for creating a request handler for the server. * @param options * @returns */ export function createRequestHandler(options) { const { build: buildOptions } = options; const manifest = new ManifestManager(path.posix.join(buildOptions.buildDirectory, buildOptions.clientPathDirectory, buildOptions.manifestPathDirectory, 'manifest.json')); return async function requestHandler(req, res) { try { // Get server entry const entry = await import(resolvePath(path.posix.join(buildOptions.buildDirectory, buildOptions.serverPathDirectory, buildOptions.entryServerPath))); // Get AppRouter const AppRouter = await (await import(resolvePath(path.posix.join(buildOptions.buildDirectory, buildOptions.serverPathDirectory, 'app.router.js')))).default; // Get Config const configPath = path.posix.join(buildOptions.buildDirectory, buildOptions.clientPathDirectory, buildOptions.assetPathDirectory, 'config.json'); const configPathExist = fs.existsSync(configPath); if (!configPathExist) { throw new Error('No config.json file found in dist/client/assets, please make a build again by running "npm run build"'); } // Read the config.json file const configData = fs.readFileSync(configPath, 'utf-8').toString(); // Parse the config.json file const config = JSON.parse(configData); // extract render function const { render, } = entry; // Get static routes const staticRoutes = generateRoutes(AppRouter); // Preload matches await preloadMatches(req.originalUrl, staticRoutes); // Create static handler let handler = createStaticHandler(staticRoutes); // Create rasengan request for static routing let request = createRasenganRequest(req, res); let context = await handler.query(request); const redirectFound = await isStaticRedirectFromConfig(req, config.redirects); if (isRedirectResponse(context) || redirectFound) { return await handleRedirectRequest(req, res, { context, redirects: config.redirects, }); } if (!(context instanceof Response)) { // Extract meta from context const metadata = extractMetaFromRRContext(context); // Get the source file from the context const source = context.loaderData.source; // Get assets tags const assets = manifest.generateMetaTags(source); // Create static router let router = createStaticRouter(handler.dataRoutes, context); const headers = extractHeadersFromRRContext(context); const Router = (_jsx(StaticRouterProvider, { router: router, context: context })); // If stream mode enabled, render the page as a plain text return await render(Router, res, { metadata, assets, buildOptions, statusCode: context.statusCode, responseHeaders: Object.fromEntries(headers), }); } return context; } catch (error) { console.error(error); res.status(500).send('Internal Server Error'); } }; } /** * This function prerenders all Rasengan routes into static HTML files. * It replaces the need for a runtime server and allows deployment to a CDN. */ export async function preRenderApp(options) { try { const { build: buildOptions, outDir = 'static' } = options; // Start timer const start = Date.now(); const createSpinner = spinner('Starting static pre-rendering...'); // Ensure .rasengan directory exists const logDir = '.rasengan'; if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } // Redirect console.log to a file const logStream = fs.createWriteStream(`${logDir}/prerender.log`, { flags: 'a', }); const originalLog = console.log; console.log = (...args) => { logStream.write(args.join(' ') + '\n'); }; createSpinner.start(); // Locate the assets directory const clientDir = path.posix.join(buildOptions.buildDirectory, buildOptions.clientPathDirectory); if (!fs.existsSync(clientDir)) { throw new Error('No "dist/client" directory found. Please make sure to run "rasengan build".'); } // Prepare the static directory fs.mkdirSync(outDir, { recursive: true }); // Read the content of dist/client const items = fs.readdirSync(clientDir, { recursive: true }); const files = []; items.forEach((item) => { const fullPath = path.posix.join(path.resolve(clientDir), item); const stat = fs.statSync(fullPath); if (stat.isFile()) { files.push(item); } else { // Create this folder fs.mkdirSync(path.posix.join(path.resolve(outDir), item), { recursive: true, }); } }); for (const file of files) { const src = path.posix.join(clientDir, file); const dest = path.posix.join(outDir, file); fs.copyFileSync(src, dest); } // 1. Load build manifest const manifest = new ManifestManager(path.posix.join(clientDir, buildOptions.manifestPathDirectory, 'manifest.json')); // 2. Load app-router const AppRouter = await (await loadModuleSSR(path.posix.join(buildOptions.buildDirectory, buildOptions.serverPathDirectory, 'app.router.js'))).default; // 3. Load App Config const configPath = path.posix.join(clientDir, // dist or dist/client buildOptions.assetPathDirectory, 'config.json'); const configData = fs.readFileSync(configPath, 'utf-8').toString(); const config = JSON.parse(configData); // 4. Generate static routes const staticRoutes = generateRoutes(AppRouter); // Extracting all routes available const { paths: routes, error: staticError } = await getAllRoutesPath(staticRoutes); let routesToPrerender = routes; if (options.routes.length > 0) { routesToPrerender = filterRoutesForPrerender(options.routes, routes); } const generatedFiles = []; // 5. Loop through routes and render them to HTML for (const route of routesToPrerender) { const pathname = route === '/' ? '/' : `${route}/`; // console.log(`🧩 Rendering ${pathname}`); createSpinner.text = `Rendering ${pathname}`; // Simulate fake request & response const { req: fakeReq, res: fakeRes } = createFakeRasenganRequest(pathname); // Preload data await preloadMatches(pathname, staticRoutes); // Create static handler const handler = createStaticHandler(staticRoutes); const request = createRasenganRequest(fakeReq, fakeRes); const context = await handler.query(request); const redirectFound = await isStaticRedirectFromConfig(fakeReq, config.redirects); if (redirectFound) { createSpinner.text = `Skipping redirect route: ${pathname}`; continue; } if (!(context instanceof Response)) { const metadata = extractMetaFromRRContext(context); const source = context.loaderData.source; const assets = manifest.generateMetaTags(source); const router = createStaticRouter(handler.dataRoutes, context); const Router = (_jsx(StaticRouterProvider, { router: router, context: context })); // Capture the HTML as string const html = await render(Router, null, { metadata, assets, buildOptions, }, false); let outputDir = ''; // Write to disk if (route.includes('*')) { // Generate a 404.html outputDir = outDir; fs.mkdirSync(outputDir, { recursive: true }); fs.writeFileSync(path.join(outputDir, '404.html'), html); generatedFiles.push('static/404.html'); } else { outputDir = path.join(outDir, route || 'index'); fs.mkdirSync(outputDir, { recursive: true }); fs.writeFileSync(path.join(outputDir, 'index.html'), html); const splittedOutputDir = outputDir.split('static/'); generatedFiles.push(`static/${splittedOutputDir[1] ? splittedOutputDir[1] + '/' : ''}index.html`); } } } // End timer const end = Date.now(); createSpinner.succeed(`${chalk.green(`${generatedFiles.length} page(s) successfully rendered in ${convertSecondsToMinutes((end - start) / 1000)}`)}`); createSpinner.stop(); console.log = originalLog; // Log SSG outputs logRenderedPagesGrouped(generatedFiles); if (staticError.size > 0) { console.log(`❌ Some error(s) found: \n${Array.from(staticError).join('\n')}`); } return { // Check if the index page is prerendered isIndexPrerendered: routesToPrerender.some((route) => route === '/'), }; } catch (error) { console.error(error); throw error; } } /** * This function is responsible for handling the document request. * @param req * @param res * @returns */ export function handleDocumentRequest() { } /** * This function is responsible for handling the data request. * @param req * @param res * @returns */ export function handleDataRequest() { }