dinou 6.1.1 → 7.1.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 +106 -0
- package/README.md +9352 -1081
- package/cli.js +314 -91
- 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 +322 -165
- 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 +175 -96
- package/dinou/core/handler.js +2303 -0
- package/dinou/core/http-adapter.js +115 -0
- package/dinou/core/import-module.js +83 -53
- package/dinou/core/layout-functions.js +183 -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 +252 -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 +91 -87
- package/dinou/core/request-context.js +47 -13
- package/dinou/core/resolve-module-ppr.js +34 -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 +113 -34
- package/package.json +18 -16
- 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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
|
|
4
|
+
function getDinouCoreDir() {
|
|
5
|
+
// Edge runtime guard: en Cloudflare Workers / Deno Edge no hay sistema de archivos dinámico
|
|
6
|
+
if (typeof process !== "undefined" && process.env && process.env.DINOU_RUNTIME === "edge") {
|
|
7
|
+
return "/dinou/core";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// 1. Modo eyectado: ./dinou/core en la raíz del proyecto
|
|
11
|
+
try {
|
|
12
|
+
const cwd = typeof process !== "undefined" && process.cwd ? process.cwd() : "";
|
|
13
|
+
const localCore = path.resolve(cwd, "dinou/core");
|
|
14
|
+
if (fs.existsSync && fs.existsSync(localCore)) return localCore;
|
|
15
|
+
} catch (e) {}
|
|
16
|
+
|
|
17
|
+
// 2. Modo librería sin empaquetar: dinou-paths.js ya está en dinou/core
|
|
18
|
+
if (typeof __dirname !== "undefined") {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync && fs.existsSync(path.join(__dirname, "handler.js"))) {
|
|
21
|
+
return __dirname;
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 3. Resolución estándar de Node
|
|
27
|
+
try {
|
|
28
|
+
const serverPath = require.resolve("dinou/server");
|
|
29
|
+
const resolvedCore = path.join(path.dirname(serverPath), "core");
|
|
30
|
+
if (fs.existsSync && fs.existsSync(resolvedCore)) return resolvedCore;
|
|
31
|
+
} catch (e) { }
|
|
32
|
+
|
|
33
|
+
// 4. Modo bundle de Webpack (ej. corriendo desde .dinou/dist3/server/handler.js)
|
|
34
|
+
// La estructura del paquete instalado es node_modules/dinou/dinou/core
|
|
35
|
+
try {
|
|
36
|
+
const cwd = typeof process !== "undefined" && process.cwd ? process.cwd() : "";
|
|
37
|
+
const nodeModulesDinouCore = path.resolve(cwd, "node_modules/dinou/dinou/core");
|
|
38
|
+
if (fs.existsSync && fs.existsSync(nodeModulesDinouCore)) return nodeModulesDinouCore;
|
|
39
|
+
|
|
40
|
+
// 5. Fallback por si la estructura fuese plana
|
|
41
|
+
return path.resolve(cwd, "node_modules/dinou/core");
|
|
42
|
+
} catch (e) {}
|
|
43
|
+
|
|
44
|
+
return "/dinou/core";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const dinouCoreDir = getDinouCoreDir();
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
dinouCoreDir,
|
|
51
|
+
getDinouCoreDir,
|
|
52
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// dinou/core/edge-ssr.js
|
|
2
|
+
// Native React 19 Edge SSR Streaming Engine for Dinou.
|
|
3
|
+
// Reconstructs React element tree from RSC stream and renders native HTML stream via react-dom/server.edge.
|
|
4
|
+
|
|
5
|
+
import { createFromReadableStream } from "react-server-dom-webpack/client.edge";
|
|
6
|
+
import { renderToReadableStream } from "react-dom/server.edge";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reconstructs an RSC stream into a React element tree and streams HTML via react-dom/server.edge.
|
|
10
|
+
* @param {ReadableStream} rscStream The RSC binary stream
|
|
11
|
+
* @param {object} consumerManifest The client components consumer manifest ({ moduleMap, serverModuleMap, moduleLoading })
|
|
12
|
+
* @param {object} options Configuration options ({ bootstrapModules, bootstrapScriptContent, onError, waitForAll })
|
|
13
|
+
* @returns {Promise<ReadableStream>} The HTML stream
|
|
14
|
+
*/
|
|
15
|
+
export async function renderRscStreamToHtmlStream(rscStream, consumerManifest, options = {}) {
|
|
16
|
+
try {
|
|
17
|
+
let effectiveManifest = consumerManifest;
|
|
18
|
+
if (consumerManifest && consumerManifest.moduleMap && !consumerManifest.moduleMap.__isDinouProxy) {
|
|
19
|
+
const origMap = consumerManifest.moduleMap;
|
|
20
|
+
const proxyMap = new Proxy(origMap, {
|
|
21
|
+
get(target, prop) {
|
|
22
|
+
if (typeof prop !== "string") return target[prop];
|
|
23
|
+
if (prop === "__isDinouProxy") return true;
|
|
24
|
+
if (prop in target) return target[prop];
|
|
25
|
+
|
|
26
|
+
const dynamicMap = typeof globalThis !== "undefined" ? globalThis.__DINOU_DYNAMIC_SSR_MODULE_MAP__ : null;
|
|
27
|
+
if (dynamicMap) {
|
|
28
|
+
if (prop in dynamicMap) {
|
|
29
|
+
target[prop] = dynamicMap[prop];
|
|
30
|
+
return dynamicMap[prop];
|
|
31
|
+
}
|
|
32
|
+
const altProp = prop.startsWith("/") ? prop.slice(1) : "/" + prop;
|
|
33
|
+
if (altProp in dynamicMap) {
|
|
34
|
+
target[prop] = dynamicMap[altProp];
|
|
35
|
+
return dynamicMap[altProp];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const clientManifest = typeof globalThis !== "undefined" ? (globalThis.__DINOU_CLIENT_MANIFEST__ || globalThis.__DINOU_RAW_CLIENT_MANIFEST__) : null;
|
|
40
|
+
if (clientManifest) {
|
|
41
|
+
const cleanProp = prop.startsWith("/") ? prop.slice(1) : prop;
|
|
42
|
+
for (const [k, v] of Object.entries(clientManifest)) {
|
|
43
|
+
if (!v?.id) continue;
|
|
44
|
+
const cleanVId = v.id.startsWith("/") ? v.id.slice(1) : v.id;
|
|
45
|
+
if (v.id === prop || cleanVId === cleanProp || ("/" + cleanVId) === prop) {
|
|
46
|
+
const fileUrl = k.split("#")[0];
|
|
47
|
+
const entry = { "*": { id: fileUrl, chunks: [], name: "*" } };
|
|
48
|
+
target[prop] = entry;
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (prop.startsWith("file://")) {
|
|
55
|
+
const canonicalProp = prop.startsWith("file:///") ? prop : "file:///" + prop.slice(7);
|
|
56
|
+
const entry = { "*": { id: canonicalProp, chunks: [], name: "*" } };
|
|
57
|
+
target[prop] = entry;
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return target[prop];
|
|
62
|
+
},
|
|
63
|
+
has(target, prop) {
|
|
64
|
+
if (prop === "__isDinouProxy") return true;
|
|
65
|
+
if (prop in target) return true;
|
|
66
|
+
const dynamicMap = typeof globalThis !== "undefined" ? globalThis.__DINOU_DYNAMIC_SSR_MODULE_MAP__ : null;
|
|
67
|
+
if (dynamicMap && (prop in dynamicMap || (prop.startsWith("/") ? prop.slice(1) : "/" + prop) in dynamicMap)) return true;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
effectiveManifest = { ...consumerManifest, moduleMap: proxyMap, serverModuleMap: null };
|
|
72
|
+
} else if (effectiveManifest) {
|
|
73
|
+
effectiveManifest = { ...effectiveManifest, serverModuleMap: null };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 1. Reconstruct JSX element tree from RSC wire stream
|
|
77
|
+
const elementTree = await createFromReadableStream(rscStream, {
|
|
78
|
+
serverConsumerManifest: effectiveManifest,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 2. Stream HTML using React 19's native Edge renderer
|
|
82
|
+
const htmlStream = await renderToReadableStream(elementTree, {
|
|
83
|
+
bootstrapModules: options.bootstrapModules || ["/main.js"],
|
|
84
|
+
bootstrapScriptContent: options.bootstrapScriptContent,
|
|
85
|
+
onError(error) {
|
|
86
|
+
if (options.onError) {
|
|
87
|
+
options.onError(error);
|
|
88
|
+
} else {
|
|
89
|
+
console.error("[Edge Native SSR] Render error:", error);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (options.waitForAll) {
|
|
95
|
+
await htmlStream.allReady;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const importMapHtml = options.importMapHtml || (typeof globalThis !== "undefined" && globalThis.__DINOU_IMPORT_MAP_HTML__) || "";
|
|
99
|
+
if (importMapHtml) {
|
|
100
|
+
const encoder = new TextEncoder();
|
|
101
|
+
const importMapBytes = encoder.encode(importMapHtml);
|
|
102
|
+
let injected = false;
|
|
103
|
+
const transform = new TransformStream({
|
|
104
|
+
transform(chunk, controller) {
|
|
105
|
+
if (!injected) {
|
|
106
|
+
injected = true;
|
|
107
|
+
controller.enqueue(importMapBytes);
|
|
108
|
+
}
|
|
109
|
+
controller.enqueue(chunk);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
return htmlStream.pipeThrough(transform);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return htmlStream;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (process.env.DINOU_DEBUG) {
|
|
118
|
+
console.error("[Edge Native SSR] Fatal stream reconstruction error:", err);
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// dinou/core/export-static.mjs
|
|
2
|
+
// Exports a standalone static website from .dinou/dist3 (assets) and .dinou/dist2 (SSG HTML)
|
|
3
|
+
// into an 'out/' directory, ready for Surge.sh, GitHub Pages, Cloudflare Pages (static), etc.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
const projectRoot = process.cwd();
|
|
9
|
+
const outDir = path.resolve(projectRoot, "out");
|
|
10
|
+
const dist3Dir = path.resolve(projectRoot, ".dinou/dist3");
|
|
11
|
+
const dist2Dir = path.resolve(projectRoot, ".dinou/dist2");
|
|
12
|
+
|
|
13
|
+
const bundlerName = process.env.DINOU_BUILD_TOOL || "esbuild";
|
|
14
|
+
console.log(`\n🦖 Dinou v7 (Static Export • ${bundlerName})`);
|
|
15
|
+
|
|
16
|
+
// 1. Clean output directory
|
|
17
|
+
if (fs.existsSync(outDir)) {
|
|
18
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
function copyRecursive(src, dest) {
|
|
23
|
+
if (!fs.existsSync(src)) return 0;
|
|
24
|
+
let count = 0;
|
|
25
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const srcPath = path.join(src, entry.name);
|
|
28
|
+
const destPath = path.join(dest, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
31
|
+
count += copyRecursive(srcPath, destPath);
|
|
32
|
+
} else {
|
|
33
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
34
|
+
fs.copyFileSync(srcPath, destPath);
|
|
35
|
+
count++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return count;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Copy production assets from .dinou/dist3 (JS bundles, CSS, favicons)
|
|
42
|
+
let assetCount = 0;
|
|
43
|
+
if (fs.existsSync(dist3Dir)) {
|
|
44
|
+
assetCount = copyRecursive(dist3Dir, outDir);
|
|
45
|
+
console.log(` 📦 Copied ${assetCount} production asset(s) from .dinou/dist3`);
|
|
46
|
+
} else {
|
|
47
|
+
console.warn("⚠️ [Dinou Export] Warning: .dinou/dist3 not found. Did you run a production build first?");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Copy pre-rendered SSG HTML pages from .dinou/dist2
|
|
51
|
+
let pageCount = 0;
|
|
52
|
+
if (fs.existsSync(dist2Dir)) {
|
|
53
|
+
pageCount = copyRecursive(dist2Dir, outDir);
|
|
54
|
+
console.log(` 📄 Copied ${pageCount} static page/payload file(s) from .dinou/dist2`);
|
|
55
|
+
} else {
|
|
56
|
+
console.warn("⚠️ [Dinou Export] Warning: .dinou/dist2 not found. Ensure your app has static routes.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const relOut = path.relative(projectRoot, outDir) || "out";
|
|
60
|
+
console.log(`\n✓ Standalone static site exported to: ${relOut}/`);
|
|
61
|
+
console.log(" 👉 Deploy with:");
|
|
62
|
+
console.log(" • Surge.sh: npx surge out <your-domain>.surge.sh");
|
|
63
|
+
console.log(" • GitHub Pages: npx gh-pages -d out");
|
|
64
|
+
console.log(" • Cloudflare: npx wrangler pages deploy out\n");
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// dinou/core/generate-route-types.js
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const PAGE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Recursively scans directory to collect all page routes according to Dinou conventions.
|
|
9
|
+
* - Ignores folders starting with '_' (private folders)
|
|
10
|
+
* - Ignores folders starting with '@' (parallel route slots)
|
|
11
|
+
* - Transparently handles route groups '(group)' by stripping them from URL paths
|
|
12
|
+
* - Tracks dynamic parameters: [id], [...slug], [[slug]], [[...slug]]
|
|
13
|
+
*/
|
|
14
|
+
function collectRoutes(dir, segments = [], routes = []) {
|
|
15
|
+
if (!fs.existsSync(dir)) return routes;
|
|
16
|
+
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
20
|
+
} catch (e) {
|
|
21
|
+
return routes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if this folder has a page file
|
|
25
|
+
const hasPage = entries.some(
|
|
26
|
+
(e) => !e.isDirectory() && PAGE_EXTENSIONS.some((ext) => e.name === `page${ext}`)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (hasPage) {
|
|
30
|
+
const routePath = segments.length === 0 ? "/" : "/" + segments.join("/");
|
|
31
|
+
routes.push({
|
|
32
|
+
path: routePath,
|
|
33
|
+
segments: [...segments],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (!entry.isDirectory()) continue;
|
|
39
|
+
|
|
40
|
+
const name = entry.name;
|
|
41
|
+
// Ignore private folders (_*) and parallel route slots (@*)
|
|
42
|
+
if (name.startsWith("_") || name.startsWith("@")) continue;
|
|
43
|
+
|
|
44
|
+
// Route groups: (group) - do not add to URL path
|
|
45
|
+
if (name.startsWith("(") && name.endsWith(")")) {
|
|
46
|
+
collectRoutes(path.join(dir, name), segments, routes);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
collectRoutes(path.join(dir, name), [...segments, name], routes);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return routes;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generates TypeScript type definitions for all discovered routes.
|
|
58
|
+
*/
|
|
59
|
+
function generateRouteDefinitions(routes) {
|
|
60
|
+
const staticRoutes = new Set();
|
|
61
|
+
const dynamicTemplateRoutes = new Set();
|
|
62
|
+
const routeParamsMap = {};
|
|
63
|
+
|
|
64
|
+
for (const route of routes) {
|
|
65
|
+
const segments = route.segments;
|
|
66
|
+
const isDynamic = segments.some((s) => s.startsWith("[") && s.endsWith("]"));
|
|
67
|
+
|
|
68
|
+
// Calculate parameter types for Level 2
|
|
69
|
+
const params = {};
|
|
70
|
+
for (const seg of segments) {
|
|
71
|
+
if (seg.startsWith("[[...") && seg.endsWith("]]")) {
|
|
72
|
+
const paramName = seg.slice(5, -2);
|
|
73
|
+
params[paramName] = "string[] | undefined";
|
|
74
|
+
} else if (seg.startsWith("[[") && seg.endsWith("]]")) {
|
|
75
|
+
const paramName = seg.slice(2, -2);
|
|
76
|
+
params[paramName] = "string | undefined";
|
|
77
|
+
} else if (seg.startsWith("[...") && seg.endsWith("]")) {
|
|
78
|
+
const paramName = seg.slice(4, -1);
|
|
79
|
+
params[paramName] = "string[]";
|
|
80
|
+
} else if (seg.startsWith("[") && seg.endsWith("]")) {
|
|
81
|
+
const paramName = seg.slice(1, -1);
|
|
82
|
+
params[paramName] = "string";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
routeParamsMap[route.path] = params;
|
|
86
|
+
|
|
87
|
+
if (!isDynamic) {
|
|
88
|
+
staticRoutes.add(route.path);
|
|
89
|
+
} else {
|
|
90
|
+
// Build template literal type for Level 1 autocomplete & validation
|
|
91
|
+
const transformedSegments = segments.map((seg) => {
|
|
92
|
+
if (seg.startsWith("[[...") && seg.endsWith("]]")) {
|
|
93
|
+
return "${string}";
|
|
94
|
+
} else if (seg.startsWith("[[") && seg.endsWith("]]")) {
|
|
95
|
+
return "${string | number}";
|
|
96
|
+
} else if (seg.startsWith("[...") && seg.endsWith("]")) {
|
|
97
|
+
return "${string}";
|
|
98
|
+
} else if (seg.startsWith("[") && seg.endsWith("]")) {
|
|
99
|
+
return "${string | number}";
|
|
100
|
+
}
|
|
101
|
+
return seg;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
dynamicTemplateRoutes.add("`/" + transformedSegments.join("/") + "`");
|
|
105
|
+
|
|
106
|
+
// For optional segments ([[foo]] or [[...foo]]), progressively peel off trailing optional
|
|
107
|
+
// segments to register all valid intermediate route variants (including the base static route).
|
|
108
|
+
let cur = [...segments];
|
|
109
|
+
while (cur.length > 0 && cur[cur.length - 1].startsWith("[[") && cur[cur.length - 1].endsWith("]]")) {
|
|
110
|
+
cur.pop();
|
|
111
|
+
if (cur.length === 0) {
|
|
112
|
+
staticRoutes.add("/");
|
|
113
|
+
} else if (!cur.some((s) => s.startsWith("[") && s.endsWith("]"))) {
|
|
114
|
+
staticRoutes.add("/" + cur.join("/"));
|
|
115
|
+
} else {
|
|
116
|
+
const transformed = cur.map((seg) => {
|
|
117
|
+
if (seg.startsWith("[[...") && seg.endsWith("]]")) return "${string}";
|
|
118
|
+
if (seg.startsWith("[[") && seg.endsWith("]]")) return "${string | number}";
|
|
119
|
+
if (seg.startsWith("[...") && seg.endsWith("]")) return "${string}";
|
|
120
|
+
if (seg.startsWith("[") && seg.endsWith("]")) return "${string | number}";
|
|
121
|
+
return seg;
|
|
122
|
+
});
|
|
123
|
+
dynamicTemplateRoutes.add("`/" + transformed.join("/") + "`");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
staticRoutes: Array.from(staticRoutes).sort(),
|
|
131
|
+
dynamicRoutes: Array.from(dynamicTemplateRoutes).sort(),
|
|
132
|
+
routeParamsMap,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generates the .dinou/types/routes.d.ts file and ensures dinou-env.d.ts references it.
|
|
138
|
+
* @param {string} projectRoot Root directory of the project (default process.cwd())
|
|
139
|
+
*/
|
|
140
|
+
function generateRouteTypes(projectRoot = process.cwd()) {
|
|
141
|
+
const srcDir = path.resolve(projectRoot, "src");
|
|
142
|
+
if (!fs.existsSync(srcDir)) return;
|
|
143
|
+
|
|
144
|
+
const routes = collectRoutes(srcDir);
|
|
145
|
+
const { staticRoutes, dynamicRoutes, routeParamsMap } = generateRouteDefinitions(routes);
|
|
146
|
+
|
|
147
|
+
const staticUnion = staticRoutes.length > 0
|
|
148
|
+
? staticRoutes.map((r) => ` | ${JSON.stringify(r)}`).join("\n")
|
|
149
|
+
: " | string";
|
|
150
|
+
|
|
151
|
+
const dynamicUnion = dynamicRoutes.length > 0
|
|
152
|
+
? dynamicRoutes.map((r) => ` | ${r}`).join("\n")
|
|
153
|
+
: " | never";
|
|
154
|
+
|
|
155
|
+
const paramsEntries = Object.entries(routeParamsMap)
|
|
156
|
+
.map(([rPath, pObj]) => {
|
|
157
|
+
const keys = Object.keys(pObj);
|
|
158
|
+
if (keys.length === 0) {
|
|
159
|
+
return ` ${JSON.stringify(rPath)}: Record<string, never>;`;
|
|
160
|
+
}
|
|
161
|
+
const props = keys
|
|
162
|
+
.map((k) => {
|
|
163
|
+
const isOptional = pObj[k].includes("undefined");
|
|
164
|
+
const cleanType = pObj[k].replace(" | undefined", "");
|
|
165
|
+
return `${k}${isOptional ? "?" : ""}: ${cleanType}`;
|
|
166
|
+
})
|
|
167
|
+
.join("; ");
|
|
168
|
+
return ` ${JSON.stringify(rPath)}: { ${props} };`;
|
|
169
|
+
})
|
|
170
|
+
.join("\n");
|
|
171
|
+
|
|
172
|
+
const content = `// This file is auto-generated by Dinou. Do not edit directly.
|
|
173
|
+
import "dinou";
|
|
174
|
+
|
|
175
|
+
declare module "dinou" {
|
|
176
|
+
export type DinouStaticRoutes =
|
|
177
|
+
${staticUnion};
|
|
178
|
+
|
|
179
|
+
export type DinouDynamicRoutes =
|
|
180
|
+
${dynamicUnion};
|
|
181
|
+
|
|
182
|
+
export type DinouInternalRoute = DinouStaticRoutes | DinouDynamicRoutes;
|
|
183
|
+
|
|
184
|
+
export type DinouRelativeOrExternalRoute =
|
|
185
|
+
| \`http://\${string}\`
|
|
186
|
+
| \`https://\${string}\`
|
|
187
|
+
| \`mailto:\${string}\`
|
|
188
|
+
| \`tel:\${string}\`
|
|
189
|
+
| \`./\${string}\`
|
|
190
|
+
| \`../\${string}\`
|
|
191
|
+
| \`#\${string}\`
|
|
192
|
+
| \`?\${string}\`;
|
|
193
|
+
|
|
194
|
+
export type DinouGeneratedRoute =
|
|
195
|
+
| DinouInternalRoute
|
|
196
|
+
| \`\${DinouInternalRoute}?\${string}\`
|
|
197
|
+
| \`\${DinouInternalRoute}#\${string}\`
|
|
198
|
+
| DinouRelativeOrExternalRoute;
|
|
199
|
+
|
|
200
|
+
export interface DinouRouteParamsMap {
|
|
201
|
+
${paramsEntries}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
namespace DinouRouter {
|
|
205
|
+
interface Register {
|
|
206
|
+
route: DinouGeneratedRoute;
|
|
207
|
+
params: DinouRouteParamsMap;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
// Write .dinou/types/routes.d.ts
|
|
214
|
+
const typesDir = path.resolve(projectRoot, ".dinou/types");
|
|
215
|
+
try {
|
|
216
|
+
fs.mkdirSync(typesDir, { recursive: true });
|
|
217
|
+
const targetFile = path.join(typesDir, "routes.d.ts");
|
|
218
|
+
const currentContent = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, "utf8") : "";
|
|
219
|
+
if (currentContent !== content) {
|
|
220
|
+
fs.writeFileSync(targetFile, content, "utf8");
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
if (process.env.DINOU_DEBUG) {
|
|
224
|
+
console.warn("[Dinou] Could not write .dinou/types/routes.d.ts:", e.message);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Ensure dinou-env.d.ts references .dinou/types/routes.d.ts
|
|
229
|
+
const envFile = path.resolve(projectRoot, "dinou-env.d.ts");
|
|
230
|
+
try {
|
|
231
|
+
if (fs.existsSync(envFile)) {
|
|
232
|
+
let envContent = fs.readFileSync(envFile, "utf8");
|
|
233
|
+
const refLine = '/// <reference path="./.dinou/types/routes.d.ts" />';
|
|
234
|
+
if (!envContent.includes(refLine)) {
|
|
235
|
+
envContent = envContent.trimEnd() + "\n" + refLine + "\n";
|
|
236
|
+
fs.writeFileSync(envFile, envContent, "utf8");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch (e) {}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
staticCount: staticRoutes.length,
|
|
243
|
+
dynamicCount: dynamicRoutes.length,
|
|
244
|
+
totalRoutes: routes.length,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = {
|
|
249
|
+
collectRoutes,
|
|
250
|
+
generateRouteDefinitions,
|
|
251
|
+
generateRouteTypes,
|
|
252
|
+
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
// dinou/core/generate-static-page.js
|
|
2
|
+
// Generates static HTML for a given path using the in-memory Dual-Bundle streaming engine.
|
|
3
|
+
// Completely eliminates child_process.fork() and render-app-to-html.js.
|
|
4
|
+
|
|
1
5
|
const path = require("path");
|
|
2
|
-
const { mkdirSync, createWriteStream, existsSync } = require("fs");
|
|
3
6
|
const fs = require("fs").promises;
|
|
4
|
-
const
|
|
5
|
-
const {
|
|
6
|
-
const { processMetadata } = require("./get-ssg-metadata.js");
|
|
7
|
+
const { existsSync, mkdirSync } = require("fs");
|
|
8
|
+
const { pathToFileURL } = require("url");
|
|
7
9
|
|
|
8
10
|
const OUT_DIR = path.resolve(".dinou/dist2");
|
|
9
11
|
|
|
@@ -12,107 +14,66 @@ async function generateStaticPage(reqPath) {
|
|
|
12
14
|
const htmlPath = path.join(OUT_DIR, finalReqPath, "index.html");
|
|
13
15
|
const tempHtmlPath = path.join(OUT_DIR, finalReqPath, `index.html.${Date.now()}-${Math.random()}.tmp`);
|
|
14
16
|
|
|
15
|
-
const query = {};
|
|
16
|
-
const paramsString = JSON.stringify(query);
|
|
17
|
-
const capturedStatus = {};
|
|
18
|
-
|
|
19
|
-
const contextForChild = {
|
|
20
|
-
req: {
|
|
21
|
-
query,
|
|
22
|
-
cookies: {},
|
|
23
|
-
headers: {
|
|
24
|
-
"user-agent": "Dinou-ISR-Revalidator",
|
|
25
|
-
host: "localhost",
|
|
26
|
-
"x-forwarded-proto": "http",
|
|
27
|
-
},
|
|
28
|
-
path: finalReqPath,
|
|
29
|
-
method: "GET",
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
17
|
try {
|
|
34
18
|
mkdirSync(path.dirname(htmlPath), { recursive: true });
|
|
35
|
-
const fileStream = createWriteStream(tempHtmlPath);
|
|
36
|
-
let htmlStream = null;
|
|
37
19
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
_cookies: [],
|
|
41
|
-
cookie(name, value, options) {
|
|
42
|
-
this._cookies.push({ name, value, options });
|
|
43
|
-
},
|
|
44
|
-
write: (chunk) => {
|
|
45
|
-
if (!fileStream.writableEnded) fileStream.write(chunk);
|
|
46
|
-
},
|
|
47
|
-
end: (chunk) => {
|
|
48
|
-
if (chunk && !fileStream.writableEnded) fileStream.write(chunk);
|
|
49
|
-
if (htmlStream) htmlStream.unpipe(fileStream);
|
|
50
|
-
if (!fileStream.writableEnded) fileStream.end();
|
|
51
|
-
},
|
|
52
|
-
status: (code) => {
|
|
53
|
-
capturedStatus.value = code;
|
|
54
|
-
},
|
|
55
|
-
setHeader: () => { },
|
|
56
|
-
clearCookie: () => { },
|
|
57
|
-
redirect: () => { },
|
|
58
|
-
};
|
|
20
|
+
const rscEnginePath = path.resolve(process.cwd(), ".dinou/node/rsc-engine.mjs");
|
|
21
|
+
const ssrEnginePath = path.resolve(process.cwd(), ".dinou/node/ssr-engine.mjs");
|
|
59
22
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
capturedStatus
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
const metadata = getStaticMetadata(finalReqPath);
|
|
69
|
-
let sideEffectScripts = "";
|
|
70
|
-
if (metadata && metadata.effects) {
|
|
71
|
-
sideEffectScripts = processMetadata(metadata.effects);
|
|
23
|
+
let rscModule = null;
|
|
24
|
+
let ssrModule = null;
|
|
25
|
+
if (existsSync(rscEnginePath) && existsSync(ssrEnginePath)) {
|
|
26
|
+
rscModule = await import(pathToFileURL(rscEnginePath).href);
|
|
27
|
+
ssrModule = await import(pathToFileURL(ssrEnginePath).href);
|
|
72
28
|
}
|
|
73
29
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
});
|
|
81
|
-
htmlStream.on("error", (err) => {
|
|
82
|
-
fileStream.end();
|
|
83
|
-
if (err.code === "ERR_STREAM_WRITE_AFTER_END") resolve();
|
|
84
|
-
else reject(err);
|
|
30
|
+
if (rscModule && ssrModule) {
|
|
31
|
+
const webReq = new Request(`http://localhost${finalReqPath}`);
|
|
32
|
+
const res = await rscModule.handleRequest(webReq, {
|
|
33
|
+
runtime: "node-bundle",
|
|
34
|
+
renderHtmlStream: ssrModule.renderHtml,
|
|
35
|
+
isSSG: true,
|
|
85
36
|
});
|
|
86
|
-
fileStream.on("error", reject);
|
|
87
|
-
});
|
|
88
37
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
await fs.writeFile(tempHtmlPath, text, "utf8");
|
|
40
|
+
|
|
41
|
+
const routeMeta = typeof rscModule.getStaticMetadata === "function" ? rscModule.getStaticMetadata(finalReqPath) : null;
|
|
42
|
+
let isRoutePpr = Boolean(routeMeta?.ppr);
|
|
43
|
+
if (!isRoutePpr) {
|
|
44
|
+
try {
|
|
45
|
+
const metaDisk = JSON.parse(await fs.readFile(path.join(OUT_DIR, finalReqPath, "metadata.json"), "utf8"));
|
|
46
|
+
if (metaDisk?.ppr) isRoutePpr = true;
|
|
47
|
+
} catch (e) {}
|
|
48
|
+
}
|
|
49
|
+
if (isRoutePpr) {
|
|
50
|
+
const shellHtmlPath = path.join(OUT_DIR, finalReqPath, "shell.html");
|
|
51
|
+
await fs.writeFile(shellHtmlPath, text, "utf8").catch(() => {});
|
|
52
|
+
try {
|
|
53
|
+
const pageReq = new Request(`http://localhost/____rsc_page____${finalReqPath}`);
|
|
54
|
+
const pageRes = await rscModule.handleRequest(pageReq, { runtime: "node-bundle" });
|
|
55
|
+
if (pageRes.status === 200) {
|
|
56
|
+
const pageRscText = await pageRes.text();
|
|
57
|
+
const shellRscPath = path.join(OUT_DIR, finalReqPath, "shell.rsc");
|
|
58
|
+
await fs.writeFile(shellRscPath, pageRscText, "utf8").catch(() => {});
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {}
|
|
61
|
+
}
|
|
102
62
|
|
|
103
|
-
|
|
104
|
-
|
|
63
|
+
return {
|
|
64
|
+
success: res.status !== 500 && text.length > 0,
|
|
65
|
+
type: "html",
|
|
66
|
+
reqPath: finalReqPath,
|
|
67
|
+
tempPath: tempHtmlPath,
|
|
68
|
+
finalPath: htmlPath,
|
|
69
|
+
status: res.status || 200,
|
|
70
|
+
ppr: isRoutePpr,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
105
73
|
|
|
106
|
-
return {
|
|
107
|
-
success,
|
|
108
|
-
type: "html",
|
|
109
|
-
reqPath: finalReqPath,
|
|
110
|
-
tempPath: tempHtmlPath,
|
|
111
|
-
finalPath: htmlPath,
|
|
112
|
-
status: status,
|
|
113
|
-
};
|
|
74
|
+
return { success: false, tempPath: tempHtmlPath };
|
|
114
75
|
} catch (error) {
|
|
115
|
-
await fs.unlink(tempHtmlPath).catch(() => {
|
|
76
|
+
await fs.unlink(tempHtmlPath).catch(() => {});
|
|
116
77
|
return { success: false, tempPath: tempHtmlPath };
|
|
117
78
|
}
|
|
118
79
|
}
|