dinou 6.1.1 → 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 +94 -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 +16 -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 -108
- 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
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const { PassThrough } = require("stream");
|
|
4
|
-
const url = require("url");
|
|
5
|
-
const getJSX = require("./get-jsx.js");
|
|
6
|
-
const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
|
|
7
|
-
const { renderToPipeableStream } = isWebpack
|
|
8
|
-
? require("react-server-dom-webpack/server")
|
|
9
|
-
: require("@roggc/react-server-dom-esm/server");
|
|
10
|
-
|
|
11
|
-
// 👇 Your context storage
|
|
12
|
-
const { requestStorage } = require("./request-context.js");
|
|
13
|
-
|
|
14
|
-
const OUT_DIR = path.resolve(".dinou/dist2");
|
|
15
|
-
// const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
|
|
16
|
-
|
|
17
|
-
async function generateStaticRSCs(routes) {
|
|
18
|
-
const manifest = JSON.parse(
|
|
19
|
-
fs.readFileSync(
|
|
20
|
-
path.resolve(
|
|
21
|
-
isWebpack
|
|
22
|
-
? ".dinou/dist3/react-client-manifest.json"
|
|
23
|
-
: ".dinou/react_client_manifest/react-client-manifest.json"
|
|
24
|
-
),
|
|
25
|
-
"utf8"
|
|
26
|
-
)
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
for (const route of routes) {
|
|
30
|
-
const reqPath = route.endsWith("/") ? route : route + "/";
|
|
31
|
-
const payloadPath = path.join(OUT_DIR, reqPath, "rsc.rsc");
|
|
32
|
-
|
|
33
|
-
// ====================================================================
|
|
34
|
-
// 1. MOCK RES: Fulfilling the ResponseProxy interface
|
|
35
|
-
// ====================================================================
|
|
36
|
-
// Although the contract says it returns void, internally we save
|
|
37
|
-
// the state in case you want to log errors (e.g., a redirect at build time).
|
|
38
|
-
const mockRes = {
|
|
39
|
-
_statusCode: 200,
|
|
40
|
-
_headers: {},
|
|
41
|
-
_redirectUrl: null,
|
|
42
|
-
_cookies: [], // Optional: for debug
|
|
43
|
-
|
|
44
|
-
// 👇 ADD THIS METHOD
|
|
45
|
-
cookie(name, value, options) {
|
|
46
|
-
// In SSG we don't do anything real, but we save a record if you want to debug
|
|
47
|
-
// console.log(`[SSG] Cookie set ignored: ${name}=${value}`);
|
|
48
|
-
this._cookies.push({ name, value, options });
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
// clearCookie(name: string, options?: ...): void;
|
|
52
|
-
clearCookie(name, options) {
|
|
53
|
-
// In SSG we don't do anything, but we fulfill the contract avoiding crash
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
// setHeader(name: string, value: string | ReadonlyArray<string>): void;
|
|
57
|
-
setHeader(name, value) {
|
|
58
|
-
this._headers[name.toLowerCase()] = value;
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
// status(code: number): void;
|
|
62
|
-
status(code) {
|
|
63
|
-
this._statusCode = code;
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
// redirect(status: number, url: string): void;
|
|
67
|
-
// redirect(url: string): void;
|
|
68
|
-
redirect(arg1, arg2) {
|
|
69
|
-
let status = 302;
|
|
70
|
-
let url = "";
|
|
71
|
-
|
|
72
|
-
if (typeof arg1 === "number") {
|
|
73
|
-
status = arg1;
|
|
74
|
-
url = arg2;
|
|
75
|
-
} else {
|
|
76
|
-
url = arg1;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this._statusCode = status;
|
|
80
|
-
this._redirectUrl = url;
|
|
81
|
-
|
|
82
|
-
// We log a warning because a redirect in SSG is usually problematic
|
|
83
|
-
console.warn(
|
|
84
|
-
`⚠️ [SSG] Redirect detected in ${reqPath} -> ${url} (${status})`
|
|
85
|
-
);
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// ====================================================================
|
|
90
|
-
// 2. MOCK REQ: Fulfilling RequestContextStore['req']
|
|
91
|
-
// ====================================================================
|
|
92
|
-
const mockReq = {
|
|
93
|
-
query: {},
|
|
94
|
-
cookies: {},
|
|
95
|
-
headers: {
|
|
96
|
-
"user-agent": "Dinou-SSG-Builder",
|
|
97
|
-
host: "localhost",
|
|
98
|
-
// Add here any default header you need
|
|
99
|
-
},
|
|
100
|
-
path: reqPath,
|
|
101
|
-
method: "GET",
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// 3. COMPLETE CONTEXT
|
|
105
|
-
const mockContext = {
|
|
106
|
-
req: mockReq,
|
|
107
|
-
res: mockRes,
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
fs.mkdirSync(path.dirname(payloadPath), { recursive: true });
|
|
112
|
-
const fileStream = fs.createWriteStream(payloadPath);
|
|
113
|
-
const passThrough = new PassThrough();
|
|
114
|
-
|
|
115
|
-
// 1. The entire lifecycle of the stream must be inside the storage
|
|
116
|
-
await requestStorage.run(mockContext, async () => {
|
|
117
|
-
const jsx = await getJSX(reqPath, {}, null, false);
|
|
118
|
-
const { pipe } = isWebpack
|
|
119
|
-
? renderToPipeableStream(jsx, manifest)
|
|
120
|
-
: renderToPipeableStream(jsx, url.pathToFileURL(process.cwd()).href + "/");
|
|
121
|
-
|
|
122
|
-
pipe(passThrough);
|
|
123
|
-
passThrough.pipe(fileStream);
|
|
124
|
-
|
|
125
|
-
// 2. IMPORTANT: We wait for the file to be written COMPLETELY
|
|
126
|
-
// before exiting the 'run' block.
|
|
127
|
-
await new Promise((resolve, reject) => {
|
|
128
|
-
fileStream.on("finish", resolve);
|
|
129
|
-
fileStream.on("error", reject);
|
|
130
|
-
// Optional: handle errors from passThrough as well
|
|
131
|
-
passThrough.on("error", reject);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Post-generation validation
|
|
136
|
-
if (mockRes._statusCode !== 200) {
|
|
137
|
-
console.log(
|
|
138
|
-
`⚠️ Generated RSC for ${reqPath} but logic set status to ${mockRes._statusCode}`
|
|
139
|
-
);
|
|
140
|
-
} else {
|
|
141
|
-
console.log("✅ Generated RSC payload:", reqPath);
|
|
142
|
-
}
|
|
143
|
-
} catch (error) {
|
|
144
|
-
console.error("❌ Error generating RSC payload for:", reqPath, error);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
console.log("🟢 Static RSC payload generation complete.");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
module.exports = generateStaticRSCs;
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
function processMetadata(effects) {
|
|
2
|
-
if (!effects) return "";
|
|
3
|
-
|
|
4
|
-
let scriptContent = "";
|
|
5
|
-
|
|
6
|
-
if (effects.cookies && effects.cookies.length > 0) {
|
|
7
|
-
effects.cookies.forEach((ck) => {
|
|
8
|
-
const name = JSON.stringify(ck.name);
|
|
9
|
-
const value = JSON.stringify(ck.value || "");
|
|
10
|
-
const path = JSON.stringify(ck.options?.path || "/");
|
|
11
|
-
|
|
12
|
-
if (ck.isClear) {
|
|
13
|
-
scriptContent += `document.cookie = ${name} + "=; Max-Age=0; path=" + ${path} + ";";`;
|
|
14
|
-
} else {
|
|
15
|
-
scriptContent += `document.cookie = ${name} + "=" + ${value} + "; path=" + ${path} + ";";`;
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (effects.redirect) {
|
|
21
|
-
scriptContent += `window.location.href = "${effects.redirect}";`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (!scriptContent) return "";
|
|
25
|
-
|
|
26
|
-
return `<script>(function(){ ${scriptContent} })();</script>`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
module.exports = {
|
|
30
|
-
processMetadata,
|
|
31
|
-
};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// dinou/register-loader.mjs
|
|
2
|
-
import { register } from "node:module";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
|
|
6
|
-
const require = createRequire(import.meta.url);
|
|
7
|
-
globalThis.__dinou_require__ = require;
|
|
8
|
-
|
|
9
|
-
const loaderPath = require.resolve("./babel-esm-loader.js");
|
|
10
|
-
|
|
11
|
-
register(pathToFileURL(loaderPath).href, pathToFileURL("./"));
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
const tsconfigPaths = require("tsconfig-paths");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
|
|
5
|
-
function getConfigFileIfExists() {
|
|
6
|
-
const tsconfigPath = path.resolve(process.cwd(), "tsconfig.json");
|
|
7
|
-
const jsconfigPath = path.resolve(process.cwd(), "jsconfig.json");
|
|
8
|
-
|
|
9
|
-
if (fs.existsSync(tsconfigPath)) return tsconfigPath;
|
|
10
|
-
if (fs.existsSync(jsconfigPath)) return jsconfigPath;
|
|
11
|
-
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const configFile = getConfigFileIfExists();
|
|
16
|
-
|
|
17
|
-
if (configFile) {
|
|
18
|
-
const config = require(configFile);
|
|
19
|
-
const { baseUrl, paths } = config.compilerOptions || {};
|
|
20
|
-
|
|
21
|
-
if (baseUrl && paths) {
|
|
22
|
-
tsconfigPaths.register({
|
|
23
|
-
baseUrl: path.resolve(process.cwd(), baseUrl),
|
|
24
|
-
paths,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1,430 +0,0 @@
|
|
|
1
|
-
const path = require("path");
|
|
2
|
-
const { fork } = require("child_process");
|
|
3
|
-
const url = require("url");
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
const getJSX = require("./get-jsx.js");
|
|
6
|
-
const { requestStorage } = require("./request-context.js");
|
|
7
|
-
|
|
8
|
-
const isDevelopment = process.env.NODE_ENV !== "production";
|
|
9
|
-
const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
|
|
10
|
-
|
|
11
|
-
const { renderToPipeableStream } = isWebpack
|
|
12
|
-
? require("react-server-dom-webpack/server")
|
|
13
|
-
: require("@roggc/react-server-dom-esm/server");
|
|
14
|
-
|
|
15
|
-
const manifestPath = path.resolve(
|
|
16
|
-
process.cwd(),
|
|
17
|
-
isWebpack
|
|
18
|
-
? (isDevelopment ? ".dinou/public/react-client-manifest.json" : ".dinou/dist3/react-client-manifest.json")
|
|
19
|
-
: ".dinou/react_client_manifest/react-client-manifest.json"
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
let cachedManifest = null;
|
|
23
|
-
function getManifest() {
|
|
24
|
-
if (!isDevelopment && cachedManifest) return cachedManifest;
|
|
25
|
-
try {
|
|
26
|
-
const content = fs.readFileSync(manifestPath, "utf8");
|
|
27
|
-
const parsed = JSON.parse(content);
|
|
28
|
-
if (parsed && Object.keys(parsed).length > 0) {
|
|
29
|
-
cachedManifest = parsed;
|
|
30
|
-
}
|
|
31
|
-
return cachedManifest || parsed;
|
|
32
|
-
} catch (e) {
|
|
33
|
-
if (cachedManifest) {
|
|
34
|
-
console.warn("Using cached client manifest due to read error:", e.message);
|
|
35
|
-
return cachedManifest;
|
|
36
|
-
}
|
|
37
|
-
console.error("Error reading client manifest:", e);
|
|
38
|
-
return {};
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function toFileUrl(p) {
|
|
43
|
-
return url.pathToFileURL(p).href;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const registerLoaderPath = toFileUrl(
|
|
47
|
-
path.join(__dirname, "register-loader.mjs"),
|
|
48
|
-
);
|
|
49
|
-
const renderHtmlPath = path.resolve(__dirname, "render-html.js");
|
|
50
|
-
|
|
51
|
-
const ESSENTIAL_NODE_ARGS = [];
|
|
52
|
-
const loaderArg = `--import=${registerLoaderPath}`;
|
|
53
|
-
const childExecArgv = ESSENTIAL_NODE_ARGS.concat(loaderArg);
|
|
54
|
-
|
|
55
|
-
const { resolveRelativeUrl } = require("./url-resolver");
|
|
56
|
-
|
|
57
|
-
function createParentResponseWrapper(reqPath, res, child) {
|
|
58
|
-
let hasRedirected = false;
|
|
59
|
-
|
|
60
|
-
const safeRedirect = (targetUrl) => {
|
|
61
|
-
if (hasRedirected) return;
|
|
62
|
-
hasRedirected = true;
|
|
63
|
-
|
|
64
|
-
const resolvedUrl = resolveRelativeUrl(targetUrl, reqPath);
|
|
65
|
-
let finalUrl = "/";
|
|
66
|
-
if (
|
|
67
|
-
typeof resolvedUrl === "string" &&
|
|
68
|
-
resolvedUrl.startsWith("/") &&
|
|
69
|
-
!resolvedUrl.startsWith("//")
|
|
70
|
-
) {
|
|
71
|
-
finalUrl = resolvedUrl;
|
|
72
|
-
} else {
|
|
73
|
-
console.warn(
|
|
74
|
-
`[Dinou Security] Blocked unsafe redirect to: ${targetUrl}`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (res.headersSent) {
|
|
79
|
-
console.log(
|
|
80
|
-
`[Dinou] Streaming active. Redirecting via JavaScript to: ${finalUrl}`,
|
|
81
|
-
);
|
|
82
|
-
const safeUrl = JSON.stringify(finalUrl);
|
|
83
|
-
res.write(`<script>window.location.href = ${safeUrl};</script>`);
|
|
84
|
-
res.end();
|
|
85
|
-
child.stdout.unpipe(res);
|
|
86
|
-
child.kill();
|
|
87
|
-
} else {
|
|
88
|
-
res.redirect(302, finalUrl);
|
|
89
|
-
child.stdout.unpipe(res);
|
|
90
|
-
child.kill();
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
setHeader: (name, value) => {
|
|
96
|
-
if (res.headersSent) {
|
|
97
|
-
console.warn(
|
|
98
|
-
`[Dinou Warning] Cannot set header '${name}' because streaming started.`,
|
|
99
|
-
);
|
|
100
|
-
} else {
|
|
101
|
-
res.setHeader(name, value);
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
cookie: (name, value, options) => {
|
|
105
|
-
if (res.headersSent) {
|
|
106
|
-
if (options && options.httpOnly) {
|
|
107
|
-
console.error(
|
|
108
|
-
`[Dinou Error] Cannot set HttpOnly cookie '${name}' because streaming has already started.`,
|
|
109
|
-
);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
console.log(
|
|
113
|
-
`[Dinou] Streaming active. Setting cookie '${name}' via JS.`,
|
|
114
|
-
);
|
|
115
|
-
let cookieStr = `${name}=${encodeURIComponent(value)}`;
|
|
116
|
-
if (options) {
|
|
117
|
-
if (options.path) cookieStr += `; path=${options.path}`;
|
|
118
|
-
if (options.domain) cookieStr += `; domain=${options.domain}`;
|
|
119
|
-
if (options.maxAge) cookieStr += `; max-age=${options.maxAge}`;
|
|
120
|
-
if (options.expires)
|
|
121
|
-
cookieStr += `; expires=${new Date(options.expires).toUTCString()}`;
|
|
122
|
-
if (options.secure) cookieStr += `; secure`;
|
|
123
|
-
if (options.sameSite)
|
|
124
|
-
cookieStr += `; samesite=${options.sameSite}`;
|
|
125
|
-
}
|
|
126
|
-
const safeCookieStr = JSON.stringify(cookieStr);
|
|
127
|
-
res.write(`<script>document.cookie = ${safeCookieStr};</script>`);
|
|
128
|
-
} else {
|
|
129
|
-
res.cookie(name, value, options);
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
clearCookie: (name, options) => {
|
|
133
|
-
if (res.headersSent) {
|
|
134
|
-
console.log(
|
|
135
|
-
`[Dinou] Streaming active. Clearing cookie '${name}' via JS.`,
|
|
136
|
-
);
|
|
137
|
-
let cookieStr = `${name}=; Max-Age=0`;
|
|
138
|
-
const path = options?.path || "/";
|
|
139
|
-
cookieStr += `; path=${path}`;
|
|
140
|
-
if (options) {
|
|
141
|
-
if (options.domain) cookieStr += `; domain=${options.domain}`;
|
|
142
|
-
if (options.secure) cookieStr += `; secure`;
|
|
143
|
-
if (options.sameSite) cookieStr += `; samesite=${options.sameSite}`;
|
|
144
|
-
}
|
|
145
|
-
cookieStr += ";";
|
|
146
|
-
const safeCookieStr = JSON.stringify(cookieStr);
|
|
147
|
-
res.write(`<script>document.cookie = ${safeCookieStr};</script>`);
|
|
148
|
-
} else {
|
|
149
|
-
res.clearCookie(name, options);
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
redirect: (arg1, arg2) => {
|
|
153
|
-
const url = arg2 || arg1;
|
|
154
|
-
safeRedirect(url);
|
|
155
|
-
},
|
|
156
|
-
status: (code) => {
|
|
157
|
-
if (res.headersSent) {
|
|
158
|
-
console.warn(
|
|
159
|
-
`[Dinou Warning] HTTP status '${code}' ignored because streaming started.`,
|
|
160
|
-
);
|
|
161
|
-
} else {
|
|
162
|
-
res.status(code);
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function renderAppToHtml(
|
|
169
|
-
reqPath,
|
|
170
|
-
paramsString,
|
|
171
|
-
contextForChild,
|
|
172
|
-
res,
|
|
173
|
-
capturedStatus = null,
|
|
174
|
-
isDynamic = false,
|
|
175
|
-
forceNotFound = false,
|
|
176
|
-
) {
|
|
177
|
-
const child = fork(
|
|
178
|
-
renderHtmlPath,
|
|
179
|
-
[
|
|
180
|
-
reqPath,
|
|
181
|
-
paramsString,
|
|
182
|
-
contextForChild ? JSON.stringify(contextForChild) : JSON.stringify({}),
|
|
183
|
-
isDynamic ? "true" : "false",
|
|
184
|
-
],
|
|
185
|
-
{
|
|
186
|
-
execArgv: childExecArgv,
|
|
187
|
-
stdio: ["ignore", "pipe", "pipe", "ipc", "pipe"], // fd 4 is the RSC stream pipe
|
|
188
|
-
},
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
const query = JSON.parse(paramsString || "{}");
|
|
192
|
-
const rscPath = path.resolve(process.cwd(), ".dinou/dist2", reqPath.replace(/^\//, ""), "rsc.rsc");
|
|
193
|
-
const hasStaticRsc = !isDynamic && fs.existsSync(rscPath);
|
|
194
|
-
|
|
195
|
-
if (hasStaticRsc) {
|
|
196
|
-
try {
|
|
197
|
-
const rscBuffer = fs.readFileSync(rscPath);
|
|
198
|
-
child.stdio[4].write(rscBuffer);
|
|
199
|
-
child.stdio[4].end();
|
|
200
|
-
} catch (err) {
|
|
201
|
-
console.error(`[Dinou] Failed to read static RSC from ${rscPath}:`, err.message);
|
|
202
|
-
if (child.stdio[4]) child.stdio[4].destroy();
|
|
203
|
-
}
|
|
204
|
-
} else {
|
|
205
|
-
// Dynamic SSR render: render RSC in parent and pipe to fd 4
|
|
206
|
-
const isNotFound = {};
|
|
207
|
-
const parentRes = createParentResponseWrapper(reqPath, res, child);
|
|
208
|
-
const context = {
|
|
209
|
-
req: contextForChild ? contextForChild.req : {},
|
|
210
|
-
res: parentRes,
|
|
211
|
-
};
|
|
212
|
-
requestStorage.run(context, () => {
|
|
213
|
-
getJSX(reqPath, query, isNotFound, isDevelopment, forceNotFound)
|
|
214
|
-
.then((jsx) => {
|
|
215
|
-
if (isNotFound.value) {
|
|
216
|
-
parentRes.status(404);
|
|
217
|
-
}
|
|
218
|
-
const manifest = getManifest();
|
|
219
|
-
const { pipe } = isWebpack
|
|
220
|
-
? renderToPipeableStream(jsx, manifest)
|
|
221
|
-
: renderToPipeableStream(jsx, url.pathToFileURL(process.cwd()).href + "/");
|
|
222
|
-
pipe(child.stdio[4]);
|
|
223
|
-
})
|
|
224
|
-
.catch((err) => {
|
|
225
|
-
console.error("Error rendering JSX in parent renderAppToHtml:", err);
|
|
226
|
-
if (child.stdio[4]) child.stdio[4].destroy();
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// 💡 on('message') Implementation (IPC Channel)
|
|
232
|
-
// ----------------------------------------------------
|
|
233
|
-
child.on("message", (message) => {
|
|
234
|
-
if (message && message.type === "DINOU_CONTEXT_COMMAND") {
|
|
235
|
-
const { command, args } = message;
|
|
236
|
-
// console.log(
|
|
237
|
-
// `[Dinou] Received context command from child: ${command}`,
|
|
238
|
-
// args
|
|
239
|
-
// );
|
|
240
|
-
// List of supported commands for quick check
|
|
241
|
-
if (
|
|
242
|
-
command === "setHeader" ||
|
|
243
|
-
command === "clearCookie" ||
|
|
244
|
-
command === "cookie" || // ⬅️ ADDED
|
|
245
|
-
command === "status" ||
|
|
246
|
-
command === "redirect"
|
|
247
|
-
) {
|
|
248
|
-
// ============================================================
|
|
249
|
-
// SCENARIO 1: STREAMING ALREADY STARTED (Headers sent)
|
|
250
|
-
// ============================================================
|
|
251
|
-
if (res.headersSent) {
|
|
252
|
-
// --- REDIRECT (JS Injection) ---
|
|
253
|
-
if (command === "redirect") {
|
|
254
|
-
const rawUrl = args.length === 1 ? args[0] : args[1];
|
|
255
|
-
const resolvedUrl = resolveRelativeUrl(rawUrl, reqPath);
|
|
256
|
-
console.log(
|
|
257
|
-
`[Dinou] Streaming active. Redirecting via JavaScript to: ${resolvedUrl}`,
|
|
258
|
-
);
|
|
259
|
-
let finalUrl = "/";
|
|
260
|
-
if (
|
|
261
|
-
typeof resolvedUrl === "string" &&
|
|
262
|
-
resolvedUrl.startsWith("/") &&
|
|
263
|
-
!resolvedUrl.startsWith("//")
|
|
264
|
-
) {
|
|
265
|
-
finalUrl = resolvedUrl;
|
|
266
|
-
} else {
|
|
267
|
-
console.warn(
|
|
268
|
-
`[Dinou Security] Blocked unsafe streaming redirect to: ${rawUrl}`,
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
const safeUrl = JSON.stringify(finalUrl);
|
|
272
|
-
res.write(`<script>window.location.href = ${safeUrl};</script>`);
|
|
273
|
-
res.end();
|
|
274
|
-
child.stdout.unpipe(res);
|
|
275
|
-
child.kill();
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// --- STATUS (Warning) ---
|
|
280
|
-
if (command === "status") {
|
|
281
|
-
console.warn(
|
|
282
|
-
`[Dinou Warning] HTTP status '${args[0]}' ignored because streaming started.`,
|
|
283
|
-
);
|
|
284
|
-
if (capturedStatus) capturedStatus.value = args[0];
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// --- CLEAR COOKIE (JS Injection - Only Non-HttpOnly) ---
|
|
289
|
-
if (command === "clearCookie") {
|
|
290
|
-
const [name, options] = args;
|
|
291
|
-
console.log(
|
|
292
|
-
`[Dinou] Streaming active. Clearing cookie '${name}' via JS.`,
|
|
293
|
-
);
|
|
294
|
-
let cookieStr = `${name}=; Max-Age=0`;
|
|
295
|
-
const path = options?.path || "/";
|
|
296
|
-
cookieStr += `; path=${path}`;
|
|
297
|
-
if (options) {
|
|
298
|
-
if (options.domain) cookieStr += `; domain=${options.domain}`;
|
|
299
|
-
if (options.secure) cookieStr += `; secure`;
|
|
300
|
-
if (options.sameSite) cookieStr += `; samesite=${options.sameSite}`;
|
|
301
|
-
}
|
|
302
|
-
cookieStr += ";";
|
|
303
|
-
const safeCookieStr = JSON.stringify(cookieStr);
|
|
304
|
-
res.write(
|
|
305
|
-
`<script>document.cookie = ${safeCookieStr};</script>`,
|
|
306
|
-
);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// --- 🍪 SET COOKIE (New - JS Injection) ---
|
|
311
|
-
if (command === "cookie") {
|
|
312
|
-
const [name, value, options] = args;
|
|
313
|
-
|
|
314
|
-
// ⚠️ CRITICAL WARNING:
|
|
315
|
-
// If the cookie is HttpOnly, JS cannot write it.
|
|
316
|
-
if (options && options.httpOnly) {
|
|
317
|
-
console.error(
|
|
318
|
-
`[Dinou Error] Cannot set HttpOnly cookie '${name}' because streaming has already started. ` +
|
|
319
|
-
`Headers are sent, and document.cookie cannot write HttpOnly cookies.`,
|
|
320
|
-
);
|
|
321
|
-
return; // We do nothing because it would fail silently in the browser
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
console.log(
|
|
325
|
-
`[Dinou] Streaming active. Setting cookie '${name}' via JS.`,
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
// We build the cookie string manually for JS
|
|
329
|
-
// Format: name=value; path=/; max-age=...
|
|
330
|
-
let cookieStr = `${name}=${encodeURIComponent(value)}`;
|
|
331
|
-
|
|
332
|
-
if (options) {
|
|
333
|
-
if (options.path) cookieStr += `; path=${options.path}`;
|
|
334
|
-
if (options.domain) cookieStr += `; domain=${options.domain}`;
|
|
335
|
-
if (options.maxAge) cookieStr += `; max-age=${options.maxAge}`;
|
|
336
|
-
if (options.expires)
|
|
337
|
-
cookieStr += `; expires=${new Date(
|
|
338
|
-
options.expires,
|
|
339
|
-
).toUTCString()}`;
|
|
340
|
-
if (options.secure) cookieStr += `; secure`;
|
|
341
|
-
if (options.sameSite)
|
|
342
|
-
cookieStr += `; samesite=${options.sameSite}`;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// console.log(`[Dinou] Cookie string to set: ${cookieStr}`);
|
|
346
|
-
const safeCookieStr = JSON.stringify(cookieStr);
|
|
347
|
-
res.write(`<script>document.cookie = ${safeCookieStr};</script>`);
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// --- SET HEADER (Warning) ---
|
|
352
|
-
if (command === "setHeader") {
|
|
353
|
-
console.warn(
|
|
354
|
-
`[Dinou Warning] Cannot set header '${args[0]}' because streaming started.`,
|
|
355
|
-
);
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ============================================================
|
|
362
|
-
// SCENARIO 2: HEADERS NOT YET SENT (Normal Express usage)
|
|
363
|
-
// ============================================================
|
|
364
|
-
if (typeof res[command] === "function") {
|
|
365
|
-
// Special safe redirect handling
|
|
366
|
-
if (command === "redirect") {
|
|
367
|
-
// 1. Normalize arguments (Status and URL)
|
|
368
|
-
let status = 302; // Express Default
|
|
369
|
-
let rawUrl = "";
|
|
370
|
-
|
|
371
|
-
if (args.length === 2) {
|
|
372
|
-
// Signature: redirect(status, url)
|
|
373
|
-
status = args[0];
|
|
374
|
-
rawUrl = args[1];
|
|
375
|
-
} else {
|
|
376
|
-
// Signature: redirect(url)
|
|
377
|
-
rawUrl = args[0];
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Resolve relative URL relative to reqPath
|
|
381
|
-
const resolvedUrl = resolveRelativeUrl(rawUrl, reqPath);
|
|
382
|
-
|
|
383
|
-
// 2. Security Logic (Safe Redirect)
|
|
384
|
-
// We allow only relative routes starting with '/' but not '//'
|
|
385
|
-
let finalUrl = "/";
|
|
386
|
-
|
|
387
|
-
if (
|
|
388
|
-
typeof resolvedUrl === "string" &&
|
|
389
|
-
resolvedUrl.startsWith("/") &&
|
|
390
|
-
!resolvedUrl.startsWith("//")
|
|
391
|
-
) {
|
|
392
|
-
finalUrl = resolvedUrl;
|
|
393
|
-
} else {
|
|
394
|
-
console.warn(
|
|
395
|
-
`[Dinou Security] Blocked unsafe redirect to: ${rawUrl}`,
|
|
396
|
-
);
|
|
397
|
-
// finalUrl remains "/"
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// 3. Execute safe redirect always using (status, url)
|
|
401
|
-
res.redirect.apply(res, [status, finalUrl]);
|
|
402
|
-
child.stdout.unpipe(res);
|
|
403
|
-
child.kill();
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Standard execution (cookie, clearCookie, status, setHeader)
|
|
408
|
-
res[command].apply(res, args);
|
|
409
|
-
} else {
|
|
410
|
-
console.error(`[Dinou] Unknown context command: ${command}`);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
// ----------------------------------------------------
|
|
415
|
-
|
|
416
|
-
// Capture child errors
|
|
417
|
-
child.on("error", (err) => {
|
|
418
|
-
console.error("Failed to start child process:", err);
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
// Listen to stderr, replicating the behavior of the original function
|
|
422
|
-
child.stderr.on("data", (chunk) => {
|
|
423
|
-
console.error(chunk.toString());
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
// ⬅️ Return identical to original, returning only the HTML stream
|
|
427
|
-
return child.stdout;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
module.exports = renderAppToHtml;
|