UNPKG

dinou

Version:

Dinou is a modern React 19 framework with React Server Components, Server Functions, and streaming SSR.

311 lines (275 loc) 9.84 kB
require("dotenv/config"); require("./register-paths"); const webpackRegister = require("react-server-dom-webpack/node-register"); const path = require("path"); const { readFileSync, existsSync, createReadStream } = require("fs"); const { renderToPipeableStream } = require("react-server-dom-webpack/server"); const express = require("express"); const getSSGJSXOrJSX = require("./get-ssg-jsx-or-jsx.js"); const { getErrorJSX } = require("./get-error-jsx.js"); const addHook = require("./asset-require-hook.js"); const { extensions } = require("./asset-extensions.js"); webpackRegister(); const babelRegister = require("@babel/register"); babelRegister({ ignore: [/[\\\/](build|server|node_modules)[\\\/]/], presets: [ ["@babel/preset-react", { runtime: "automatic" }], "@babel/preset-typescript", ], plugins: ["@babel/transform-modules-commonjs"], extensions: [".js", ".jsx", ".ts", ".tsx"], }); const createScopedName = require("./createScopedName"); require("css-modules-require-hook")({ generateScopedName: createScopedName, }); addHook({ extensions, name: function (localName, filepath) { const result = createScopedName(localName, filepath); return result + ".[ext]"; }, publicPath: "/assets/", }); const importModule = require("./import-module"); const generateStatic = require("./generate-static.js"); const renderAppToHtml = require("./render-app-to-html.js"); const revalidating = require("./revalidating.js"); const isDevelopment = process.env.NODE_ENV !== "production"; const outputFolder = isDevelopment ? "public" : "dist3"; const chokidar = require("chokidar"); const { fileURLToPath } = require("url"); if (isDevelopment) { const manifestPath = path.resolve( process.cwd(), `${outputFolder}/react-client-manifest.json` ); let currentManifest = {}; const watcher = chokidar.watch(manifestPath, { persistent: true }); let isInitial = true; watcher.on("add", () => { if (Object.keys(currentManifest).length === 0 && isInitial) { // console.log("Initial manifest loaded."); currentManifest = JSON.parse(readFileSync(manifestPath, "utf8")); isInitial = false; return; } }); function getParents(resolvedPath) { const parents = []; Object.values(require.cache).forEach((mod) => { if ( mod.children && mod.children.some((child) => child.id === resolvedPath) ) { parents.push(mod.id); } }); return parents; } function clearRequireCache(modulePath, visited = new Set()) { try { const resolved = require.resolve(modulePath); if (visited.has(resolved)) return; visited.add(resolved); if (require.cache[resolved]) { delete require.cache[resolved]; // console.log(`[Server HMR] Cleared cache for ${resolved}`); const parents = getParents(resolved); for (const parent of parents) { // Optional: Skip if parent not in src/ (safety) if (parent.startsWith(path.resolve(process.cwd(), "src"))) { clearRequireCache(parent, visited); } } } } catch (err) { console.warn( `[Server HMR] Could not resolve or clear ${modulePath}: ${err.message}` ); } } watcher.on("change", () => { try { const newManifest = JSON.parse(readFileSync(manifestPath, "utf8")); // Handle removed entries: client -> server switch for (const key in currentManifest) { if (!(key in newManifest)) { const absPath = fileURLToPath(key); clearRequireCache(absPath); // console.log(`Cleared cache for ${absPath} (client -> server)`); } } // Handle added entries: server -> client switch for (const key in newManifest) { if (!(key in currentManifest)) { const absPath = fileURLToPath(key); clearRequireCache(absPath); // console.log(`Cleared cache for ${absPath} (server -> client)`); } } currentManifest = newManifest; } catch (err) { console.error("Error handling manifest change:", err); } }); const srcWatcher = chokidar.watch(path.resolve(process.cwd(), "src"), { persistent: true, ignored: /node_modules/, }); srcWatcher.on("change", (changedPath) => { const posixPath = changedPath.split(path.sep).join(path.posix.sep); const isClientComponent = Object.keys(currentManifest).some((key) => key.includes(posixPath) ); if (!isClientComponent) { clearRequireCache(changedPath); // console.log( // `[Server HMR] Cleared cache for ${changedPath} in srcWatcher` // ); } }); } const cookieParser = require("cookie-parser"); const appUseCookieParser = cookieParser(); const app = express(); app.use(appUseCookieParser); app.use(express.json()); app.use(express.static(path.resolve(process.cwd(), outputFolder))); app.get("/.well-known/appspecific/com.chrome.devtools.json", (req, res) => { res.setHeader("Content-Type", "application/json"); res.json({ name: "Dinou DevTools", description: "Dinou DevTools for Chrome", version: "1.0.0", devtools_page: `/${outputFolder}/devtools.html`, }); }); app.get(/^\/____rsc_payload____\/.*\/?$/, async (req, res) => { try { const reqPath = ( req.path.endsWith("/") ? req.path : req.path + "/" ).replace("/____rsc_payload____", ""); if (!isDevelopment && Object.keys({ ...req.query }).length === 0) { const payloadPath = path.join("dist2", reqPath, "rsc.rsc"); if (existsSync(payloadPath)) { res.setHeader("Content-Type", "application/octet-stream"); const readStream = createReadStream(payloadPath); readStream.on("error", (err) => { console.error("Error reading RSC file:", err); res.status(500).end(); }); return readStream.pipe(res); } } const jsx = await getSSGJSXOrJSX( reqPath, { ...req.query }, { ...req.cookies }, isDevelopment ); const manifest = JSON.parse( readFileSync( path.resolve(`${outputFolder}/react-client-manifest.json`), "utf8" ) ); const { pipe } = renderToPipeableStream(jsx, manifest); pipe(res); } catch (error) { console.error("Error rendering RSC:", error); res.status(500).send("Internal Server Error"); } }); app.post(/^\/____rsc_payload_error____\/.*\/?$/, async (req, res) => { try { const reqPath = ( req.path.endsWith("/") ? req.path : req.path + "/" ).replace("/____rsc_payload_error____", ""); const jsx = await getErrorJSX(reqPath, { ...req.query }, req.body.error); const manifest = readFileSync( path.resolve(process.cwd(), `${outputFolder}/react-client-manifest.json`), "utf8" ); const moduleMap = JSON.parse(manifest); const { pipe } = renderToPipeableStream(jsx, moduleMap); pipe(res); } catch (error) { console.error("Error rendering RSC:", error); res.status(500).send("Internal Server Error"); } }); app.get(/^\/.*\/?$/, (req, res) => { try { const reqPath = req.path.endsWith("/") ? req.path : req.path + "/"; if (!isDevelopment && Object.keys({ ...req.query }).length === 0) { revalidating(reqPath); const htmlPath = path.join("dist2", reqPath, "index.html"); if (existsSync(htmlPath)) { res.setHeader("Content-Type", "text/html"); return createReadStream(htmlPath).pipe(res); } } const appHtmlStream = renderAppToHtml( reqPath, JSON.stringify({ ...req.query }), JSON.stringify({ ...req.cookies }) ); res.setHeader("Content-Type", "text/html"); appHtmlStream.pipe(res); appHtmlStream.on("error", (error) => { console.error("Stream error:", error); res.status(500).send("Internal Server Error"); }); } catch (error) { console.error("Error rendering React app:", error); res.status(500).send("Internal Server Error"); } }); app.post("/____server_function____", async (req, res) => { try { const { id, args } = req.body; const [fileUrl, exportName] = id.split("#"); let relativePath = fileUrl.replace(/^file:\/\/\/?/, ""); const absolutePath = path.resolve(process.cwd(), relativePath); const mod = await importModule(absolutePath); const fn = exportName === "default" ? mod.default : mod[exportName]; if (typeof fn !== "function") { return res.status(400).json({ error: "Export is not a function" }); } const context = { req, res }; args.push(context); const result = await fn(...args); if ( result && result.$$typeof === Symbol.for("react.transitional.element") ) { res.setHeader("Content-Type", "text/x-component"); const manifest = readFileSync( path.resolve( process.cwd(), `${outputFolder}/react-client-manifest.json` ), "utf8" ); const moduleMap = JSON.parse(manifest); const { pipe } = renderToPipeableStream(result, moduleMap); pipe(res); } else { res.json(result); } } catch (err) { console.error(`Server function error [${req.body.id}]:`, err); res.status(500).json({ error: err.message }); } }); const port = process.env.PORT || 3000; app.listen(port, async () => { if (!isDevelopment) { await generateStatic(); } else { console.log("⚙️ Rendering dynamically in dev mode"); } console.log(`Listening on port ${port}`); });