nukejs 0.0.5 → 0.0.7
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/README.md +96 -8
- package/dist/Link.js +16 -0
- package/dist/Link.js.map +7 -0
- package/dist/build-common.d.ts +57 -80
- package/dist/build-common.js +156 -168
- package/dist/build-common.js.map +2 -2
- package/dist/build-node.d.ts +14 -0
- package/dist/build-node.js +50 -51
- package/dist/build-node.js.map +2 -2
- package/dist/build-vercel.d.ts +18 -0
- package/dist/build-vercel.js +76 -63
- package/dist/build-vercel.js.map +2 -2
- package/dist/builder.d.ts +10 -0
- package/dist/builder.js +31 -62
- package/dist/builder.js.map +3 -3
- package/dist/bundle.js +69 -6
- package/dist/bundle.js.map +2 -2
- package/dist/component-analyzer.d.ts +13 -10
- package/dist/component-analyzer.js +26 -17
- package/dist/component-analyzer.js.map +2 -2
- package/dist/hmr-bundle.js +17 -4
- package/dist/hmr-bundle.js.map +2 -2
- package/dist/html-store.d.ts +7 -0
- package/dist/html-store.js.map +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/renderer.js +2 -7
- package/dist/renderer.js.map +2 -2
- package/dist/router.js +16 -4
- package/dist/router.js.map +2 -2
- package/dist/ssr.js +21 -4
- package/dist/ssr.js.map +2 -2
- package/dist/use-html.js +5 -1
- package/dist/use-html.js.map +2 -2
- package/dist/use-router.js +28 -0
- package/dist/use-router.js.map +7 -0
- package/package.json +1 -1
- package/dist/as-is/Link.tsx +0 -20
- package/dist/as-is/useRouter.ts +0 -33
- /package/dist/{as-is/Link.d.ts → Link.d.ts} +0 -0
- /package/dist/{as-is/useRouter.d.ts → use-router.d.ts} +0 -0
package/dist/build-common.js
CHANGED
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
5
5
|
import { build } from "esbuild";
|
|
6
6
|
import { findClientComponentsInTree } from "./component-analyzer.js";
|
|
7
|
+
const NODE_BUILTINS = [
|
|
8
|
+
"node:*",
|
|
9
|
+
"http",
|
|
10
|
+
"https",
|
|
11
|
+
"fs",
|
|
12
|
+
"path",
|
|
13
|
+
"url",
|
|
14
|
+
"crypto",
|
|
15
|
+
"stream",
|
|
16
|
+
"buffer",
|
|
17
|
+
"events",
|
|
18
|
+
"util",
|
|
19
|
+
"os",
|
|
20
|
+
"net",
|
|
21
|
+
"tls",
|
|
22
|
+
"child_process",
|
|
23
|
+
"worker_threads",
|
|
24
|
+
"cluster",
|
|
25
|
+
"dgram",
|
|
26
|
+
"dns",
|
|
27
|
+
"readline",
|
|
28
|
+
"zlib",
|
|
29
|
+
"assert",
|
|
30
|
+
"module",
|
|
31
|
+
"perf_hooks",
|
|
32
|
+
"string_decoder",
|
|
33
|
+
"timers",
|
|
34
|
+
"async_hooks",
|
|
35
|
+
"v8",
|
|
36
|
+
"vm"
|
|
37
|
+
];
|
|
7
38
|
function walkFiles(dir, base = dir) {
|
|
8
39
|
if (!fs.existsSync(dir)) return [];
|
|
9
40
|
const results = [];
|
|
@@ -22,12 +53,14 @@ function analyzeFile(relPath, prefix = "api") {
|
|
|
22
53
|
let segments = normalized.split("/");
|
|
23
54
|
if (segments.at(-1) === "index") segments = segments.slice(0, -1);
|
|
24
55
|
const paramNames = [];
|
|
56
|
+
const catchAllNames = [];
|
|
25
57
|
const regexParts = [];
|
|
26
58
|
let specificity = 0;
|
|
27
59
|
for (const seg of segments) {
|
|
28
60
|
const optCatchAll = seg.match(/^\[\[\.\.\.(.+)\]\]$/);
|
|
29
61
|
if (optCatchAll) {
|
|
30
62
|
paramNames.push(optCatchAll[1]);
|
|
63
|
+
catchAllNames.push(optCatchAll[1]);
|
|
31
64
|
regexParts.push("(.*)");
|
|
32
65
|
specificity += 1;
|
|
33
66
|
continue;
|
|
@@ -35,10 +68,18 @@ function analyzeFile(relPath, prefix = "api") {
|
|
|
35
68
|
const catchAll = seg.match(/^\[\.\.\.(.+)\]$/);
|
|
36
69
|
if (catchAll) {
|
|
37
70
|
paramNames.push(catchAll[1]);
|
|
71
|
+
catchAllNames.push(catchAll[1]);
|
|
38
72
|
regexParts.push("(.+)");
|
|
39
73
|
specificity += 10;
|
|
40
74
|
continue;
|
|
41
75
|
}
|
|
76
|
+
const optDynamic = seg.match(/^\[\[([^.][^\]]*)\]\]$/);
|
|
77
|
+
if (optDynamic) {
|
|
78
|
+
paramNames.push(optDynamic[1]);
|
|
79
|
+
regexParts.push("__OPT__([^/]*)");
|
|
80
|
+
specificity += 30;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
42
83
|
const dynamic = seg.match(/^\[(.+)\]$/);
|
|
43
84
|
if (dynamic) {
|
|
44
85
|
paramNames.push(dynamic[1]);
|
|
@@ -49,11 +90,26 @@ function analyzeFile(relPath, prefix = "api") {
|
|
|
49
90
|
regexParts.push(seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
50
91
|
specificity += 1e3;
|
|
51
92
|
}
|
|
52
|
-
|
|
93
|
+
let srcRegex;
|
|
94
|
+
if (segments.length === 0) {
|
|
95
|
+
srcRegex = "^/$";
|
|
96
|
+
} else {
|
|
97
|
+
let body = "";
|
|
98
|
+
for (let i = 0; i < regexParts.length; i++) {
|
|
99
|
+
const part = regexParts[i];
|
|
100
|
+
if (part.startsWith("__OPT__")) {
|
|
101
|
+
const cap = part.slice(7);
|
|
102
|
+
body += i === 0 ? cap : `(?:/${cap})?`;
|
|
103
|
+
} else {
|
|
104
|
+
body += (i === 0 ? "" : "/") + part;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
srcRegex = "^/" + body + "$";
|
|
108
|
+
}
|
|
53
109
|
const funcSegments = normalized.split("/");
|
|
54
110
|
if (funcSegments.at(-1) === "index") funcSegments.pop();
|
|
55
111
|
const funcPath = funcSegments.length === 0 ? `/${prefix}/_index` : `/${prefix}/` + funcSegments.join("/");
|
|
56
|
-
return { srcRegex, paramNames, funcPath, specificity };
|
|
112
|
+
return { srcRegex, paramNames, catchAllNames, funcPath, specificity };
|
|
57
113
|
}
|
|
58
114
|
function isServerComponent(filePath) {
|
|
59
115
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -80,14 +136,18 @@ function findPageLayouts(routeFilePath, pagesDir) {
|
|
|
80
136
|
}
|
|
81
137
|
function extractDefaultExportName(filePath) {
|
|
82
138
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
83
|
-
|
|
84
|
-
|
|
139
|
+
let m = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
|
|
140
|
+
if (m?.[1]) return m[1];
|
|
141
|
+
m = content.match(/var\s+\w+_default\s*=\s*(\w+)/);
|
|
142
|
+
if (m?.[1]) return m[1];
|
|
143
|
+
m = content.match(/export\s*\{[^}]*\b(\w+)\s+as\s+default\b[^}]*\}/);
|
|
144
|
+
if (m?.[1] && !m[1].endsWith("_default")) return m[1];
|
|
145
|
+
return null;
|
|
85
146
|
}
|
|
86
147
|
function collectServerPages(pagesDir) {
|
|
87
148
|
if (!fs.existsSync(pagesDir)) return [];
|
|
88
149
|
return walkFiles(pagesDir).filter((relPath) => {
|
|
89
|
-
|
|
90
|
-
if (base === "layout") return false;
|
|
150
|
+
if (path.basename(relPath, path.extname(relPath)) === "layout") return false;
|
|
91
151
|
return isServerComponent(path.join(pagesDir, relPath));
|
|
92
152
|
}).map((relPath) => ({
|
|
93
153
|
...analyzeFile(relPath, "page"),
|
|
@@ -97,27 +157,21 @@ function collectServerPages(pagesDir) {
|
|
|
97
157
|
function collectGlobalClientRegistry(serverPages, pagesDir) {
|
|
98
158
|
const registry = /* @__PURE__ */ new Map();
|
|
99
159
|
for (const { absPath } of serverPages) {
|
|
100
|
-
for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
|
|
160
|
+
for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
|
|
101
161
|
registry.set(id, p);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir)) {
|
|
162
|
+
for (const layoutPath of findPageLayouts(absPath, pagesDir))
|
|
163
|
+
for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))
|
|
105
164
|
registry.set(id, p);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
165
|
}
|
|
109
166
|
return registry;
|
|
110
167
|
}
|
|
111
168
|
function buildPerPageRegistry(absPath, layoutPaths, pagesDir) {
|
|
112
169
|
const registry = /* @__PURE__ */ new Map();
|
|
113
|
-
for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
|
|
170
|
+
for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
|
|
114
171
|
registry.set(id, p);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (const [id, p] of findClientComponentsInTree(lp, pagesDir)) {
|
|
172
|
+
for (const lp of layoutPaths)
|
|
173
|
+
for (const [id, p] of findClientComponentsInTree(lp, pagesDir))
|
|
118
174
|
registry.set(id, p);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
175
|
const clientComponentNames = {};
|
|
122
176
|
for (const [id, filePath] of registry) {
|
|
123
177
|
const name = extractDefaultExportName(filePath);
|
|
@@ -131,30 +185,29 @@ async function buildPages(pagesDir, staticDir) {
|
|
|
131
185
|
console.warn(`\u26A0 Pages found in ${pagesDir} but none are server components`);
|
|
132
186
|
}
|
|
133
187
|
if (serverPages.length === 0) return [];
|
|
134
|
-
const
|
|
135
|
-
const prerenderedHtml = await bundleClientComponents(
|
|
136
|
-
const
|
|
188
|
+
const globalRegistry = collectGlobalClientRegistry(serverPages, pagesDir);
|
|
189
|
+
const prerenderedHtml = await bundleClientComponents(globalRegistry, pagesDir, staticDir);
|
|
190
|
+
const prerenderedRecord = Object.fromEntries(prerenderedHtml);
|
|
137
191
|
const builtPages = [];
|
|
138
192
|
for (const page of serverPages) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const { registry, clientComponentNames } = buildPerPageRegistry(absPath, layoutPaths, pagesDir);
|
|
193
|
+
console.log(` building ${page.absPath} \u2192 ${page.funcPath} [page]`);
|
|
194
|
+
const layoutPaths = findPageLayouts(page.absPath, pagesDir);
|
|
195
|
+
const { registry, clientComponentNames } = buildPerPageRegistry(page.absPath, layoutPaths, pagesDir);
|
|
143
196
|
const bundleText = await bundlePageHandler({
|
|
144
|
-
absPath,
|
|
197
|
+
absPath: page.absPath,
|
|
145
198
|
pagesDir,
|
|
146
199
|
clientComponentNames,
|
|
147
200
|
allClientIds: [...registry.keys()],
|
|
148
201
|
layoutPaths,
|
|
149
|
-
prerenderedHtml:
|
|
202
|
+
prerenderedHtml: prerenderedRecord,
|
|
203
|
+
catchAllNames: page.catchAllNames
|
|
150
204
|
});
|
|
151
205
|
builtPages.push({ ...page, bundleText });
|
|
152
206
|
}
|
|
153
207
|
return builtPages;
|
|
154
208
|
}
|
|
155
209
|
function makeApiAdapterSource(handlerFilename) {
|
|
156
|
-
return `
|
|
157
|
-
import type { IncomingMessage, ServerResponse } from 'http';
|
|
210
|
+
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
158
211
|
import * as mod from ${JSON.stringify("./" + handlerFilename)};
|
|
159
212
|
|
|
160
213
|
function enhance(res: ServerResponse) {
|
|
@@ -197,7 +250,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
197
250
|
}
|
|
198
251
|
await fn(apiReq, apiRes);
|
|
199
252
|
}
|
|
200
|
-
|
|
253
|
+
`;
|
|
201
254
|
}
|
|
202
255
|
function makePageAdapterSource(opts) {
|
|
203
256
|
const {
|
|
@@ -206,32 +259,21 @@ function makePageAdapterSource(opts) {
|
|
|
206
259
|
clientComponentNames,
|
|
207
260
|
allClientIds,
|
|
208
261
|
layoutArrayItems,
|
|
209
|
-
prerenderedHtml
|
|
262
|
+
prerenderedHtml,
|
|
263
|
+
catchAllNames
|
|
210
264
|
} = opts;
|
|
211
|
-
return `
|
|
212
|
-
import
|
|
265
|
+
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
266
|
+
import { createElement as __createElement__ } from 'react';
|
|
267
|
+
import { renderToString as __renderToString__ } from 'react-dom/server';
|
|
213
268
|
import * as __page__ from ${pageImport};
|
|
214
269
|
${layoutImports}
|
|
215
270
|
|
|
216
|
-
// \u2500\u2500\u2500 Pre-built client component registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
217
|
-
// Computed at BUILD TIME from the import tree. Source files are not deployed,
|
|
218
|
-
// so we must not read them with fs.readFileSync at runtime.
|
|
219
|
-
// Key: default-export function name \u2192 Value: stable content-hash ID
|
|
220
271
|
const CLIENT_COMPONENTS: Record<string, string> = ${JSON.stringify(clientComponentNames)};
|
|
221
|
-
|
|
222
|
-
// All client component IDs reachable from this page (page + layouts).
|
|
223
|
-
// Sent to initRuntime so the browser pre-loads all bundles for SPA navigation.
|
|
224
272
|
const ALL_CLIENT_IDS: string[] = ${JSON.stringify(allClientIds)};
|
|
225
|
-
|
|
226
|
-
// Pre-rendered HTML for each client component, produced at BUILD TIME by
|
|
227
|
-
// renderToString with default props. Used directly in the span wrapper so
|
|
228
|
-
// the server response contains real markup and React hydration never sees a
|
|
229
|
-
// mismatch. No react-dom/server is needed at runtime.
|
|
230
273
|
const PRERENDERED_HTML: Record<string, string> = ${JSON.stringify(prerenderedHtml)};
|
|
274
|
+
const CATCH_ALL_NAMES = new Set(${JSON.stringify(catchAllNames)});
|
|
231
275
|
|
|
232
|
-
// \u2500\u2500\u2500 html-store (inlined
|
|
233
|
-
// Uses the same globalThis Symbol key as html-store.ts so any useHtml() call
|
|
234
|
-
// (however imported) writes into the same store that runWithHtmlStore reads.
|
|
276
|
+
// \u2500\u2500\u2500 html-store (inlined) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
235
277
|
type TitleValue = string | ((prev: string) => string);
|
|
236
278
|
interface HtmlStore {
|
|
237
279
|
titleOps: TitleValue[];
|
|
@@ -243,11 +285,10 @@ interface HtmlStore {
|
|
|
243
285
|
style: { content?: string; media?: string }[];
|
|
244
286
|
}
|
|
245
287
|
const __STORE_KEY__ = Symbol.for('__nukejs_html_store__');
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
288
|
+
const __getStore = (): HtmlStore | null => (globalThis as any)[__STORE_KEY__] ?? null;
|
|
289
|
+
const __setStore = (s: HtmlStore | null): void => { (globalThis as any)[__STORE_KEY__] = s; };
|
|
290
|
+
const __emptyStore = (): HtmlStore =>
|
|
291
|
+
({ titleOps: [], htmlAttrs: {}, bodyAttrs: {}, meta: [], link: [], script: [], style: [] });
|
|
251
292
|
async function runWithHtmlStore(fn: () => Promise<void>): Promise<HtmlStore> {
|
|
252
293
|
__setStore(__emptyStore());
|
|
253
294
|
try { await fn(); return { ...(__getStore() ?? __emptyStore()) }; }
|
|
@@ -280,59 +321,44 @@ function openTag(tag: string, attrs: Record<string, string | undefined>): string
|
|
|
280
321
|
const s = renderAttrs(attrs as Record<string, string | boolean | undefined>);
|
|
281
322
|
return s ? \`<\${tag} \${s}>\` : \`<\${tag}>\`;
|
|
282
323
|
}
|
|
283
|
-
function metaKey(k: string): string { return k === 'httpEquiv' ? 'http-equiv' : k; }
|
|
284
|
-
function linkKey(k: string): string {
|
|
285
|
-
if (k === 'hrefLang') return 'hreflang';
|
|
286
|
-
if (k === 'crossOrigin') return 'crossorigin';
|
|
287
|
-
return k;
|
|
288
|
-
}
|
|
289
324
|
function renderMetaTag(tag: Record<string, string | undefined>): string {
|
|
325
|
+
const key = (k: string) => k === 'httpEquiv' ? 'http-equiv' : k;
|
|
290
326
|
const attrs: Record<string, string | undefined> = {};
|
|
291
|
-
for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[
|
|
327
|
+
for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;
|
|
292
328
|
return \` <meta \${renderAttrs(attrs as any)} />\`;
|
|
293
329
|
}
|
|
294
330
|
function renderLinkTag(tag: Record<string, string | undefined>): string {
|
|
331
|
+
const key = (k: string) => k === 'hrefLang' ? 'hreflang' : k === 'crossOrigin' ? 'crossorigin' : k;
|
|
295
332
|
const attrs: Record<string, string | undefined> = {};
|
|
296
|
-
for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[
|
|
333
|
+
for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;
|
|
297
334
|
return \` <link \${renderAttrs(attrs as any)} />\`;
|
|
298
335
|
}
|
|
299
336
|
function renderScriptTag(tag: any): string {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
};
|
|
304
|
-
const s = renderAttrs(attrs);
|
|
305
|
-
const open = s ? \`<script \${s}>\` : '<script>';
|
|
306
|
-
return \` \${open}\${tag.src ? '' : (tag.content ?? '')}</script>\`;
|
|
337
|
+
const s = renderAttrs({ src: tag.src, type: tag.type, crossorigin: tag.crossOrigin,
|
|
338
|
+
integrity: tag.integrity, defer: tag.defer, async: tag.async, nomodule: tag.noModule });
|
|
339
|
+
return \` \${s ? \`<script \${s}>\` : '<script>'}\${tag.src ? '' : (tag.content ?? '')}</script>\`;
|
|
307
340
|
}
|
|
308
341
|
function renderStyleTag(tag: any): string {
|
|
309
342
|
const media = tag.media ? \` media="\${escapeAttr(tag.media)}"\` : '';
|
|
310
343
|
return \` <style\${media}>\${tag.content ?? ''}</style>\`;
|
|
311
344
|
}
|
|
312
345
|
|
|
313
|
-
// \u2500\u2500\u2500
|
|
346
|
+
// \u2500\u2500\u2500 Renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
314
347
|
const VOID_TAGS = new Set([
|
|
315
348
|
'area','base','br','col','embed','hr','img','input',
|
|
316
349
|
'link','meta','param','source','track','wbr',
|
|
317
350
|
]);
|
|
318
351
|
|
|
319
|
-
// \u2500\u2500\u2500 Prop serialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
320
|
-
// Converts React element trees in props to a JSON-safe format that
|
|
321
|
-
// bundle.ts / initRuntime can reconstruct on the client.
|
|
322
352
|
function serializeProps(value: any): any {
|
|
323
353
|
if (value == null || typeof value !== 'object') return value;
|
|
324
354
|
if (typeof value === 'function') return undefined;
|
|
325
|
-
if (Array.isArray(value))
|
|
326
|
-
return value.map(serializeProps).filter((v: any) => v !== undefined);
|
|
327
|
-
}
|
|
355
|
+
if (Array.isArray(value)) return value.map(serializeProps).filter((v: any) => v !== undefined);
|
|
328
356
|
if ((value as any).$$typeof) {
|
|
329
|
-
const { type, props:
|
|
330
|
-
if (typeof type === 'string') {
|
|
331
|
-
return { __re: 'html', tag: type, props: serializeProps(elProps) };
|
|
332
|
-
}
|
|
357
|
+
const { type, props: p } = value as any;
|
|
358
|
+
if (typeof type === 'string') return { __re: 'html', tag: type, props: serializeProps(p) };
|
|
333
359
|
if (typeof type === 'function') {
|
|
334
360
|
const cid = CLIENT_COMPONENTS[type.name];
|
|
335
|
-
if (cid) return { __re: 'client', componentId: cid, props: serializeProps(
|
|
361
|
+
if (cid) return { __re: 'client', componentId: cid, props: serializeProps(p) };
|
|
336
362
|
}
|
|
337
363
|
return undefined;
|
|
338
364
|
}
|
|
@@ -344,24 +370,16 @@ function serializeProps(value: any): any {
|
|
|
344
370
|
return out;
|
|
345
371
|
}
|
|
346
372
|
|
|
347
|
-
// \u2500\u2500\u2500 Async recursive renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
348
|
-
// Handles: null/undefined/boolean \u2192 '', strings/numbers \u2192 escaped text, arrays,
|
|
349
|
-
// Fragment, void/non-void HTML elements, class components, sync + async functions.
|
|
350
|
-
// Client components \u2192 <span data-hydrate-id="\u2026"> markers for browser hydration.
|
|
351
373
|
async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
|
|
352
374
|
if (node == null || typeof node === 'boolean') return '';
|
|
353
375
|
if (typeof node === 'string') return escapeHtml(node);
|
|
354
376
|
if (typeof node === 'number') return String(node);
|
|
355
|
-
if (Array.isArray(node))
|
|
356
|
-
return (await Promise.all(node.map(n => renderNode(n, hydrated)))).join('');
|
|
357
|
-
}
|
|
377
|
+
if (Array.isArray(node)) return (await Promise.all(node.map(n => renderNode(n, hydrated)))).join('');
|
|
358
378
|
|
|
359
379
|
const { type, props } = node as { type: any; props: Record<string, any> };
|
|
360
380
|
if (!type) return '';
|
|
361
381
|
|
|
362
|
-
if (type === Symbol.for('react.fragment'))
|
|
363
|
-
return renderNode(props?.children ?? null, hydrated);
|
|
364
|
-
}
|
|
382
|
+
if (type === Symbol.for('react.fragment')) return renderNode(props?.children ?? null, hydrated);
|
|
365
383
|
|
|
366
384
|
if (typeof type === 'string') {
|
|
367
385
|
const { children, dangerouslySetInnerHTML, ...rest } = props || {};
|
|
@@ -392,21 +410,16 @@ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
|
|
|
392
410
|
if (clientId) {
|
|
393
411
|
hydrated.add(clientId);
|
|
394
412
|
const serializedProps = serializeProps(props ?? {});
|
|
395
|
-
// Render with actual props so children/content appear in the SSR HTML.
|
|
396
|
-
// Fall back to the build-time pre-rendered HTML if the component throws
|
|
397
|
-
// (e.g. it references browser-only APIs during render).
|
|
398
413
|
let ssrHtml: string;
|
|
399
414
|
try {
|
|
400
|
-
|
|
401
|
-
ssrHtml = await renderNode(result, new Set());
|
|
415
|
+
ssrHtml = __renderToString__(__createElement__(type as any, props || {}));
|
|
402
416
|
} catch {
|
|
403
417
|
ssrHtml = PRERENDERED_HTML[clientId] ?? '';
|
|
404
418
|
}
|
|
405
419
|
return \`<span data-hydrate-id="\${clientId}" data-hydrate-props="\${escapeHtml(JSON.stringify(serializedProps))}">\${ssrHtml}</span>\`;
|
|
406
420
|
}
|
|
407
421
|
const instance = type.prototype?.isReactComponent ? new (type as any)(props) : null;
|
|
408
|
-
|
|
409
|
-
return renderNode(result, hydrated);
|
|
422
|
+
return renderNode(instance ? instance.render() : await (type as Function)(props || {}), hydrated);
|
|
410
423
|
}
|
|
411
424
|
|
|
412
425
|
return '';
|
|
@@ -417,9 +430,8 @@ const LAYOUT_COMPONENTS: Array<(props: any) => any> = [${layoutArrayItems}];
|
|
|
417
430
|
|
|
418
431
|
function wrapWithLayouts(element: any): any {
|
|
419
432
|
let el = element;
|
|
420
|
-
for (let i = LAYOUT_COMPONENTS.length - 1; i >= 0; i--)
|
|
433
|
+
for (let i = LAYOUT_COMPONENTS.length - 1; i >= 0; i--)
|
|
421
434
|
el = { type: LAYOUT_COMPONENTS[i], props: { children: el }, key: null, ref: null };
|
|
422
|
-
}
|
|
423
435
|
return el;
|
|
424
436
|
}
|
|
425
437
|
|
|
@@ -427,40 +439,43 @@ function wrapWithLayouts(element: any): any {
|
|
|
427
439
|
export default async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
428
440
|
try {
|
|
429
441
|
const parsed = new URL(req.url || '/', 'http://localhost');
|
|
430
|
-
const params: Record<string, string> = {};
|
|
431
|
-
parsed.searchParams.forEach((
|
|
442
|
+
const params: Record<string, string | string[]> = {};
|
|
443
|
+
parsed.searchParams.forEach((_, k) => {
|
|
444
|
+
params[k] = CATCH_ALL_NAMES.has(k)
|
|
445
|
+
? parsed.searchParams.getAll(k)
|
|
446
|
+
: parsed.searchParams.get(k) as string;
|
|
447
|
+
});
|
|
432
448
|
const url = req.url || '/';
|
|
433
449
|
|
|
434
450
|
const hydrated = new Set<string>();
|
|
435
|
-
const
|
|
436
|
-
const wrapped = wrapWithLayouts(pageElement);
|
|
451
|
+
const wrapped = wrapWithLayouts({ type: __page__.default, props: params as any, key: null, ref: null });
|
|
437
452
|
|
|
438
453
|
let appHtml = '';
|
|
439
|
-
const store = await runWithHtmlStore(async () => {
|
|
440
|
-
appHtml = await renderNode(wrapped, hydrated);
|
|
441
|
-
});
|
|
454
|
+
const store = await runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); });
|
|
442
455
|
|
|
443
|
-
const pageTitle = resolveTitle(store.titleOps, '
|
|
444
|
-
const
|
|
456
|
+
const pageTitle = resolveTitle(store.titleOps, 'NukeJS');
|
|
457
|
+
const headScripts = store.script.filter((s: any) => (s.position ?? 'head') === 'head');
|
|
458
|
+
const bodyScripts = store.script.filter((s: any) => s.position === 'body');
|
|
459
|
+
const headLines = [
|
|
445
460
|
' <meta charset="utf-8" />',
|
|
446
461
|
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
447
462
|
\` <title>\${escapeHtml(pageTitle)}</title>\`,
|
|
448
|
-
...(store.meta.length || store.link.length || store.style.length ||
|
|
463
|
+
...(store.meta.length || store.link.length || store.style.length || headScripts.length ? [
|
|
449
464
|
' <!--n-head-->',
|
|
450
465
|
...store.meta.map(renderMetaTag),
|
|
451
466
|
...store.link.map(renderLinkTag),
|
|
452
467
|
...store.style.map(renderStyleTag),
|
|
453
|
-
...
|
|
468
|
+
...headScripts.map(renderScriptTag),
|
|
454
469
|
' <!--/n-head-->',
|
|
455
470
|
] : []),
|
|
456
471
|
];
|
|
472
|
+
const bodyScriptLines = bodyScripts.length
|
|
473
|
+
? [' <!--n-body-scripts-->', ...bodyScripts.map(renderScriptTag), ' <!--/n-body-scripts-->']
|
|
474
|
+
: [];
|
|
475
|
+
const bodyScriptsHtml = bodyScriptLines.length ? '\\n' + bodyScriptLines.join('\\n') + '\\n' : '';
|
|
457
476
|
|
|
458
477
|
const runtimeData = JSON.stringify({
|
|
459
|
-
hydrateIds: [...hydrated],
|
|
460
|
-
allIds: ALL_CLIENT_IDS,
|
|
461
|
-
url,
|
|
462
|
-
params,
|
|
463
|
-
debug: 'silent',
|
|
478
|
+
hydrateIds: [...hydrated], allIds: ALL_CLIENT_IDS, url, params, debug: 'silent',
|
|
464
479
|
}).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
|
|
465
480
|
|
|
466
481
|
const html = \`<!DOCTYPE html>
|
|
@@ -489,7 +504,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
489
504
|
const data = JSON.parse(document.getElementById('__n_data').textContent);
|
|
490
505
|
await initRuntime(data);
|
|
491
506
|
</script>
|
|
492
|
-
</body>
|
|
507
|
+
\${bodyScriptsHtml}</body>
|
|
493
508
|
</html>\`;
|
|
494
509
|
|
|
495
510
|
res.statusCode = 200;
|
|
@@ -502,10 +517,10 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
502
517
|
res.end('Internal Server Error');
|
|
503
518
|
}
|
|
504
519
|
}
|
|
505
|
-
|
|
520
|
+
`;
|
|
506
521
|
}
|
|
507
522
|
async function bundleApiHandler(absPath) {
|
|
508
|
-
const adapterName = `_api_adapter_${
|
|
523
|
+
const adapterName = `_api_adapter_${randomBytes(4).toString("hex")}.ts`;
|
|
509
524
|
const adapterPath = path.join(path.dirname(absPath), adapterName);
|
|
510
525
|
fs.writeFileSync(adapterPath, makeApiAdapterSource(path.basename(absPath)));
|
|
511
526
|
let text;
|
|
@@ -526,23 +541,28 @@ async function bundleApiHandler(absPath) {
|
|
|
526
541
|
return text;
|
|
527
542
|
}
|
|
528
543
|
async function bundlePageHandler(opts) {
|
|
529
|
-
const {
|
|
530
|
-
|
|
544
|
+
const {
|
|
545
|
+
absPath,
|
|
546
|
+
clientComponentNames,
|
|
547
|
+
allClientIds,
|
|
548
|
+
layoutPaths,
|
|
549
|
+
prerenderedHtml,
|
|
550
|
+
catchAllNames
|
|
551
|
+
} = opts;
|
|
531
552
|
const adapterDir = path.dirname(absPath);
|
|
532
|
-
const adapterPath = path.join(adapterDir,
|
|
553
|
+
const adapterPath = path.join(adapterDir, `_page_adapter_${randomBytes(4).toString("hex")}.ts`);
|
|
533
554
|
const layoutImports = layoutPaths.map((lp, i) => {
|
|
534
555
|
const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
|
|
535
|
-
|
|
536
|
-
return `import __layout_${i}__ from ${JSON.stringify(importPath)};`;
|
|
556
|
+
return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
|
|
537
557
|
}).join("\n");
|
|
538
|
-
const layoutArrayItems = layoutPaths.map((_, i) => `__layout_${i}__`).join(", ");
|
|
539
558
|
fs.writeFileSync(adapterPath, makePageAdapterSource({
|
|
540
559
|
pageImport: JSON.stringify("./" + path.basename(absPath)),
|
|
541
560
|
layoutImports,
|
|
542
561
|
clientComponentNames,
|
|
543
562
|
allClientIds,
|
|
544
|
-
layoutArrayItems,
|
|
545
|
-
prerenderedHtml
|
|
563
|
+
layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
|
|
564
|
+
prerenderedHtml,
|
|
565
|
+
catchAllNames
|
|
546
566
|
}));
|
|
547
567
|
let text;
|
|
548
568
|
try {
|
|
@@ -553,38 +573,8 @@ async function bundlePageHandler(opts) {
|
|
|
553
573
|
platform: "node",
|
|
554
574
|
target: "node20",
|
|
555
575
|
jsx: "automatic",
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
"node:*",
|
|
559
|
-
"http",
|
|
560
|
-
"https",
|
|
561
|
-
"fs",
|
|
562
|
-
"path",
|
|
563
|
-
"url",
|
|
564
|
-
"crypto",
|
|
565
|
-
"stream",
|
|
566
|
-
"buffer",
|
|
567
|
-
"events",
|
|
568
|
-
"util",
|
|
569
|
-
"os",
|
|
570
|
-
"net",
|
|
571
|
-
"tls",
|
|
572
|
-
"child_process",
|
|
573
|
-
"worker_threads",
|
|
574
|
-
"cluster",
|
|
575
|
-
"dgram",
|
|
576
|
-
"dns",
|
|
577
|
-
"readline",
|
|
578
|
-
"zlib",
|
|
579
|
-
"assert",
|
|
580
|
-
"module",
|
|
581
|
-
"perf_hooks",
|
|
582
|
-
"string_decoder",
|
|
583
|
-
"timers",
|
|
584
|
-
"async_hooks",
|
|
585
|
-
"v8",
|
|
586
|
-
"vm"
|
|
587
|
-
],
|
|
576
|
+
packages: "external",
|
|
577
|
+
external: NODE_BUILTINS,
|
|
588
578
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
589
579
|
write: false
|
|
590
580
|
});
|
|
@@ -615,7 +605,7 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
|
|
|
615
605
|
fs.writeFileSync(path.join(outDir, `${id}.js`), browserResult.outputFiles[0].text);
|
|
616
606
|
const ssrTmp = path.join(
|
|
617
607
|
path.dirname(filePath),
|
|
618
|
-
`_ssr_${id}_${
|
|
608
|
+
`_ssr_${id}_${randomBytes(4).toString("hex")}.mjs`
|
|
619
609
|
);
|
|
620
610
|
try {
|
|
621
611
|
const ssrResult = await build({
|
|
@@ -635,7 +625,7 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
|
|
|
635
625
|
const { renderToString } = await import("react-dom/server");
|
|
636
626
|
prerendered.set(id, renderToString(createElement(Component, {})));
|
|
637
627
|
console.log(` prerendered ${id}`);
|
|
638
|
-
} catch
|
|
628
|
+
} catch {
|
|
639
629
|
prerendered.set(id, "");
|
|
640
630
|
} finally {
|
|
641
631
|
if (fs.existsSync(ssrTmp)) fs.unlinkSync(ssrTmp);
|
|
@@ -660,7 +650,6 @@ import React, {
|
|
|
660
650
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
661
651
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
662
652
|
export { initRuntime, setupLocationChangeMonitor } from "./${bundleFile}.js";
|
|
663
|
-
|
|
664
653
|
export {
|
|
665
654
|
useState, useEffect, useContext, useReducer, useCallback, useMemo,
|
|
666
655
|
useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
|
|
@@ -695,19 +684,18 @@ function copyPublicFiles(publicDir, destDir) {
|
|
|
695
684
|
(function walk(src, dest) {
|
|
696
685
|
fs.mkdirSync(dest, { recursive: true });
|
|
697
686
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
698
|
-
const
|
|
699
|
-
const
|
|
687
|
+
const s = path.join(src, entry.name);
|
|
688
|
+
const d = path.join(dest, entry.name);
|
|
700
689
|
if (entry.isDirectory()) {
|
|
701
|
-
walk(
|
|
690
|
+
walk(s, d);
|
|
702
691
|
} else {
|
|
703
|
-
fs.copyFileSync(
|
|
692
|
+
fs.copyFileSync(s, d);
|
|
704
693
|
count++;
|
|
705
694
|
}
|
|
706
695
|
}
|
|
707
696
|
})(publicDir, destDir);
|
|
708
|
-
if (count > 0)
|
|
697
|
+
if (count > 0)
|
|
709
698
|
console.log(` copied ${count} public file(s) \u2192 ${path.relative(process.cwd(), destDir)}/`);
|
|
710
|
-
}
|
|
711
699
|
}
|
|
712
700
|
export {
|
|
713
701
|
analyzeFile,
|