rasengan
Version:
The modern React Framework
243 lines (242 loc) • 11.4 kB
JavaScript
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() { }