dinou 6.1.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +9461 -1080
  3. package/cli.js +392 -169
  4. package/dinou/adapters/bun.js +62 -0
  5. package/dinou/adapters/cloudflare.js +65 -0
  6. package/dinou/adapters/deno.js +48 -0
  7. package/dinou/adapters/netlify.js +85 -0
  8. package/dinou/adapters/node.mjs +31 -0
  9. package/dinou/bun/build.mjs +1077 -0
  10. package/dinou/cloudflare/build.mjs +1121 -0
  11. package/dinou/cloudflare/shims/child_process.js +14 -0
  12. package/dinou/cloudflare/shims/empty.js +2 -0
  13. package/dinou/cloudflare/shims/fs.js +99 -0
  14. package/dinou/cloudflare/shims/os.js +29 -0
  15. package/dinou/config.js +1 -0
  16. package/dinou/config.mjs +2 -0
  17. package/dinou/constants.js +29 -4
  18. package/dinou/core/babel-esm-loader.js +66 -37
  19. package/dinou/core/bailout-proxy.js +61 -0
  20. package/dinou/core/build-static-pages.js +309 -157
  21. package/dinou/core/cache-revalidate.js +471 -144
  22. package/dinou/core/cache-slot.js +387 -0
  23. package/dinou/core/client-redirect.jsx +12 -7
  24. package/dinou/core/client-webpack.jsx +457 -82
  25. package/dinou/core/client.jsx +452 -82
  26. package/dinou/core/config.js +11 -0
  27. package/dinou/core/dinou-paths.js +52 -0
  28. package/dinou/core/edge-ssr.js +122 -0
  29. package/dinou/core/export-static.mjs +64 -0
  30. package/dinou/core/generate-route-types.js +252 -0
  31. package/dinou/core/generate-static-page.js +55 -94
  32. package/dinou/core/generate-static-rsc.js +47 -15
  33. package/dinou/core/generate-static.js +212 -10
  34. package/dinou/core/generating-isg.js +5 -0
  35. package/dinou/core/get-asset-from-manifest.js +9 -3
  36. package/dinou/core/get-error-jsx.js +221 -41
  37. package/dinou/core/get-file-path-and-dynamic-params.js +9 -13
  38. package/dinou/core/get-jsx.js +172 -95
  39. package/dinou/core/handler.js +2296 -0
  40. package/dinou/core/http-adapter.js +115 -0
  41. package/dinou/core/import-module.js +83 -53
  42. package/dinou/core/layout-functions.js +181 -0
  43. package/dinou/core/link.jsx +18 -6
  44. package/dinou/core/load-dinou-config.js +60 -0
  45. package/dinou/core/manifest-provider.js +261 -0
  46. package/dinou/core/navigation-utils.js +4 -0
  47. package/dinou/core/navigation.js +50 -39
  48. package/dinou/core/parse-exports.js +207 -2
  49. package/dinou/core/ppr-context.js +105 -0
  50. package/dinou/core/ppr-runtime.js +263 -0
  51. package/dinou/core/redirect.jsx +20 -19
  52. package/dinou/core/render-jsx-to-client-jsx.js +133 -15
  53. package/dinou/core/request-context.js +47 -13
  54. package/dinou/core/resolve-module-ppr.js +32 -0
  55. package/dinou/core/revalidating.js +28 -9
  56. package/dinou/core/route-generator.js +107 -0
  57. package/dinou/core/rsc-renderer.js +90 -0
  58. package/dinou/core/run-ssg.js +19 -0
  59. package/dinou/core/scan-dependency-components.js +218 -0
  60. package/dinou/core/server-function-proxy-webpack.js +3 -0
  61. package/dinou/core/server-function-proxy.js +3 -0
  62. package/dinou/core/slot.js +302 -0
  63. package/dinou/core/status-manifest.js +41 -1
  64. package/dinou/core/storage-adapter.js +522 -0
  65. package/dinou/core/url-resolver.js +16 -16
  66. package/dinou/core/vfs.js +139 -68
  67. package/dinou/deno/build.mjs +1112 -0
  68. package/dinou/deno/seed-kv.js +115 -0
  69. package/dinou/esbuild/build.mjs +51 -1
  70. package/dinou/esbuild/dev.mjs +290 -224
  71. package/dinou/esbuild/helpers-esbuild/get-config-esbuild-prod.mjs +3 -2
  72. package/dinou/esbuild/helpers-esbuild/get-config-esbuild.mjs +104 -16
  73. package/dinou/esbuild/helpers-esbuild/get-esbuild-entries.mjs +31 -19
  74. package/dinou/esbuild/helpers-esbuild/strip-namespace.mjs +9 -0
  75. package/dinou/esbuild/helpers-esbuild/swc-disk-cache.mjs +225 -0
  76. package/dinou/esbuild/helpers-esbuild/update-manifest-for-module.mjs +7 -0
  77. package/dinou/esbuild/helpers-esbuild/write.mjs +111 -2
  78. package/dinou/esbuild/plugins-esbuild/assets-plugin.mjs +236 -161
  79. package/dinou/esbuild/plugins-esbuild/css-processor-plugin.mjs +101 -5
  80. package/dinou/esbuild/plugins-esbuild/react-client-manifest-plugin.mjs +63 -24
  81. package/dinou/esbuild/plugins-esbuild/server-functions-plugin.mjs +177 -56
  82. package/dinou/esbuild/plugins-esbuild/skip-missing-entry-points-plugin.mjs +3 -0
  83. package/dinou/esbuild/plugins-esbuild/stable-chunk-names-and-maps-plugin.mjs +182 -137
  84. package/dinou/esbuild/plugins-postcss/postcss-extract-plugin.js +38 -11
  85. package/dinou/esbuild/react-refresh/esm-hmr/client.mjs +55 -8
  86. package/dinou/esbuild/react-refresh/esm-hmr/server.js +1 -1
  87. package/dinou/esbuild/react-refresh/esm-hmr-plugin.mjs +238 -101
  88. package/dinou/esbuild/react-refresh/is-react-refresh-boundary.mjs +18 -4
  89. package/dinou/index.d.ts +315 -11
  90. package/dinou/index.js +12 -6
  91. package/dinou/index.mjs +9 -0
  92. package/dinou/node/build.mjs +195 -0
  93. package/dinou/node/bundle-dual-engine.mjs +1012 -0
  94. package/dinou/node/dev.mjs +2589 -0
  95. package/dinou/node/port-selector.mjs +164 -0
  96. package/dinou/node/terminal-status.mjs +345 -0
  97. package/dinou/package.json +17 -14
  98. package/dinou/rollup/react-refresh/esm-hmr/client.js +66 -10
  99. package/dinou/rollup/react-refresh/esm-hmr/server.js +1 -1
  100. package/dinou/rollup/react-refresh/is-react-refresh-boundary.js +24 -5
  101. package/dinou/rollup/react-refresh/react-refresh-runtime.js +13 -6
  102. package/dinou/rollup/react-refresh/react-refresh-wrap-modules.js +56 -11
  103. package/dinou/rollup/react-refresh/rollup-plugin-esm-hmr.js +176 -48
  104. package/dinou/rollup/rollup-plugins/dinou-asset-plugin.js +9 -5
  105. package/dinou/rollup/rollup-plugins/rollup-plugin-memory.js +66 -0
  106. package/dinou/rollup/rollup-plugins/rollup-plugin-react-client-manifest.js +468 -123
  107. package/dinou/rollup/rollup-plugins/rollup-plugin-server-functions.js +44 -12
  108. package/dinou/rollup/rollup-plugins/rollup-plugin-swc.js +100 -0
  109. package/dinou/rollup/rollup.config.js +155 -43
  110. package/dinou/server.d.ts +114 -12
  111. package/dinou/server.js +35 -10
  112. package/dinou/server.mjs +30 -9
  113. package/dinou/webpack/helpers/get-webpack-entries.js +14 -19
  114. package/dinou/webpack/loaders/server-functions-loader.js +22 -1
  115. package/dinou/webpack/loaders/swc-loader.js +76 -0
  116. package/dinou/webpack/plugins/manifest-generator-plugin.js +1 -0
  117. package/dinou/webpack/plugins/server-functions-plugin.js +75 -19
  118. package/dinou/webpack/plugins/webpack-memory-plugin.js +66 -0
  119. package/dinou/webpack/webpack.config.js +227 -29
  120. package/eject.js +173 -94
  121. package/package.json +110 -107
  122. package/dinou/core/asset-require-hook.js +0 -58
  123. package/dinou/core/client-error-webpack.jsx +0 -432
  124. package/dinou/core/client-error.jsx +0 -432
  125. package/dinou/core/concurrency-manager.js +0 -48
  126. package/dinou/core/context-proxy.js +0 -68
  127. package/dinou/core/generate-static-pages.js +0 -130
  128. package/dinou/core/generate-static-rscs.js +0 -151
  129. package/dinou/core/get-ssg-metadata.js +0 -31
  130. package/dinou/core/register-loader.mjs +0 -11
  131. package/dinou/core/register-paths.js +0 -27
  132. package/dinou/core/render-app-to-html.js +0 -430
  133. package/dinou/core/render-html.js +0 -407
  134. package/dinou/core/server.js +0 -1542
  135. package/dinou/esbuild/plugins-esbuild/write-metafile-plugin.mjs +0 -16
@@ -1,1542 +0,0 @@
1
- require("dotenv/config");
2
- const Module = require("module");
3
- const originalResolveFilename = Module._resolveFilename;
4
- const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
5
- globalThis.__dinou_require__ = require;
6
- const path = require("path");
7
-
8
- const { normalizePathCase } = require("./path-utils.js");
9
-
10
- let reactServerPath, reactDomServerPath, reactJsxRuntimePath, reactJsxDevRuntimePath;
11
-
12
- if (!isWebpack) {
13
- const reactPkgJson = require.resolve("react/package.json");
14
- reactServerPath = path.join(path.dirname(reactPkgJson), "react.react-server.js");
15
- reactJsxRuntimePath = path.join(path.dirname(reactPkgJson), "jsx-runtime.react-server.js");
16
- reactJsxDevRuntimePath = path.join(path.dirname(reactPkgJson), "jsx-dev-runtime.react-server.js");
17
-
18
- const reactDomPkgJson = require.resolve("react-dom/package.json");
19
- reactDomServerPath = path.join(path.dirname(reactDomPkgJson), "react-dom.react-server.js");
20
- }
21
-
22
- Module._resolveFilename = function (request, parent, isMain, options) {
23
- if (!isWebpack) {
24
- if (request === "react") {
25
- return reactServerPath;
26
- } else if (request === "react-dom") {
27
- return reactDomServerPath;
28
- } else if (request === "react/jsx-runtime") {
29
- return reactJsxRuntimePath;
30
- } else if (request === "react/jsx-dev-runtime") {
31
- return reactJsxDevRuntimePath;
32
- }
33
- }
34
- return originalResolveFilename.call(this, request, parent, isMain, options);
35
- };
36
-
37
- require("./register-paths");
38
- const webpackRegister = require("react-server-dom-webpack/node-register");
39
- const { readFileSync, existsSync, createReadStream } = require("fs");
40
- // const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
41
- const { renderToPipeableStream } = isWebpack
42
- ? require("react-server-dom-webpack/server")
43
- : require("@roggc/react-server-dom-esm/server");
44
- const express = require("express");
45
- const getJSX = require("./get-jsx.js");
46
- const { getFilePathAndDynamicParams } = require("./get-file-path-and-dynamic-params.js");
47
- const { getErrorJSX } = require("./get-error-jsx.js");
48
- const addHook = require("./asset-require-hook.js");
49
- const { extensions } = require("./asset-extensions.js");
50
- if (isWebpack) {
51
- webpackRegister();
52
- }
53
- const babelRegister = require("@babel/register");
54
- babelRegister({
55
- ignore: [/node_modules[\\/](?!dinou)/],
56
- presets: [
57
- ["@babel/preset-react", { runtime: "automatic" }],
58
- "@babel/preset-typescript",
59
- ],
60
- plugins: ["@babel/transform-modules-commonjs"],
61
- extensions: [".js", ".jsx", ".ts", ".tsx"],
62
- });
63
- const createScopedName = require("./createScopedName");
64
- require("./css-require-hook.js")();
65
- addHook({
66
- extensions,
67
- name: function (localName, filepath) {
68
- const result = createScopedName(localName, filepath);
69
- return result + ".[ext]";
70
- },
71
- publicPath: "/assets/",
72
- });
73
- const importModule = require("./import-module");
74
- const generateStatic = require("./generate-static.js");
75
-
76
- // Load Dinou configuration and plugins
77
- let dinouConfig = { plugins: [] };
78
- const dinouConfigPath = path.resolve(process.cwd(), "dinou.config.js");
79
- if (existsSync(dinouConfigPath)) {
80
- try {
81
- dinouConfig = require(dinouConfigPath);
82
- } catch (err) {
83
- console.error("[Dinou] Error loading dinou.config.js:", err);
84
- }
85
- }
86
-
87
- const renderAppToHtml = require("./render-app-to-html.js");
88
- const { revalidating, regenerating } = require("./revalidating.js");
89
- const isDevelopment = process.env.NODE_ENV !== "production";
90
- const outputFolder = isDevelopment ? ".dinou/public" : ".dinou/dist3";
91
- const chokidar = require("chokidar");
92
- const { fileURLToPath, pathToFileURL } = require("url");
93
- const parseExports = require("./parse-exports.js");
94
- const { requestStorage } = require("./request-context.js");
95
- const { useServerRegex } = require("../constants.js");
96
- const processLimiter = require("./concurrency-manager.js");
97
- const { generatingISG } = require("./generating-isg.js");
98
- const { getStatus } = require("./status-manifest.js");
99
- if (isDevelopment) {
100
- const manifestPath = path.resolve(
101
- process.cwd(),
102
- isWebpack
103
- ? `${outputFolder}/react-client-manifest.json`
104
- : `.dinou/react_client_manifest/react-client-manifest.json`,
105
- );
106
- const manifestFolderPath = path.resolve(
107
- process.cwd(),
108
- isWebpack ? outputFolder : ".dinou/react_client_manifest",
109
- );
110
-
111
- let manifestWatcher = null;
112
-
113
- function startManifestWatcher() {
114
- let currentManifest = {};
115
- let isInitial = true;
116
- // If an old watcher already exists, close it first
117
- if (manifestWatcher) {
118
- try {
119
- manifestWatcher.close();
120
- } catch (e) {
121
- console.warn("Failed closing old watcher:", e);
122
- }
123
- }
124
-
125
- // console.log("[Watcher] Starting watcher");
126
-
127
- manifestWatcher = chokidar.watch(manifestFolderPath, {
128
- persistent: true,
129
- ignored: /node_modules/,
130
- });
131
-
132
- async function loadManifestWithRetry({
133
- manifestPath,
134
- maxRetries = 10,
135
- delayMs = 100,
136
- } = {}) {
137
- let attempts = 0;
138
- while (attempts < maxRetries) {
139
- try {
140
- // console.log(`Attempting to load manifest (try ${attempts + 1})...`);
141
- const text = readFileSync(manifestPath, "utf8");
142
- if (!text.trim()) throw new Error("Empty JSON");
143
- return JSON.parse(text);
144
- } catch (err) {
145
- attempts++;
146
- if (attempts >= maxRetries) {
147
- throw err; // Rethrow after max retries
148
- }
149
- // Wait for the specified delay before retrying
150
- await new Promise((resolve) => setTimeout(resolve, delayMs));
151
- }
152
- }
153
- }
154
-
155
- function getParents(resolvedPath) {
156
- const parents = [];
157
- Object.values(require.cache).forEach((mod) => {
158
- if (
159
- mod.children &&
160
- mod.children.some((child) => child.id === resolvedPath)
161
- ) {
162
- parents.push(mod.id);
163
- }
164
- });
165
- return parents;
166
- }
167
-
168
- function clearRequireCache(modulePath, visited = new Set()) {
169
- try {
170
- const resolved = require.resolve(modulePath);
171
- if (visited.has(resolved)) return;
172
- visited.add(resolved);
173
-
174
- if (require.cache[resolved]) {
175
- delete require.cache[resolved];
176
- // console.log(`[Server HMR] Cleared cache for ${resolved}`);
177
-
178
- const parents = getParents(resolved);
179
- for (const parent of parents) {
180
- // Optional: Skip if parent not in src/ (safety)
181
- if (parent.startsWith(path.resolve(process.cwd(), "src"))) {
182
- clearRequireCache(parent, visited);
183
- }
184
- }
185
- }
186
- } catch (err) {
187
- console.warn(
188
- `[Server HMR] Could not resolve or clear ${modulePath}: ${err.message}`,
189
- );
190
- }
191
- }
192
-
193
- let manifestTimeout;
194
-
195
- function readJSONWithRetry(pathToRead, retries = 4, delay = 10) {
196
- for (let i = 0; i < retries; i++) {
197
- try {
198
- const text = readFileSync(pathToRead, "utf8");
199
- if (!text.trim()) throw new Error("Empty JSON");
200
- return JSON.parse(text);
201
- } catch (err) {
202
- if (i === retries - 1) throw err;
203
- // tiny sleep
204
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
205
- }
206
- }
207
- }
208
-
209
- function handleManifestUpdate(newManifest) {
210
- // console.log("handle manifest update", newManifest, currentManifest);
211
- for (const key in currentManifest) {
212
- if (!(key in newManifest)) {
213
- const absPath = fileURLToPath(key);
214
- clearRequireCache(absPath);
215
- // console.log(`Cleared cache for ${absPath} (client -> server)`);
216
- }
217
- }
218
-
219
- // Handle added entries: server -> client switch
220
- for (const key in newManifest) {
221
- if (!(key in currentManifest)) {
222
- const absPath = fileURLToPath(key);
223
- clearRequireCache(absPath);
224
- // console.log(`Cleared cache for ${absPath} (server -> client)`);
225
- }
226
- }
227
-
228
- currentManifest = newManifest;
229
- }
230
-
231
- async function onManifestChange(chokidarPath, stats, delayMs = 100) {
232
- if (isWebpack && chokidarPath !== manifestPath) return;
233
- if (Object.keys(currentManifest).length === 0 && isInitial) {
234
- try {
235
- currentManifest = await loadManifestWithRetry({
236
- manifestPath,
237
- delayMs,
238
- });
239
- // console.log("Loaded initial manifest for HMR.", currentManifest);
240
- isInitial = false;
241
- } catch (err) {
242
- console.error("Failed to load initial manifest after retries:", err);
243
- }
244
- return;
245
- }
246
- try {
247
- // console.log("change event");
248
- clearTimeout(manifestTimeout);
249
-
250
- manifestTimeout = setTimeout(() => {
251
- try {
252
- const newManifest = readJSONWithRetry(manifestPath);
253
- handleManifestUpdate(newManifest);
254
- } catch (err) {
255
- console.error("Manifest not ready:", err.message);
256
- }
257
- }, 50);
258
- } catch (err) {
259
- console.error("Error handling manifest change:", err);
260
- }
261
- }
262
-
263
- manifestWatcher.on("add", onManifestChange);
264
- manifestWatcher.on("unlinkDir", startManifestWatcher);
265
- manifestWatcher.on("unlink", () => {
266
- isInitial = true;
267
- currentManifest = {};
268
- });
269
- manifestWatcher.on("change", onManifestChange);
270
- }
271
- startManifestWatcher();
272
- }
273
- let serverFunctionsManifest = null;
274
-
275
- if (!isDevelopment) {
276
- // In prod/build: load generated manifest
277
- const manifestPath = path.resolve(
278
- process.cwd(),
279
- isWebpack
280
- ? `${outputFolder}/server-functions-manifest.json`
281
- : `.dinou/server_functions_manifest/server-functions-manifest.json`,
282
- ); // Adjust 'dist/' to your outdir
283
- if (existsSync(manifestPath)) {
284
- serverFunctionsManifest = JSON.parse(readFileSync(manifestPath, "utf8"));
285
- // Convert arrays to Sets for fast lookups
286
- for (const key in serverFunctionsManifest) {
287
- serverFunctionsManifest[key] = new Set(serverFunctionsManifest[key]);
288
- }
289
- console.log("[server] Loaded server functions manifest");
290
- } else {
291
- console.error("[server] Manifest not found - falling back to file reads");
292
- }
293
- }
294
- const cookieParser = require("cookie-parser");
295
- const appUseCookieParser = cookieParser();
296
- const app = express();
297
- app.use(appUseCookieParser);
298
-
299
- // Run plugins onServerInit hook
300
- if (dinouConfig.plugins && Array.isArray(dinouConfig.plugins)) {
301
- for (const plugin of dinouConfig.plugins) {
302
- if (typeof plugin.onServerInit === "function") {
303
- try {
304
- plugin.onServerInit(app);
305
- console.log(`[Dinou] Plugin initialized: "${plugin.name || "unnamed"}"`);
306
- } catch (err) {
307
- console.error(`[Dinou] Error initializing plugin "${plugin.name || "unnamed"}":`, err);
308
- }
309
- }
310
- }
311
- }
312
-
313
- app.use(express.json());
314
-
315
- // ============================================================
316
- // šŸ›”ļø ANTI-BOT SHIELD (Prevent ISG from collapsing)
317
- // ============================================================
318
- const botGarbagePatterns = [
319
- /\.php$/i, // Any PHP file
320
- /\.env$/i, // Attempts to steal environment variables
321
- /\.git\b/i, // Attempts to access Git repository
322
- /wp-admin/i, // WordPress admin
323
- /wp-content/i, // WordPress content
324
- /wp-includes/i, // WordPress core files
325
- /xmlrpc\.php/i, // WordPress DDoS attacks
326
- /\.sql$/i, // Attempts to download database dumps
327
- /\.asp$/i, // Legacy ASP pages
328
- /\.jsp$/i, // JSP pages
329
- /\.cgi$/i, // Old CGI scripts
330
- /\.bak$/i, // Backup files
331
- /\.log$/i, // Log files
332
- ];
333
-
334
- app.use((req, res, next) => {
335
- const isGarbage = botGarbagePatterns.some((pattern) => pattern.test(req.path));
336
- if (isGarbage) {
337
- // Return 404 immediately to prevent the ISG engine from spawning processes
338
- return res.status(404).send("Not Found");
339
- }
340
- next();
341
- });
342
-
343
- const { resolveRelativeUrl } = require("./url-resolver");
344
-
345
- function getContext(req, res) {
346
- let hasRedirected = false;
347
- // Helper to execute res methods safely
348
- const safeResCall = (methodName, ...args) => {
349
- if (hasRedirected) return;
350
- if (res.headersSent) {
351
- if (methodName === "redirect" && req.path.includes("____rsc_payload")) {
352
- return; // Silence streaming redirects during RSC payload requests
353
- }
354
- console.log(
355
- `[Dinou] res.${methodName} called but headers already sent. Ignoring.`,
356
- );
357
- return; // Exit silently
358
- }
359
- if (methodName === "redirect") {
360
- hasRedirected = true;
361
- // 1. Normalize arguments (Status and URL)
362
- let url = args[0];
363
- let status = 302; // Default
364
-
365
- if (args.length === 2) {
366
- status = args[0];
367
- url = args[1];
368
- }
369
-
370
- function safeRedirect(targetUrl) {
371
- const resolvedUrl = resolveRelativeUrl(targetUrl, req.path);
372
- let finalUrl = "/";
373
- if (
374
- typeof resolvedUrl === "string" &&
375
- resolvedUrl.startsWith("/") &&
376
- !resolvedUrl.startsWith("//")
377
- ) {
378
- finalUrl = resolvedUrl;
379
- } else {
380
- console.warn(
381
- `[Dinou Security] Blocked unsafe redirect to: ${targetUrl}`,
382
- );
383
- }
384
-
385
- if (req.path.includes("____rsc_payload")) {
386
- res.setHeader("x-rsc-redirect", finalUrl);
387
- res.status(200).end();
388
- return;
389
- }
390
-
391
- res.redirect.apply(res, [status, finalUrl]);
392
- }
393
- return safeRedirect(url);
394
- }
395
- // Execute maintaining 'this' context with bind/apply
396
- return res[methodName].apply(res, args);
397
- };
398
-
399
- const context = {
400
- req: {
401
- cookies: { ...req.cookies },
402
- headers: {
403
- "user-agent": req.headers["user-agent"],
404
- cookie: req.headers["cookie"],
405
- referer: req.headers["referer"],
406
- host: req.headers["host"],
407
- authorization: req.headers["authorization"],
408
- "accept-language": req.headers["accept-language"],
409
- "x-forwarded-for": req.headers["x-forwarded-for"],
410
- forwarded: req.headers["forwarded"],
411
- "content-type": req.headers["content-type"],
412
- origin: req.headers["origin"],
413
- },
414
- query: { ...req.query },
415
- path: req.path,
416
- method: req.method,
417
- },
418
- res: {
419
- // 1. STATUS
420
- status: (code) => safeResCall("status", code),
421
-
422
- // 2. SET HEADER
423
- setHeader: (name, value) => safeResCall("setHeader", name, value),
424
-
425
- // 3. CLEAR COOKIE
426
- // Note: If headersSent is true, the cookie will NOT be cleared in this RSC request.
427
- // If clearing it is vital, you should handle it in the Client Component or separate API route.
428
- clearCookie: (name, options) => safeResCall("clearCookie", name, options),
429
- cookie: (name, value, options) =>
430
- safeResCall("cookie", name, value, options),
431
-
432
- // 4. REDIRECT (Your existing smart wrapper)
433
- redirect: (...args) => safeResCall("redirect", ...args),
434
- },
435
- };
436
-
437
- // Run plugins onRequestContext hook
438
- if (dinouConfig.plugins && Array.isArray(dinouConfig.plugins)) {
439
- for (const plugin of dinouConfig.plugins) {
440
- if (typeof plugin.onRequestContext === "function") {
441
- try {
442
- plugin.onRequestContext(req, res, context);
443
- } catch (err) {
444
- console.error(`[Dinou] Error running onRequestContext in plugin "${plugin.name || "unnamed"}":`, err);
445
- }
446
- }
447
- }
448
- }
449
-
450
- return context;
451
- }
452
-
453
- function getContextForServerFunctionEndpoint(req, res) {
454
- const context = {
455
- req: {
456
- cookies: { ...req.cookies },
457
- headers: {
458
- "user-agent": req.headers["user-agent"],
459
- cookie: req.headers["cookie"],
460
- referer: req.headers["referer"],
461
- host: req.headers["host"],
462
- authorization: req.headers["authorization"],
463
- "accept-language": req.headers["accept-language"],
464
- "x-forwarded-for": req.headers["x-forwarded-for"],
465
- forwarded: req.headers["forwarded"],
466
- "content-type": req.headers["content-type"],
467
- origin: req.headers["origin"],
468
- },
469
- query: { ...req.query },
470
- path: req.path,
471
- method: req.method,
472
- },
473
- res: {
474
- redirect: (urlOrStatus, url) => {
475
- const rawUrl = url || urlOrStatus;
476
- const referer = req.headers["referer"];
477
- let refererPath = "/";
478
- if (referer) {
479
- try {
480
- refererPath = new URL(referer).pathname;
481
- } catch (e) { }
482
- }
483
- const resolvedUrl = resolveRelativeUrl(rawUrl, refererPath);
484
- let finalUrl = "/";
485
- if (
486
- typeof resolvedUrl === "string" &&
487
- resolvedUrl.startsWith("/") &&
488
- !resolvedUrl.startsWith("//")
489
- ) {
490
- finalUrl = resolvedUrl;
491
- } else {
492
- console.warn(
493
- `[Dinou Security] Blocked unsafe server function redirect to: ${rawUrl}`,
494
- );
495
- }
496
- // We throw a special object that the endpoint will intercept
497
- throw {
498
- $$type: "dinou-internal-redirect",
499
- url: finalUrl,
500
- };
501
- },
502
- status: (code) => {
503
- // If there is already a stream, we can't change the status, ignore or log warning
504
- if (!res.headersSent) res.status(code);
505
- },
506
- setHeader: (n, v) => {
507
- if (!res.headersSent) res.setHeader(n, v);
508
- },
509
-
510
- // ============================================================
511
- // COOKIE IMPLEMENTATION (Hybrid: Headers + Script Injection)
512
- // ============================================================
513
- cookie: (name, value, options) => {
514
- // SCENARIO A: We haven't started responding yet
515
- // Use Express native method. It's the best.
516
- if (!res.headersSent) {
517
- // Ensure correct Content-Type if it's the first write
518
- res.setHeader("Content-Type", "text/x-component");
519
- res.cookie(name, value, options);
520
- return;
521
- }
522
-
523
- // SCENARIO B: Streaming active (Headers Sent)
524
- // Send safe stream command.
525
-
526
- // šŸ›‘ Security: JS cannot write HttpOnly cookies
527
- if (options && options.httpOnly) {
528
- console.error(
529
- `[Dinou Error] Cannot set HttpOnly cookie '${name}' in Server Function endpoint because streaming has started.`,
530
- );
531
- return;
532
- }
533
-
534
- // Manual cookie string construction
535
- // Format: key=value; attributes...
536
- let cookieStr = `${name}=${encodeURIComponent(value)}`;
537
-
538
- if (options) {
539
- if (options.path) cookieStr += `; path=${options.path}`;
540
- if (options.domain) cookieStr += `; domain=${options.domain}`;
541
- if (options.maxAge) cookieStr += `; max-age=${options.maxAge}`;
542
- if (options.expires)
543
- cookieStr += `; expires=${new Date(options.expires).toUTCString()}`;
544
- if (options.secure) cookieStr += `; secure`;
545
- if (options.sameSite) cookieStr += `; samesite=${options.sameSite}`;
546
- }
547
-
548
- // Safe packaging for stream command
549
- const safeCookieStr = JSON.stringify(cookieStr);
550
-
551
- // Write to stream
552
- res.write(`D:{"type":"cookie","cookie":${safeCookieStr}}\n`);
553
- },
554
-
555
- clearCookie: (name, options) => {
556
- // SCENARIO A: Native
557
- if (!res.headersSent) {
558
- res.setHeader("Content-Type", "text/x-component");
559
- res.clearCookie(name, options);
560
- return;
561
- }
562
-
563
- // SCENARIO B: Custom stream command
564
- let cookieStr = `${name}=; Max-Age=0`;
565
- const path = options?.path || "/";
566
- cookieStr += `; path=${path}`;
567
- if (options) {
568
- if (options.domain) cookieStr += `; domain=${options.domain}`;
569
- if (options.secure) cookieStr += `; secure`;
570
- if (options.sameSite) cookieStr += `; samesite=${options.sameSite}`;
571
- }
572
- cookieStr += ";";
573
- const safeCookieStr = JSON.stringify(cookieStr);
574
- res.write(`D:{"type":"cookie","cookie":${safeCookieStr}}\n`);
575
- },
576
- },
577
- };
578
-
579
- // Run plugins onRequestContext hook
580
- if (dinouConfig.plugins && Array.isArray(dinouConfig.plugins)) {
581
- for (const plugin of dinouConfig.plugins) {
582
- if (typeof plugin.onRequestContext === "function") {
583
- try {
584
- plugin.onRequestContext(req, res, context);
585
- } catch (err) {
586
- console.error(`[Dinou] Error running onRequestContext in plugin "${plugin.name || "unnamed"}":`, err);
587
- }
588
- }
589
- }
590
- }
591
-
592
- return context;
593
- }
594
-
595
- app.use(express.static(path.resolve(process.cwd(), outputFolder)));
596
-
597
- const clientManifestResolvedPath = path.resolve(
598
- process.cwd(),
599
- isWebpack
600
- ? `${outputFolder}/react-client-manifest.json`
601
- : `.dinou/react_client_manifest/react-client-manifest.json`,
602
- );
603
-
604
- function isManifestReady() {
605
- try {
606
- return existsSync(clientManifestResolvedPath) && readFileSync(clientManifestResolvedPath, "utf8").trim().length > 2;
607
- } catch (e) {
608
- return false;
609
- }
610
- }
611
-
612
- let isReady = isDevelopment; // In dev we are always ready (or almost)
613
-
614
- app.get("/__DINOU_STATUS_PLAYWRIGHT__", (req, res) => {
615
- res.json({
616
- status: "ok",
617
- isReady: isDevelopment ? isManifestReady() : isReady,
618
- mode: isDevelopment ? "development" : "production",
619
- });
620
- });
621
-
622
- app.get("/.well-known/appspecific/com.chrome.devtools.json", (req, res) => {
623
- res.setHeader("Content-Type", "application/json");
624
- res.json({
625
- name: "Dinou DevTools",
626
- description: "Dinou DevTools for Chrome",
627
- version: "1.0.0",
628
- devtools_page: `/${outputFolder}/devtools.html`,
629
- });
630
- });
631
-
632
- let cachedClientManifest = null;
633
- if (!isDevelopment) {
634
- // Initial load
635
- cachedClientManifest = JSON.parse(
636
- readFileSync(
637
- path.resolve(
638
- process.cwd(),
639
- isWebpack
640
- ? `${outputFolder}/react-client-manifest.json`
641
- : `.dinou/react_client_manifest/react-client-manifest.json`,
642
- ),
643
- "utf8",
644
- ),
645
- );
646
- }
647
-
648
- const isDynamic = new Map();
649
-
650
- async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
651
- try {
652
- const reqPath = (
653
- req.path.endsWith("/") ? req.path : req.path + "/"
654
- ).replace(
655
- isOld
656
- ? isStatic
657
- ? "/____rsc_payload_old_static____"
658
- : "/____rsc_payload_old____"
659
- : isStatic
660
- ? "/____rsc_payload_static____"
661
- : "/____rsc_payload____",
662
- "",
663
- );
664
- // 1. Correct Map initialization
665
- if (!isDynamic.has(reqPath)) {
666
- // Initialize with a mutable object.
667
- // By default we assume it is NOT dynamic (false) until proven otherwise.
668
- isDynamic.set(reqPath, { value: false });
669
- }
670
-
671
- // Get reference to the mutable object
672
- const dynamicState = isDynamic.get(reqPath);
673
- // console.log(
674
- // "rscPayload-> dynamicState.value, isStatic, reqPath",
675
- // dynamicState.value,
676
- // isStatic,
677
- // reqPath
678
- // );
679
- if ((!isDevelopment && !dynamicState.value) || isStatic) {
680
- let currentGeneratedAt = null;
681
- try {
682
- const metadataPath = path.join(".dinou/dist2", reqPath, "metadata.json");
683
- if (existsSync(metadataPath)) {
684
- const metaObj = JSON.parse(readFileSync(metadataPath, "utf8"));
685
- currentGeneratedAt = metaObj.generatedAt || null;
686
- }
687
- } catch (e) { }
688
-
689
- const useOld =
690
- isOld ||
691
- regenerating.has(reqPath) ||
692
- (req.query.buildId &&
693
- currentGeneratedAt &&
694
- req.query.buildId !== String(currentGeneratedAt));
695
-
696
- const payloadPath = path.resolve(
697
- ".dinou/dist2",
698
- reqPath.replace(/^\//, ""),
699
- useOld ? "rsc._old.rsc" : "rsc.rsc",
700
- );
701
-
702
- const distDir = path.resolve(".dinou/dist2");
703
-
704
- if (!payloadPath.startsWith(distDir)) {
705
- return res.status(403).end();
706
- }
707
- if (existsSync(payloadPath)) {
708
- res.setHeader("Content-Type", "application/octet-stream");
709
- res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
710
- try {
711
- const buffer = readFileSync(payloadPath);
712
- return res.send(buffer);
713
- } catch (err) {
714
- console.error("Error reading RSC file:", err);
715
- return res.status(500).end();
716
- }
717
- }
718
- }
719
- let isPathBlocked = false;
720
- const reqSegments = reqPath.split("/").filter(Boolean);
721
- const srcFolder = path.resolve(process.cwd(), "src");
722
- const [pagePath, dynamicParams] = getFilePathAndDynamicParams(
723
- reqSegments,
724
- req.query,
725
- srcFolder,
726
- );
727
-
728
- if (pagePath) {
729
- let cachedConfig = pageFunctionsConfigCache.get(pagePath);
730
- if (!cachedConfig) {
731
- const pageFolder = path.dirname(pagePath);
732
- const [pageFunctionsPath] = getFilePathAndDynamicParams(
733
- reqSegments,
734
- req.query,
735
- pageFolder,
736
- "page_functions",
737
- true,
738
- true,
739
- undefined,
740
- reqSegments.length,
741
- );
742
-
743
- if (pageFunctionsPath) {
744
- const pageFunctionsModule = await importModule(pageFunctionsPath);
745
- const allowISGValue = pageFunctionsModule.allowISG
746
- ? await pageFunctionsModule.allowISG()
747
- : true;
748
-
749
- let staticPathsSet = null;
750
- if (pageFunctionsModule.getStaticPaths) {
751
- const paths = await pageFunctionsModule.getStaticPaths();
752
- staticPathsSet = new Set(
753
- (paths || []).map((pathObj) => {
754
- const sortedEntries = Object.entries(pathObj).sort((a, b) =>
755
- a[0].localeCompare(b[0])
756
- );
757
- return JSON.stringify(sortedEntries);
758
- })
759
- );
760
- }
761
-
762
- cachedConfig = {
763
- allowISG: allowISGValue,
764
- staticPathsSet,
765
- validateParams: pageFunctionsModule.validateParams || null,
766
- };
767
- } else {
768
- cachedConfig = {
769
- allowISG: true,
770
- staticPathsSet: null,
771
- validateParams: null,
772
- };
773
- }
774
-
775
- if (!isDevelopment) {
776
- pageFunctionsConfigCache.set(pagePath, cachedConfig);
777
- }
778
- }
779
-
780
- const { allowISG: allowISGValue, staticPathsSet, validateParams: validateParamsFn } = cachedConfig;
781
- const hasParams = Object.keys(dynamicParams || {}).length > 0;
782
- if (hasParams) {
783
- if (validateParamsFn) {
784
- const isValid = await validateParamsFn(dynamicParams);
785
- if (!isValid) {
786
- isPathBlocked = true;
787
- }
788
- }
789
-
790
- if (!isPathBlocked && allowISGValue === false) {
791
- let isPathAllowed = false;
792
- if (staticPathsSet) {
793
- const sortedQueryEntries = Object.entries(dynamicParams)
794
- .sort((a, b) => a[0].localeCompare(b[0]))
795
- .map(([k, v]) => {
796
- if (Array.isArray(v)) return [k, v.join(",")];
797
- return [k, String(v)];
798
- });
799
- const serializedQuery = JSON.stringify(sortedQueryEntries);
800
- isPathAllowed = staticPathsSet.has(serializedQuery);
801
- }
802
- if (!isPathAllowed) {
803
- if (isDevelopment) {
804
- isPathBlocked = true;
805
- } else {
806
- const htmlPath = path.join(".dinou/dist2", reqPath, "index.html");
807
- if (!existsSync(htmlPath)) {
808
- isPathBlocked = true;
809
- }
810
- }
811
- }
812
- }
813
- }
814
- }
815
-
816
- const context = getContext(req, res);
817
- const isNotFound = {};
818
- await requestStorage.run(context, async () => {
819
- const jsx = await getJSX(
820
- reqPath,
821
- { ...req.query },
822
- isNotFound,
823
- isDevelopment,
824
- isPathBlocked,
825
- );
826
-
827
- if (isNotFound.value) {
828
- res.status(404);
829
- }
830
-
831
- if (res.headersSent) {
832
- return;
833
- }
834
-
835
- const manifest = isDevelopment
836
- ? JSON.parse(
837
- readFileSync(
838
- path.resolve(
839
- process.cwd(),
840
- isWebpack
841
- ? `${outputFolder}/react-client-manifest.json`
842
- : `.dinou/react_client_manifest/react-client-manifest.json`,
843
- ),
844
- "utf8",
845
- ),
846
- )
847
- : cachedClientManifest;
848
-
849
- const { pipe } = isWebpack
850
- ? renderToPipeableStream(jsx, manifest)
851
- : renderToPipeableStream(jsx, pathToFileURL(process.cwd()).href + "/");
852
- pipe(res);
853
- });
854
- } catch (error) {
855
- console.error("Error rendering RSC:", error);
856
- try {
857
- const serializedError = {
858
- message: error.message || "Unknown Error",
859
- name: error.name,
860
- stack: isDevelopment ? error.stack : undefined,
861
- };
862
- const context = getContext(req, res);
863
- await requestStorage.run(context, async () => {
864
- const jsx = await getErrorJSX(
865
- reqPath,
866
- { ...req.query },
867
- serializedError,
868
- isDevelopment,
869
- );
870
- const manifest = isDevelopment
871
- ? JSON.parse(
872
- readFileSync(
873
- path.resolve(
874
- process.cwd(),
875
- isWebpack
876
- ? `${outputFolder}/react-client-manifest.json`
877
- : `.dinou/react_client_manifest/react-client-manifest.json`,
878
- ),
879
- "utf8",
880
- ),
881
- )
882
- : cachedClientManifest;
883
- const { pipe } = isWebpack
884
- ? renderToPipeableStream(jsx, manifest)
885
- : renderToPipeableStream(jsx, pathToFileURL(process.cwd()).href + "/");
886
- res.status(500);
887
- pipe(res);
888
- });
889
- } catch (innerError) {
890
- console.error("Fatal error rendering fallback error RSC:", innerError);
891
- res.status(500).send("Internal Server Error");
892
- }
893
- }
894
- }
895
-
896
- app.get(/^\/____rsc_payload____\/.*\/?$/, async (req, res) => {
897
- await serveRSCPayload(req, res, false, false);
898
- });
899
-
900
- app.get(/^\/____rsc_payload_old____\/.*\/?$/, async (req, res) => {
901
- await serveRSCPayload(req, res, true, false);
902
- });
903
-
904
- app.get(/^\/____rsc_payload_static____\/.*\/?$/, async (req, res) => {
905
- await serveRSCPayload(req, res, false, true);
906
- });
907
-
908
- app.get(/^\/____rsc_payload_old_static____\/.*\/?$/, async (req, res) => {
909
- await serveRSCPayload(req, res, true, true);
910
- });
911
-
912
- app.post(/^\/____rsc_payload_error____\/.*\/?$/, async (req, res) => {
913
- try {
914
- const reqPath = (
915
- req.path.endsWith("/") ? req.path : req.path + "/"
916
- ).replace("/____rsc_payload_error____", "");
917
-
918
- const context = getContext(req, res);
919
- await requestStorage.run(context, async () => {
920
- const jsx = await getErrorJSX(
921
- reqPath,
922
- { ...req.query },
923
- req.body.error,
924
- isDevelopment,
925
- );
926
- const manifest = isDevelopment
927
- ? JSON.parse(
928
- readFileSync(
929
- path.resolve(
930
- process.cwd(),
931
- isWebpack
932
- ? `${outputFolder}/react-client-manifest.json`
933
- : `.dinou/react_client_manifest/react-client-manifest.json`,
934
- ),
935
- "utf8",
936
- ),
937
- )
938
- : cachedClientManifest;
939
- const { pipe } = isWebpack
940
- ? renderToPipeableStream(jsx, manifest)
941
- : renderToPipeableStream(jsx, pathToFileURL(process.cwd()).href + "/");
942
- pipe(res);
943
- });
944
- } catch (error) {
945
- console.error("Error rendering RSC:", error);
946
- res.status(500).send("Internal Server Error");
947
- }
948
- });
949
-
950
- const pageFunctionsConfigCache = new Map();
951
-
952
- app.get(/^\/.*\/?$/, async (req, res) => {
953
- try {
954
- const reqSegments = req.path.split("/").filter(Boolean);
955
- const srcFolder = path.resolve(process.cwd(), "src");
956
- const [pagePath, dynamicParams] = getFilePathAndDynamicParams(
957
- reqSegments,
958
- req.query,
959
- srcFolder,
960
- );
961
-
962
- if (!pagePath) {
963
- if (path.extname(req.path)) {
964
- return res.status(404).send("Not Found");
965
- }
966
- }
967
-
968
- let isPathBlocked = false;
969
-
970
- if (pagePath) {
971
- let cachedConfig = pageFunctionsConfigCache.get(pagePath);
972
- if (!cachedConfig) {
973
- const pageFolder = path.dirname(pagePath);
974
- const [pageFunctionsPath] = getFilePathAndDynamicParams(
975
- reqSegments,
976
- req.query,
977
- pageFolder,
978
- "page_functions",
979
- true,
980
- true,
981
- undefined,
982
- reqSegments.length,
983
- );
984
-
985
- if (pageFunctionsPath) {
986
- const pageFunctionsModule = await importModule(pageFunctionsPath);
987
- const allowISGValue = pageFunctionsModule.allowISG
988
- ? await pageFunctionsModule.allowISG()
989
- : true;
990
-
991
- let staticPathsSet = null;
992
- if (pageFunctionsModule.getStaticPaths) {
993
- const paths = await pageFunctionsModule.getStaticPaths();
994
- staticPathsSet = new Set(
995
- (paths || []).map((pathObj) => {
996
- const sortedEntries = Object.entries(pathObj).sort((a, b) =>
997
- a[0].localeCompare(b[0])
998
- );
999
- return JSON.stringify(sortedEntries);
1000
- })
1001
- );
1002
- }
1003
-
1004
- cachedConfig = {
1005
- allowISG: allowISGValue,
1006
- staticPathsSet,
1007
- validateParams: pageFunctionsModule.validateParams || null,
1008
- };
1009
- } else {
1010
- cachedConfig = {
1011
- allowISG: true,
1012
- staticPathsSet: null,
1013
- validateParams: null,
1014
- };
1015
- }
1016
-
1017
- if (!isDevelopment) {
1018
- pageFunctionsConfigCache.set(pagePath, cachedConfig);
1019
- }
1020
- }
1021
-
1022
- const { allowISG: allowISGValue, staticPathsSet, validateParams: validateParamsFn } = cachedConfig;
1023
- const hasParams = Object.keys(dynamicParams || {}).length > 0;
1024
- if (hasParams) {
1025
- if (validateParamsFn) {
1026
- const isValid = await validateParamsFn(dynamicParams);
1027
- if (!isValid) {
1028
- isPathBlocked = true;
1029
- }
1030
- }
1031
-
1032
- if (!isPathBlocked && allowISGValue === false) {
1033
- let isPathAllowed = false;
1034
- if (staticPathsSet) {
1035
- const sortedQueryEntries = Object.entries(dynamicParams)
1036
- .sort((a, b) => a[0].localeCompare(b[0]))
1037
- .map(([k, v]) => {
1038
- if (Array.isArray(v)) return [k, v.join(",")];
1039
- return [k, String(v)];
1040
- });
1041
- const serializedQuery = JSON.stringify(sortedQueryEntries);
1042
- isPathAllowed = staticPathsSet.has(serializedQuery);
1043
- }
1044
- if (!isPathAllowed) {
1045
- if (isDevelopment) {
1046
- isPathBlocked = true;
1047
- } else {
1048
- const htmlPath = path.join(".dinou/dist2", req.path, "index.html");
1049
- if (!existsSync(htmlPath)) {
1050
- isPathBlocked = true;
1051
- }
1052
- }
1053
- }
1054
- }
1055
- }
1056
- }
1057
-
1058
- const reqPath = req.path.endsWith("/") ? req.path : req.path + "/";
1059
-
1060
- // 1. Correct Map initialization
1061
- if (!isDynamic.has(reqPath)) {
1062
- // Initialize with a mutable object.
1063
- // By default we assume it is NOT dynamic (false) until proven otherwise.
1064
- isDynamic.set(reqPath, { value: false });
1065
- }
1066
-
1067
- // Get reference to the mutable object
1068
- const dynamicState = isDynamic.get(reqPath);
1069
- // console.log("dynamicState.value", dynamicState.value);
1070
- if (!isDevelopment && !dynamicState.value && pagePath && !isPathBlocked) {
1071
- revalidating(reqPath, dynamicState);
1072
- let htmlPathOld;
1073
- if (regenerating.has(reqPath)) {
1074
- // Still regenerating, serve old HTML if exists
1075
- htmlPathOld = path.join(".dinou/dist2", reqPath, "index._old.html");
1076
- }
1077
- const htmlPath = path.join(".dinou/dist2", reqPath, "index.html");
1078
- // Decide which file to read
1079
- const fileToRead = htmlPathOld || htmlPath;
1080
-
1081
- if (existsSync(fileToRead) && !dynamicState.value) {
1082
- res.setHeader("Content-Type", "text/html");
1083
- res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
1084
- const status = getStatus(reqPath);
1085
-
1086
- if (status) {
1087
- res.statusCode = status;
1088
- } else {
1089
- res.statusCode = 200; // Default
1090
- }
1091
- try {
1092
- let htmlContent = readFileSync(fileToRead, "utf8");
1093
- let buildId = "";
1094
- try {
1095
- const metadataPath = path.join(".dinou/dist2", reqPath, "metadata.json");
1096
- if (existsSync(metadataPath)) {
1097
- const metaObj = JSON.parse(readFileSync(metadataPath, "utf8"));
1098
- buildId = metaObj.generatedAt || "";
1099
- }
1100
- } catch (e) { }
1101
-
1102
- let scripts = `<script>window.__DINOU_USE_STATIC__=true;</script>`;
1103
- if (htmlPathOld) {
1104
- scripts += `<script>window.__DINOU_USE_OLD_RSC__=true;</script>`;
1105
- }
1106
- if (buildId) {
1107
- scripts += `<script>window.__DINOU_BUILD_ID__="${buildId}";</script>`;
1108
- }
1109
-
1110
- htmlContent = htmlContent.replace("</head>", `${scripts}</head>`);
1111
- res.send(htmlContent);
1112
- } catch (err) {
1113
- console.error("Error reading HTML file:", err);
1114
- if (!res.headersSent) res.status(500).send("Server Error");
1115
- }
1116
- return;
1117
- }
1118
- }
1119
-
1120
- const contextForChild = {
1121
- req: {
1122
- // Only serialize what is necessary for getContext().req
1123
- query: { ...req.query },
1124
- cookies: { ...req.cookies },
1125
- headers: {
1126
- "user-agent": req.headers["user-agent"],
1127
- cookie: req.headers["cookie"],
1128
- referer: req.headers["referer"],
1129
- host: req.headers["host"],
1130
- authorization: req.headers["authorization"],
1131
- "accept-language": req.headers["accept-language"],
1132
- "x-forwarded-for": req.headers["x-forwarded-for"],
1133
- forwarded: req.headers["forwarded"],
1134
- "content-type": req.headers["content-type"],
1135
- origin: req.headers["origin"],
1136
- },
1137
- path: req.path,
1138
- method: req.method,
1139
- },
1140
- // Do not include res here
1141
- };
1142
-
1143
- // Run plugins onRequestContext hook
1144
- if (dinouConfig.plugins && Array.isArray(dinouConfig.plugins)) {
1145
- for (const plugin of dinouConfig.plugins) {
1146
- if (typeof plugin.onRequestContext === "function") {
1147
- try {
1148
- plugin.onRequestContext(req, res, contextForChild);
1149
- } catch (err) {
1150
- console.error(`[Dinou] Error running onRequestContext in plugin "${plugin.name || "unnamed"}":`, err);
1151
- }
1152
- }
1153
- }
1154
- }
1155
-
1156
- processLimiter
1157
- .run(async () => {
1158
- const isDynamic = true;
1159
- const capturedStatus = null;
1160
- const appHtmlStream = renderAppToHtml(
1161
- reqPath,
1162
- JSON.stringify({ ...req.query }),
1163
- contextForChild,
1164
- res,
1165
- capturedStatus,
1166
- isDynamic,
1167
- isPathBlocked,
1168
- );
1169
-
1170
- res.setHeader("Content-Type", "text/html");
1171
- appHtmlStream.pipe(res);
1172
-
1173
- // šŸ‘‡ ISG TRIGGER GOES HERE šŸ‘‡
1174
- // We use 'finish' to ensure the user received everything (Status 200).
1175
- // It's "Fire and Forget": We don't use 'await', runs in background.
1176
- res.on("finish", () => {
1177
- if (
1178
- !isDevelopment &&
1179
- res.statusCode === 200 && // Only if success
1180
- req.method === "GET" && // Only GET requests
1181
- isReady
1182
- ) {
1183
- generatingISG(reqPath, dynamicState);
1184
- }
1185
- });
1186
- // šŸ‘† END OF ISG TRIGGER šŸ‘†
1187
-
1188
- // šŸ’” TRICK: We want to release the concurrency slot ONLY when
1189
- // the stream has finished sending or there is an error.
1190
- await new Promise((resolve) => {
1191
- appHtmlStream.on("end", resolve);
1192
- appHtmlStream.on("error", (error) => {
1193
- console.error("Stream error:", error);
1194
- if (!res.headersSent) res.status(500).send("Internal Server Error");
1195
- resolve(); // Better resolve at the end
1196
- });
1197
- res.on("close", resolve); // If user closes the tab
1198
- });
1199
- })
1200
- .catch((err) => {
1201
- console.error("Error in limited SSR:", err);
1202
- if (!res.headersSent) res.status(500).send("Server Busy or Error");
1203
- });
1204
- } catch (error) {
1205
- console.error("Error rendering React app:", error);
1206
- res.status(500).send("Internal Server Error");
1207
- }
1208
- });
1209
-
1210
- // Helper function to verify origin
1211
- function isOriginAllowed(req) {
1212
- // 1. In server-to-server or tools environments, sometimes there is no Origin.
1213
- // If you decide it is mandatory, return false here.
1214
- // But modern browsers ALWAYS send Origin on POST.
1215
- const origin = req.headers.origin;
1216
-
1217
- // If no origin (e.g. curl call or server-side fetch without headers),
1218
- // you decide whether to be strict or permissive.
1219
- if (!origin) return false; // Change to true if you want to allow without origin.
1220
-
1221
- try {
1222
- // Parse to ignore protocol (http/https) and port if they differ subtly
1223
- const originHost = new URL(origin).host;
1224
- const serverHost = req.headers["x-forwarded-host"] || req.headers.host;
1225
-
1226
- // Compare host (domain:port)
1227
- return originHost === serverHost;
1228
- } catch (e) {
1229
- return false; // If origin URL is invalid, reject.
1230
- }
1231
- }
1232
-
1233
- const multer = require("multer");
1234
-
1235
- const upload = multer().any();
1236
-
1237
- const runMiddleware = (req, res, fn) => {
1238
- return new Promise((resolve, reject) => {
1239
- fn(req, res, (result) => {
1240
- if (result instanceof Error) return reject(result);
1241
- return resolve(result);
1242
- });
1243
- });
1244
- };
1245
-
1246
- app.post("/____server_function____", async (req, res) => {
1247
- try {
1248
- if (req.headers["content-type"]?.includes("multipart/form-data")) {
1249
- await runMiddleware(req, res, upload);
1250
- }
1251
-
1252
- // 1. Check Origin (Prevent calls from other domains)
1253
- const origin = req.headers.origin;
1254
- const host = req.headers["x-forwarded-host"] || req.headers.host;
1255
-
1256
- // Note: Locally sometimes origin is undefined or null, allow in dev if necessary
1257
- if (!isDevelopment && origin && !origin.includes(host)) {
1258
- return res.status(403).json({ error: "Invalid Origin" });
1259
- }
1260
-
1261
- // 2. Check Custom Header (Robust CSRF defense)
1262
- // Make sure your client (server-function-proxy.js) sends this header
1263
- if (req.headers["x-server-function-call"] !== "1") {
1264
- return res.status(403).json({ error: "Missing security header" });
1265
- }
1266
-
1267
- // 2. Origin Check (NEW)
1268
- if (!isDevelopment && !isOriginAllowed(req)) {
1269
- console.error(
1270
- `[Security] Blocked request from origin: ${req.headers.origin}`,
1271
- );
1272
- return res.status(403).json({ error: "Origin not allowed" });
1273
- }
1274
-
1275
- let id, args;
1276
-
1277
- if (req.headers["content-type"]?.includes("multipart/form-data")) {
1278
- id = req.body.__dinou_func_id;
1279
-
1280
- const formData = new FormData();
1281
-
1282
- for (const key in req.body) {
1283
- if (
1284
- key === "__dinou_func_id" ||
1285
- key === "__dinou_args" ||
1286
- key === "__dinou_formData_index"
1287
- )
1288
- continue;
1289
- formData.append(key, req.body[key]);
1290
- }
1291
-
1292
- if (req.files && Array.isArray(req.files)) {
1293
- for (const file of req.files) {
1294
- const blob = new Blob([file.buffer], { type: file.mimetype });
1295
- formData.append(file.fieldname, blob, file.originalname);
1296
- }
1297
- }
1298
-
1299
- if (req.body.__dinou_args) {
1300
- try {
1301
- const parsedArgs = JSON.parse(req.body.__dinou_args);
1302
- if (req.body.__dinou_formData_index !== undefined) {
1303
- const formDataIndex = parseInt(req.body.__dinou_formData_index, 10);
1304
- parsedArgs[formDataIndex] = formData;
1305
- args = parsedArgs;
1306
- } else {
1307
- args = [formData, ...parsedArgs];
1308
- }
1309
- } catch (e) {
1310
- console.error("Error parsing extra args in multipart request", e);
1311
- args = [formData];
1312
- }
1313
- } else {
1314
- args = [formData];
1315
- }
1316
- } else {
1317
- id = req.body.id;
1318
- args = req.body.args;
1319
- }
1320
-
1321
- // Basic input validation: id must be string, args an array
1322
- if (typeof id !== "string" || !Array.isArray(args)) {
1323
- return res.status(400).json({ error: "Invalid request body" });
1324
- }
1325
-
1326
- const [fileUrl, exportName] = id.split("#");
1327
-
1328
- // Validate fileUrl: must start with 'file://' and not contain suspicious chars
1329
- if (!fileUrl.startsWith("file://")) {
1330
- return res.status(400).json({ error: "Invalid file URL format" });
1331
- }
1332
-
1333
- let relativePath;
1334
-
1335
- // Check if the URL is a relative reference (e.g. file:///src/...)
1336
- // If so, extract it directly without using fileURLToPath (which throws on Windows without a drive letter)
1337
- const isRelativeSrc = fileUrl.startsWith("file:///src/") || fileUrl.startsWith("file:///src\\");
1338
-
1339
- if (isRelativeSrc) {
1340
- relativePath = fileUrl.replace(/^file:\/\/\/?/, "").trim();
1341
- } else {
1342
- const resolvedPath = fileURLToPath(fileUrl);
1343
- relativePath = resolvedPath;
1344
-
1345
- const normalizedCwd = normalizePathCase(process.cwd());
1346
- const normalizedResolved = normalizePathCase(resolvedPath);
1347
-
1348
- if (normalizedResolved.startsWith(normalizedCwd)) {
1349
- relativePath = path.relative(normalizedCwd, normalizedResolved);
1350
- } else {
1351
- relativePath = relativePath.replace(/^[\\/]+/, "");
1352
- }
1353
- }
1354
- const normalizedRelative = relativePath.replace(/\\/g, "/");
1355
- if (
1356
- normalizedRelative.startsWith("/") ||
1357
- normalizedRelative.includes("..") ||
1358
- normalizedRelative.includes(":")
1359
- ) {
1360
- return res
1361
- .status(400)
1362
- .json({ error: "Invalid path: no absolute, traversal, or drive letter allowed" });
1363
- }
1364
-
1365
- // Restrict to 'src/' folder: prepend 'src/' if missing, and resolve absolutePath
1366
- if (!relativePath.startsWith("src/") && !relativePath.startsWith("src\\")) {
1367
- relativePath = path.join("src", relativePath);
1368
- }
1369
- const absolutePath = path.resolve(process.cwd(), relativePath);
1370
-
1371
- // Verify that absolutePath is strictly inside 'src/'
1372
- const srcDir = path.resolve(process.cwd(), "src");
1373
- if (!absolutePath.startsWith(srcDir + path.sep)) {
1374
- return res
1375
- .status(403)
1376
- .json({ error: "Access denied: file outside src directory" });
1377
- }
1378
-
1379
- // Verify that the file exists
1380
- if (!existsSync(absolutePath)) {
1381
- // console.error("āŒ [Dinou Server Function 404] File not found! id:", id, "relativePath:", relativePath, "absolutePath:", absolutePath);
1382
- return res.status(404).json({ error: "File not found" });
1383
- }
1384
-
1385
- const normalizedKey = relativePath.replace(/\\/g, "/");
1386
- let allowedExports;
1387
- if (serverFunctionsManifest) {
1388
- // Prod: use manifest (relativePath is already normalized)
1389
- allowedExports = serverFunctionsManifest[normalizedKey];
1390
- } else {
1391
- if (!isDevelopment) {
1392
- return res
1393
- .status(403)
1394
- .json({ error: "Access denied: Manifest missing in production" });
1395
- }
1396
- const fileContent = readFileSync(absolutePath, "utf8"); // Reads only once
1397
- if (!useServerRegex.test(fileContent)) {
1398
- return res
1399
- .status(403)
1400
- .json({ error: "Not a valid server function file" });
1401
- }
1402
- // Parse exports
1403
- const exports = parseExports(fileContent);
1404
- allowedExports = new Set(exports);
1405
- }
1406
-
1407
- // Validate exportName against allowedExports
1408
- if (
1409
- !exportName ||
1410
- !allowedExports ||
1411
- !allowedExports.has(exportName)
1412
- ) {
1413
- return res.status(400).json({ error: "Invalid export name" });
1414
- }
1415
-
1416
- // Proceed with import
1417
- const mod = await importModule(absolutePath);
1418
-
1419
- // Validate exportName
1420
- if (!exportName || (exportName !== "default" && !mod[exportName])) {
1421
- return res.status(400).json({ error: "Invalid export name" });
1422
- }
1423
- const fn = exportName === "default" ? mod.default : mod[exportName];
1424
-
1425
- if (typeof fn !== "function") {
1426
- return res.status(400).json({ error: "Export is not a function" });
1427
- }
1428
-
1429
- const context = getContextForServerFunctionEndpoint(req, res);
1430
-
1431
- let result;
1432
- try {
1433
- result = await requestStorage.run(context, async () => {
1434
- return await fn(...args);
1435
- });
1436
- } catch (err) {
1437
- // šŸ’” WE INTERCEPT THE REDIRECT
1438
- if (err && err.$$type === "dinou-internal-redirect") {
1439
- const safeUrl = JSON.stringify(err.url);
1440
-
1441
- if (!res.headersSent) {
1442
- // SCENARIO A: Clean (Content-Type application/json)
1443
- res.setHeader("Content-Type", "application/json");
1444
- res.setHeader("X-Dinou-Redirect", err.url);
1445
- return res.status(200).json({ redirect: err.url });
1446
- } else {
1447
- // SCENARIO B: Dirty/Active Stream
1448
- // Write a custom line-based stream command
1449
- res.write(`D:{"type":"redirect","url":${safeUrl}}\n`);
1450
-
1451
- // āš ļø IMPORTANT:
1452
- // 1. We close the response, since we redirected and there will be no RSC payload.
1453
- // 2. WE STOP execution so it doesn't continue to res.json() below.
1454
- res.end();
1455
- return;
1456
- }
1457
- }
1458
- throw err; // If it's another error, throw it to the outer catch
1459
- }
1460
-
1461
- if (!res.headersSent) res.setHeader("Content-Type", "text/x-component");
1462
- const manifestPath = path.resolve(
1463
- process.cwd(),
1464
- isWebpack
1465
- ? `${outputFolder}/react-client-manifest.json`
1466
- : `.dinou/react_client_manifest/react-client-manifest.json`,
1467
- );
1468
- // Verify that the manifest exists to avoid errors
1469
- if (!existsSync(manifestPath)) {
1470
- return res.status(500).json({ error: "Manifest not found" });
1471
- }
1472
- const manifest = isDevelopment
1473
- ? JSON.parse(readFileSync(manifestPath, "utf8"))
1474
- : cachedClientManifest;
1475
- const { pipe } = isWebpack
1476
- ? renderToPipeableStream(result, manifest)
1477
- : renderToPipeableStream(result, pathToFileURL(process.cwd()).href + "/");
1478
- pipe(res);
1479
- } catch (err) {
1480
- console.error(`Server function error [${req.body?.id}]:`, err);
1481
- // In production, do not send full err.message to avoid leaks
1482
- res.status(500).json({ error: "Internal server error" });
1483
- }
1484
- });
1485
-
1486
- const port = process.env.PORT || 3000;
1487
-
1488
- const http = require("http");
1489
-
1490
- // ============================================================
1491
- // STARTUP SEQUENCE
1492
- // ============================================================
1493
- (async () => {
1494
- try {
1495
- console.log("šŸ‘‰ [Startup] Initializing HTTP Server...");
1496
- const server = http.createServer(app);
1497
-
1498
- // 2. ERROR HANDLING (Anti-Zombies)
1499
- server.on("error", (error) => {
1500
- if (error.code === "EADDRINUSE") {
1501
- console.error(`\nāŒ FATAL ERROR: Port ${port} is already in use!`);
1502
- } else {
1503
- console.error("āŒ [Server Error]:", error);
1504
- }
1505
- process.exit(1);
1506
- });
1507
-
1508
- await new Promise((resolve) => {
1509
- server.listen(port, () => {
1510
- console.log(
1511
- `\nšŸš€ Dinou Server is ready and listening on http://localhost:${port}`,
1512
- );
1513
- console.log(
1514
- ` Environment: ${isDevelopment ? "Development" : "Production"}`,
1515
- );
1516
- resolve();
1517
- });
1518
- });
1519
-
1520
- if (!isDevelopment) {
1521
- console.log("šŸ—ļø [Background] Starting static generation (SSG)...");
1522
-
1523
- generateStatic()
1524
- .then(() => {
1525
- console.log("āœ… [Background] Static generation finished.");
1526
- isReady = true;
1527
- })
1528
- .catch((err) => {
1529
- console.error(
1530
- "āŒ [Background] Static generation failed (App continues in Dynamic Mode):",
1531
- err,
1532
- );
1533
- isReady = true;
1534
- });
1535
- } else {
1536
- console.log("āš™ļø [Startup] Running in Development Mode");
1537
- }
1538
- } catch (error) {
1539
- console.error("šŸ’„ [Fatal Startup Error]:", error);
1540
- process.exit(1);
1541
- }
1542
- })();