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.
- package/CHANGELOG.md +99 -0
- package/README.md +9461 -1080
- package/cli.js +392 -169
- package/dinou/adapters/bun.js +62 -0
- package/dinou/adapters/cloudflare.js +65 -0
- package/dinou/adapters/deno.js +48 -0
- package/dinou/adapters/netlify.js +85 -0
- package/dinou/adapters/node.mjs +31 -0
- package/dinou/bun/build.mjs +1077 -0
- package/dinou/cloudflare/build.mjs +1121 -0
- package/dinou/cloudflare/shims/child_process.js +14 -0
- package/dinou/cloudflare/shims/empty.js +2 -0
- package/dinou/cloudflare/shims/fs.js +99 -0
- package/dinou/cloudflare/shims/os.js +29 -0
- package/dinou/config.js +1 -0
- package/dinou/config.mjs +2 -0
- package/dinou/constants.js +29 -4
- package/dinou/core/babel-esm-loader.js +66 -37
- package/dinou/core/bailout-proxy.js +61 -0
- package/dinou/core/build-static-pages.js +309 -157
- package/dinou/core/cache-revalidate.js +471 -144
- package/dinou/core/cache-slot.js +387 -0
- package/dinou/core/client-redirect.jsx +12 -7
- package/dinou/core/client-webpack.jsx +457 -82
- package/dinou/core/client.jsx +452 -82
- package/dinou/core/config.js +11 -0
- package/dinou/core/dinou-paths.js +52 -0
- package/dinou/core/edge-ssr.js +122 -0
- package/dinou/core/export-static.mjs +64 -0
- package/dinou/core/generate-route-types.js +252 -0
- package/dinou/core/generate-static-page.js +55 -94
- package/dinou/core/generate-static-rsc.js +47 -15
- package/dinou/core/generate-static.js +212 -10
- package/dinou/core/generating-isg.js +5 -0
- package/dinou/core/get-asset-from-manifest.js +9 -3
- package/dinou/core/get-error-jsx.js +221 -41
- package/dinou/core/get-file-path-and-dynamic-params.js +9 -13
- package/dinou/core/get-jsx.js +172 -95
- package/dinou/core/handler.js +2296 -0
- package/dinou/core/http-adapter.js +115 -0
- package/dinou/core/import-module.js +83 -53
- package/dinou/core/layout-functions.js +181 -0
- package/dinou/core/link.jsx +18 -6
- package/dinou/core/load-dinou-config.js +60 -0
- package/dinou/core/manifest-provider.js +261 -0
- package/dinou/core/navigation-utils.js +4 -0
- package/dinou/core/navigation.js +50 -39
- package/dinou/core/parse-exports.js +207 -2
- package/dinou/core/ppr-context.js +105 -0
- package/dinou/core/ppr-runtime.js +263 -0
- package/dinou/core/redirect.jsx +20 -19
- package/dinou/core/render-jsx-to-client-jsx.js +133 -15
- package/dinou/core/request-context.js +47 -13
- package/dinou/core/resolve-module-ppr.js +32 -0
- package/dinou/core/revalidating.js +28 -9
- package/dinou/core/route-generator.js +107 -0
- package/dinou/core/rsc-renderer.js +90 -0
- package/dinou/core/run-ssg.js +19 -0
- package/dinou/core/scan-dependency-components.js +218 -0
- package/dinou/core/server-function-proxy-webpack.js +3 -0
- package/dinou/core/server-function-proxy.js +3 -0
- package/dinou/core/slot.js +302 -0
- package/dinou/core/status-manifest.js +41 -1
- package/dinou/core/storage-adapter.js +522 -0
- package/dinou/core/url-resolver.js +16 -16
- package/dinou/core/vfs.js +139 -68
- package/dinou/deno/build.mjs +1112 -0
- package/dinou/deno/seed-kv.js +115 -0
- package/dinou/esbuild/build.mjs +51 -1
- package/dinou/esbuild/dev.mjs +290 -224
- package/dinou/esbuild/helpers-esbuild/get-config-esbuild-prod.mjs +3 -2
- package/dinou/esbuild/helpers-esbuild/get-config-esbuild.mjs +104 -16
- package/dinou/esbuild/helpers-esbuild/get-esbuild-entries.mjs +31 -19
- package/dinou/esbuild/helpers-esbuild/strip-namespace.mjs +9 -0
- package/dinou/esbuild/helpers-esbuild/swc-disk-cache.mjs +225 -0
- package/dinou/esbuild/helpers-esbuild/update-manifest-for-module.mjs +7 -0
- package/dinou/esbuild/helpers-esbuild/write.mjs +111 -2
- package/dinou/esbuild/plugins-esbuild/assets-plugin.mjs +236 -161
- package/dinou/esbuild/plugins-esbuild/css-processor-plugin.mjs +101 -5
- package/dinou/esbuild/plugins-esbuild/react-client-manifest-plugin.mjs +63 -24
- package/dinou/esbuild/plugins-esbuild/server-functions-plugin.mjs +177 -56
- package/dinou/esbuild/plugins-esbuild/skip-missing-entry-points-plugin.mjs +3 -0
- package/dinou/esbuild/plugins-esbuild/stable-chunk-names-and-maps-plugin.mjs +182 -137
- package/dinou/esbuild/plugins-postcss/postcss-extract-plugin.js +38 -11
- package/dinou/esbuild/react-refresh/esm-hmr/client.mjs +55 -8
- package/dinou/esbuild/react-refresh/esm-hmr/server.js +1 -1
- package/dinou/esbuild/react-refresh/esm-hmr-plugin.mjs +238 -101
- package/dinou/esbuild/react-refresh/is-react-refresh-boundary.mjs +18 -4
- package/dinou/index.d.ts +315 -11
- package/dinou/index.js +12 -6
- package/dinou/index.mjs +9 -0
- package/dinou/node/build.mjs +195 -0
- package/dinou/node/bundle-dual-engine.mjs +1012 -0
- package/dinou/node/dev.mjs +2589 -0
- package/dinou/node/port-selector.mjs +164 -0
- package/dinou/node/terminal-status.mjs +345 -0
- package/dinou/package.json +17 -14
- package/dinou/rollup/react-refresh/esm-hmr/client.js +66 -10
- package/dinou/rollup/react-refresh/esm-hmr/server.js +1 -1
- package/dinou/rollup/react-refresh/is-react-refresh-boundary.js +24 -5
- package/dinou/rollup/react-refresh/react-refresh-runtime.js +13 -6
- package/dinou/rollup/react-refresh/react-refresh-wrap-modules.js +56 -11
- package/dinou/rollup/react-refresh/rollup-plugin-esm-hmr.js +176 -48
- package/dinou/rollup/rollup-plugins/dinou-asset-plugin.js +9 -5
- package/dinou/rollup/rollup-plugins/rollup-plugin-memory.js +66 -0
- package/dinou/rollup/rollup-plugins/rollup-plugin-react-client-manifest.js +468 -123
- package/dinou/rollup/rollup-plugins/rollup-plugin-server-functions.js +44 -12
- package/dinou/rollup/rollup-plugins/rollup-plugin-swc.js +100 -0
- package/dinou/rollup/rollup.config.js +155 -43
- package/dinou/server.d.ts +114 -12
- package/dinou/server.js +35 -10
- package/dinou/server.mjs +30 -9
- package/dinou/webpack/helpers/get-webpack-entries.js +14 -19
- package/dinou/webpack/loaders/server-functions-loader.js +22 -1
- package/dinou/webpack/loaders/swc-loader.js +76 -0
- package/dinou/webpack/plugins/manifest-generator-plugin.js +1 -0
- package/dinou/webpack/plugins/server-functions-plugin.js +75 -19
- package/dinou/webpack/plugins/webpack-memory-plugin.js +66 -0
- package/dinou/webpack/webpack.config.js +227 -29
- package/eject.js +173 -94
- package/package.json +110 -107
- package/dinou/core/asset-require-hook.js +0 -58
- package/dinou/core/client-error-webpack.jsx +0 -432
- package/dinou/core/client-error.jsx +0 -432
- package/dinou/core/concurrency-manager.js +0 -48
- package/dinou/core/context-proxy.js +0 -68
- package/dinou/core/generate-static-pages.js +0 -130
- package/dinou/core/generate-static-rscs.js +0 -151
- package/dinou/core/get-ssg-metadata.js +0 -31
- package/dinou/core/register-loader.mjs +0 -11
- package/dinou/core/register-paths.js +0 -27
- package/dinou/core/render-app-to-html.js +0 -430
- package/dinou/core/render-html.js +0 -407
- package/dinou/core/server.js +0 -1542
- package/dinou/esbuild/plugins-esbuild/write-metafile-plugin.mjs +0 -16
package/dinou/core/server.js
DELETED
|
@@ -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
|
-
})();
|