nukejs 0.0.5 → 0.0.6
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 +11 -5
- package/dist/as-is/Link.js +14 -0
- package/dist/as-is/Link.js.map +7 -0
- package/dist/as-is/useRouter.js +28 -0
- package/dist/as-is/useRouter.js.map +7 -0
- package/dist/build-common.d.ts +51 -80
- package/dist/build-common.js +139 -165
- 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 +16 -0
- package/dist/builder.js +54 -54
- package/dist/builder.js.map +3 -3
- package/dist/bundle.js +9 -2
- package/dist/bundle.js.map +2 -2
- package/dist/component-analyzer.d.ts +7 -10
- package/dist/component-analyzer.js +14 -16
- package/dist/component-analyzer.js.map +2 -2
- package/dist/router.d.ts +19 -20
- package/dist/router.js +12 -8
- package/dist/router.js.map +2 -2
- package/dist/ssr.js +1 -1
- package/dist/ssr.js.map +2 -2
- package/package.json +1 -1
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,12 @@ function findPageLayouts(routeFilePath, pagesDir) {
|
|
|
80
136
|
}
|
|
81
137
|
function extractDefaultExportName(filePath) {
|
|
82
138
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
83
|
-
|
|
84
|
-
return match?.[1] ?? null;
|
|
139
|
+
return content.match(/export\s+default\s+(?:function\s+)?(\w+)/)?.[1] ?? null;
|
|
85
140
|
}
|
|
86
141
|
function collectServerPages(pagesDir) {
|
|
87
142
|
if (!fs.existsSync(pagesDir)) return [];
|
|
88
143
|
return walkFiles(pagesDir).filter((relPath) => {
|
|
89
|
-
|
|
90
|
-
if (base === "layout") return false;
|
|
144
|
+
if (path.basename(relPath, path.extname(relPath)) === "layout") return false;
|
|
91
145
|
return isServerComponent(path.join(pagesDir, relPath));
|
|
92
146
|
}).map((relPath) => ({
|
|
93
147
|
...analyzeFile(relPath, "page"),
|
|
@@ -97,27 +151,21 @@ function collectServerPages(pagesDir) {
|
|
|
97
151
|
function collectGlobalClientRegistry(serverPages, pagesDir) {
|
|
98
152
|
const registry = /* @__PURE__ */ new Map();
|
|
99
153
|
for (const { absPath } of serverPages) {
|
|
100
|
-
for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
|
|
154
|
+
for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
|
|
101
155
|
registry.set(id, p);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir)) {
|
|
156
|
+
for (const layoutPath of findPageLayouts(absPath, pagesDir))
|
|
157
|
+
for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))
|
|
105
158
|
registry.set(id, p);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
159
|
}
|
|
109
160
|
return registry;
|
|
110
161
|
}
|
|
111
162
|
function buildPerPageRegistry(absPath, layoutPaths, pagesDir) {
|
|
112
163
|
const registry = /* @__PURE__ */ new Map();
|
|
113
|
-
for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
|
|
164
|
+
for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
|
|
114
165
|
registry.set(id, p);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (const [id, p] of findClientComponentsInTree(lp, pagesDir)) {
|
|
166
|
+
for (const lp of layoutPaths)
|
|
167
|
+
for (const [id, p] of findClientComponentsInTree(lp, pagesDir))
|
|
118
168
|
registry.set(id, p);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
169
|
const clientComponentNames = {};
|
|
122
170
|
for (const [id, filePath] of registry) {
|
|
123
171
|
const name = extractDefaultExportName(filePath);
|
|
@@ -131,30 +179,29 @@ async function buildPages(pagesDir, staticDir) {
|
|
|
131
179
|
console.warn(`\u26A0 Pages found in ${pagesDir} but none are server components`);
|
|
132
180
|
}
|
|
133
181
|
if (serverPages.length === 0) return [];
|
|
134
|
-
const
|
|
135
|
-
const prerenderedHtml = await bundleClientComponents(
|
|
136
|
-
const
|
|
182
|
+
const globalRegistry = collectGlobalClientRegistry(serverPages, pagesDir);
|
|
183
|
+
const prerenderedHtml = await bundleClientComponents(globalRegistry, pagesDir, staticDir);
|
|
184
|
+
const prerenderedRecord = Object.fromEntries(prerenderedHtml);
|
|
137
185
|
const builtPages = [];
|
|
138
186
|
for (const page of serverPages) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const { registry, clientComponentNames } = buildPerPageRegistry(absPath, layoutPaths, pagesDir);
|
|
187
|
+
console.log(` building ${page.absPath} \u2192 ${page.funcPath} [page]`);
|
|
188
|
+
const layoutPaths = findPageLayouts(page.absPath, pagesDir);
|
|
189
|
+
const { registry, clientComponentNames } = buildPerPageRegistry(page.absPath, layoutPaths, pagesDir);
|
|
143
190
|
const bundleText = await bundlePageHandler({
|
|
144
|
-
absPath,
|
|
191
|
+
absPath: page.absPath,
|
|
145
192
|
pagesDir,
|
|
146
193
|
clientComponentNames,
|
|
147
194
|
allClientIds: [...registry.keys()],
|
|
148
195
|
layoutPaths,
|
|
149
|
-
prerenderedHtml:
|
|
196
|
+
prerenderedHtml: prerenderedRecord,
|
|
197
|
+
catchAllNames: page.catchAllNames
|
|
150
198
|
});
|
|
151
199
|
builtPages.push({ ...page, bundleText });
|
|
152
200
|
}
|
|
153
201
|
return builtPages;
|
|
154
202
|
}
|
|
155
203
|
function makeApiAdapterSource(handlerFilename) {
|
|
156
|
-
return `
|
|
157
|
-
import type { IncomingMessage, ServerResponse } from 'http';
|
|
204
|
+
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
158
205
|
import * as mod from ${JSON.stringify("./" + handlerFilename)};
|
|
159
206
|
|
|
160
207
|
function enhance(res: ServerResponse) {
|
|
@@ -197,7 +244,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
197
244
|
}
|
|
198
245
|
await fn(apiReq, apiRes);
|
|
199
246
|
}
|
|
200
|
-
|
|
247
|
+
`;
|
|
201
248
|
}
|
|
202
249
|
function makePageAdapterSource(opts) {
|
|
203
250
|
const {
|
|
@@ -206,32 +253,19 @@ function makePageAdapterSource(opts) {
|
|
|
206
253
|
clientComponentNames,
|
|
207
254
|
allClientIds,
|
|
208
255
|
layoutArrayItems,
|
|
209
|
-
prerenderedHtml
|
|
256
|
+
prerenderedHtml,
|
|
257
|
+
catchAllNames
|
|
210
258
|
} = opts;
|
|
211
|
-
return `
|
|
212
|
-
import type { IncomingMessage, ServerResponse } from 'http';
|
|
259
|
+
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
213
260
|
import * as __page__ from ${pageImport};
|
|
214
261
|
${layoutImports}
|
|
215
262
|
|
|
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
263
|
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
264
|
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
265
|
const PRERENDERED_HTML: Record<string, string> = ${JSON.stringify(prerenderedHtml)};
|
|
266
|
+
const CATCH_ALL_NAMES = new Set(${JSON.stringify(catchAllNames)});
|
|
231
267
|
|
|
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.
|
|
268
|
+
// \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
269
|
type TitleValue = string | ((prev: string) => string);
|
|
236
270
|
interface HtmlStore {
|
|
237
271
|
titleOps: TitleValue[];
|
|
@@ -243,11 +277,10 @@ interface HtmlStore {
|
|
|
243
277
|
style: { content?: string; media?: string }[];
|
|
244
278
|
}
|
|
245
279
|
const __STORE_KEY__ = Symbol.for('__nukejs_html_store__');
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
280
|
+
const __getStore = (): HtmlStore | null => (globalThis as any)[__STORE_KEY__] ?? null;
|
|
281
|
+
const __setStore = (s: HtmlStore | null): void => { (globalThis as any)[__STORE_KEY__] = s; };
|
|
282
|
+
const __emptyStore = (): HtmlStore =>
|
|
283
|
+
({ titleOps: [], htmlAttrs: {}, bodyAttrs: {}, meta: [], link: [], script: [], style: [] });
|
|
251
284
|
async function runWithHtmlStore(fn: () => Promise<void>): Promise<HtmlStore> {
|
|
252
285
|
__setStore(__emptyStore());
|
|
253
286
|
try { await fn(); return { ...(__getStore() ?? __emptyStore()) }; }
|
|
@@ -280,59 +313,44 @@ function openTag(tag: string, attrs: Record<string, string | undefined>): string
|
|
|
280
313
|
const s = renderAttrs(attrs as Record<string, string | boolean | undefined>);
|
|
281
314
|
return s ? \`<\${tag} \${s}>\` : \`<\${tag}>\`;
|
|
282
315
|
}
|
|
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
316
|
function renderMetaTag(tag: Record<string, string | undefined>): string {
|
|
317
|
+
const key = (k: string) => k === 'httpEquiv' ? 'http-equiv' : k;
|
|
290
318
|
const attrs: Record<string, string | undefined> = {};
|
|
291
|
-
for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[
|
|
319
|
+
for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;
|
|
292
320
|
return \` <meta \${renderAttrs(attrs as any)} />\`;
|
|
293
321
|
}
|
|
294
322
|
function renderLinkTag(tag: Record<string, string | undefined>): string {
|
|
323
|
+
const key = (k: string) => k === 'hrefLang' ? 'hreflang' : k === 'crossOrigin' ? 'crossorigin' : k;
|
|
295
324
|
const attrs: Record<string, string | undefined> = {};
|
|
296
|
-
for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[
|
|
325
|
+
for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;
|
|
297
326
|
return \` <link \${renderAttrs(attrs as any)} />\`;
|
|
298
327
|
}
|
|
299
328
|
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>\`;
|
|
329
|
+
const s = renderAttrs({ src: tag.src, type: tag.type, crossorigin: tag.crossOrigin,
|
|
330
|
+
integrity: tag.integrity, defer: tag.defer, async: tag.async, nomodule: tag.noModule });
|
|
331
|
+
return \` \${s ? \`<script \${s}>\` : '<script>'}\${tag.src ? '' : (tag.content ?? '')}</script>\`;
|
|
307
332
|
}
|
|
308
333
|
function renderStyleTag(tag: any): string {
|
|
309
334
|
const media = tag.media ? \` media="\${escapeAttr(tag.media)}"\` : '';
|
|
310
335
|
return \` <style\${media}>\${tag.content ?? ''}</style>\`;
|
|
311
336
|
}
|
|
312
337
|
|
|
313
|
-
// \u2500\u2500\u2500
|
|
338
|
+
// \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
339
|
const VOID_TAGS = new Set([
|
|
315
340
|
'area','base','br','col','embed','hr','img','input',
|
|
316
341
|
'link','meta','param','source','track','wbr',
|
|
317
342
|
]);
|
|
318
343
|
|
|
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
344
|
function serializeProps(value: any): any {
|
|
323
345
|
if (value == null || typeof value !== 'object') return value;
|
|
324
346
|
if (typeof value === 'function') return undefined;
|
|
325
|
-
if (Array.isArray(value))
|
|
326
|
-
return value.map(serializeProps).filter((v: any) => v !== undefined);
|
|
327
|
-
}
|
|
347
|
+
if (Array.isArray(value)) return value.map(serializeProps).filter((v: any) => v !== undefined);
|
|
328
348
|
if ((value as any).$$typeof) {
|
|
329
|
-
const { type, props:
|
|
330
|
-
if (typeof type === 'string') {
|
|
331
|
-
return { __re: 'html', tag: type, props: serializeProps(elProps) };
|
|
332
|
-
}
|
|
349
|
+
const { type, props: p } = value as any;
|
|
350
|
+
if (typeof type === 'string') return { __re: 'html', tag: type, props: serializeProps(p) };
|
|
333
351
|
if (typeof type === 'function') {
|
|
334
352
|
const cid = CLIENT_COMPONENTS[type.name];
|
|
335
|
-
if (cid) return { __re: 'client', componentId: cid, props: serializeProps(
|
|
353
|
+
if (cid) return { __re: 'client', componentId: cid, props: serializeProps(p) };
|
|
336
354
|
}
|
|
337
355
|
return undefined;
|
|
338
356
|
}
|
|
@@ -344,24 +362,16 @@ function serializeProps(value: any): any {
|
|
|
344
362
|
return out;
|
|
345
363
|
}
|
|
346
364
|
|
|
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
365
|
async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
|
|
352
366
|
if (node == null || typeof node === 'boolean') return '';
|
|
353
367
|
if (typeof node === 'string') return escapeHtml(node);
|
|
354
368
|
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
|
-
}
|
|
369
|
+
if (Array.isArray(node)) return (await Promise.all(node.map(n => renderNode(n, hydrated)))).join('');
|
|
358
370
|
|
|
359
371
|
const { type, props } = node as { type: any; props: Record<string, any> };
|
|
360
372
|
if (!type) return '';
|
|
361
373
|
|
|
362
|
-
if (type === Symbol.for('react.fragment'))
|
|
363
|
-
return renderNode(props?.children ?? null, hydrated);
|
|
364
|
-
}
|
|
374
|
+
if (type === Symbol.for('react.fragment')) return renderNode(props?.children ?? null, hydrated);
|
|
365
375
|
|
|
366
376
|
if (typeof type === 'string') {
|
|
367
377
|
const { children, dangerouslySetInnerHTML, ...rest } = props || {};
|
|
@@ -392,21 +402,16 @@ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
|
|
|
392
402
|
if (clientId) {
|
|
393
403
|
hydrated.add(clientId);
|
|
394
404
|
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
405
|
let ssrHtml: string;
|
|
399
406
|
try {
|
|
400
|
-
|
|
401
|
-
ssrHtml = await renderNode(result, new Set());
|
|
407
|
+
ssrHtml = await renderNode(await (type as Function)(props || {}), new Set());
|
|
402
408
|
} catch {
|
|
403
409
|
ssrHtml = PRERENDERED_HTML[clientId] ?? '';
|
|
404
410
|
}
|
|
405
411
|
return \`<span data-hydrate-id="\${clientId}" data-hydrate-props="\${escapeHtml(JSON.stringify(serializedProps))}">\${ssrHtml}</span>\`;
|
|
406
412
|
}
|
|
407
413
|
const instance = type.prototype?.isReactComponent ? new (type as any)(props) : null;
|
|
408
|
-
|
|
409
|
-
return renderNode(result, hydrated);
|
|
414
|
+
return renderNode(instance ? instance.render() : await (type as Function)(props || {}), hydrated);
|
|
410
415
|
}
|
|
411
416
|
|
|
412
417
|
return '';
|
|
@@ -417,9 +422,8 @@ const LAYOUT_COMPONENTS: Array<(props: any) => any> = [${layoutArrayItems}];
|
|
|
417
422
|
|
|
418
423
|
function wrapWithLayouts(element: any): any {
|
|
419
424
|
let el = element;
|
|
420
|
-
for (let i = LAYOUT_COMPONENTS.length - 1; i >= 0; i--)
|
|
425
|
+
for (let i = LAYOUT_COMPONENTS.length - 1; i >= 0; i--)
|
|
421
426
|
el = { type: LAYOUT_COMPONENTS[i], props: { children: el }, key: null, ref: null };
|
|
422
|
-
}
|
|
423
427
|
return el;
|
|
424
428
|
}
|
|
425
429
|
|
|
@@ -427,21 +431,22 @@ function wrapWithLayouts(element: any): any {
|
|
|
427
431
|
export default async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
428
432
|
try {
|
|
429
433
|
const parsed = new URL(req.url || '/', 'http://localhost');
|
|
430
|
-
const params: Record<string, string> = {};
|
|
431
|
-
parsed.searchParams.forEach((
|
|
434
|
+
const params: Record<string, string | string[]> = {};
|
|
435
|
+
parsed.searchParams.forEach((_, k) => {
|
|
436
|
+
params[k] = CATCH_ALL_NAMES.has(k)
|
|
437
|
+
? parsed.searchParams.getAll(k)
|
|
438
|
+
: parsed.searchParams.get(k) as string;
|
|
439
|
+
});
|
|
432
440
|
const url = req.url || '/';
|
|
433
441
|
|
|
434
442
|
const hydrated = new Set<string>();
|
|
435
|
-
const
|
|
436
|
-
const wrapped = wrapWithLayouts(pageElement);
|
|
443
|
+
const wrapped = wrapWithLayouts({ type: __page__.default, props: params as any, key: null, ref: null });
|
|
437
444
|
|
|
438
445
|
let appHtml = '';
|
|
439
|
-
const store = await runWithHtmlStore(async () => {
|
|
440
|
-
appHtml = await renderNode(wrapped, hydrated);
|
|
441
|
-
});
|
|
446
|
+
const store = await runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); });
|
|
442
447
|
|
|
443
|
-
const pageTitle = resolveTitle(store.titleOps, '
|
|
444
|
-
const headLines
|
|
448
|
+
const pageTitle = resolveTitle(store.titleOps, 'Nuke');
|
|
449
|
+
const headLines = [
|
|
445
450
|
' <meta charset="utf-8" />',
|
|
446
451
|
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
447
452
|
\` <title>\${escapeHtml(pageTitle)}</title>\`,
|
|
@@ -456,11 +461,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
456
461
|
];
|
|
457
462
|
|
|
458
463
|
const runtimeData = JSON.stringify({
|
|
459
|
-
hydrateIds: [...hydrated],
|
|
460
|
-
allIds: ALL_CLIENT_IDS,
|
|
461
|
-
url,
|
|
462
|
-
params,
|
|
463
|
-
debug: 'silent',
|
|
464
|
+
hydrateIds: [...hydrated], allIds: ALL_CLIENT_IDS, url, params, debug: 'silent',
|
|
464
465
|
}).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
|
|
465
466
|
|
|
466
467
|
const html = \`<!DOCTYPE html>
|
|
@@ -502,10 +503,10 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
502
503
|
res.end('Internal Server Error');
|
|
503
504
|
}
|
|
504
505
|
}
|
|
505
|
-
|
|
506
|
+
`;
|
|
506
507
|
}
|
|
507
508
|
async function bundleApiHandler(absPath) {
|
|
508
|
-
const adapterName = `_api_adapter_${
|
|
509
|
+
const adapterName = `_api_adapter_${randomBytes(4).toString("hex")}.ts`;
|
|
509
510
|
const adapterPath = path.join(path.dirname(absPath), adapterName);
|
|
510
511
|
fs.writeFileSync(adapterPath, makeApiAdapterSource(path.basename(absPath)));
|
|
511
512
|
let text;
|
|
@@ -526,23 +527,28 @@ async function bundleApiHandler(absPath) {
|
|
|
526
527
|
return text;
|
|
527
528
|
}
|
|
528
529
|
async function bundlePageHandler(opts) {
|
|
529
|
-
const {
|
|
530
|
-
|
|
530
|
+
const {
|
|
531
|
+
absPath,
|
|
532
|
+
clientComponentNames,
|
|
533
|
+
allClientIds,
|
|
534
|
+
layoutPaths,
|
|
535
|
+
prerenderedHtml,
|
|
536
|
+
catchAllNames
|
|
537
|
+
} = opts;
|
|
531
538
|
const adapterDir = path.dirname(absPath);
|
|
532
|
-
const adapterPath = path.join(adapterDir,
|
|
539
|
+
const adapterPath = path.join(adapterDir, `_page_adapter_${randomBytes(4).toString("hex")}.ts`);
|
|
533
540
|
const layoutImports = layoutPaths.map((lp, i) => {
|
|
534
541
|
const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
|
|
535
|
-
|
|
536
|
-
return `import __layout_${i}__ from ${JSON.stringify(importPath)};`;
|
|
542
|
+
return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
|
|
537
543
|
}).join("\n");
|
|
538
|
-
const layoutArrayItems = layoutPaths.map((_, i) => `__layout_${i}__`).join(", ");
|
|
539
544
|
fs.writeFileSync(adapterPath, makePageAdapterSource({
|
|
540
545
|
pageImport: JSON.stringify("./" + path.basename(absPath)),
|
|
541
546
|
layoutImports,
|
|
542
547
|
clientComponentNames,
|
|
543
548
|
allClientIds,
|
|
544
|
-
layoutArrayItems,
|
|
545
|
-
prerenderedHtml
|
|
549
|
+
layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
|
|
550
|
+
prerenderedHtml,
|
|
551
|
+
catchAllNames
|
|
546
552
|
}));
|
|
547
553
|
let text;
|
|
548
554
|
try {
|
|
@@ -553,38 +559,8 @@ async function bundlePageHandler(opts) {
|
|
|
553
559
|
platform: "node",
|
|
554
560
|
target: "node20",
|
|
555
561
|
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
|
-
],
|
|
562
|
+
packages: "external",
|
|
563
|
+
external: NODE_BUILTINS,
|
|
588
564
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
589
565
|
write: false
|
|
590
566
|
});
|
|
@@ -615,7 +591,7 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
|
|
|
615
591
|
fs.writeFileSync(path.join(outDir, `${id}.js`), browserResult.outputFiles[0].text);
|
|
616
592
|
const ssrTmp = path.join(
|
|
617
593
|
path.dirname(filePath),
|
|
618
|
-
`_ssr_${id}_${
|
|
594
|
+
`_ssr_${id}_${randomBytes(4).toString("hex")}.mjs`
|
|
619
595
|
);
|
|
620
596
|
try {
|
|
621
597
|
const ssrResult = await build({
|
|
@@ -635,7 +611,7 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
|
|
|
635
611
|
const { renderToString } = await import("react-dom/server");
|
|
636
612
|
prerendered.set(id, renderToString(createElement(Component, {})));
|
|
637
613
|
console.log(` prerendered ${id}`);
|
|
638
|
-
} catch
|
|
614
|
+
} catch {
|
|
639
615
|
prerendered.set(id, "");
|
|
640
616
|
} finally {
|
|
641
617
|
if (fs.existsSync(ssrTmp)) fs.unlinkSync(ssrTmp);
|
|
@@ -660,7 +636,6 @@ import React, {
|
|
|
660
636
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
661
637
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
662
638
|
export { initRuntime, setupLocationChangeMonitor } from "./${bundleFile}.js";
|
|
663
|
-
|
|
664
639
|
export {
|
|
665
640
|
useState, useEffect, useContext, useReducer, useCallback, useMemo,
|
|
666
641
|
useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
|
|
@@ -695,19 +670,18 @@ function copyPublicFiles(publicDir, destDir) {
|
|
|
695
670
|
(function walk(src, dest) {
|
|
696
671
|
fs.mkdirSync(dest, { recursive: true });
|
|
697
672
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
698
|
-
const
|
|
699
|
-
const
|
|
673
|
+
const s = path.join(src, entry.name);
|
|
674
|
+
const d = path.join(dest, entry.name);
|
|
700
675
|
if (entry.isDirectory()) {
|
|
701
|
-
walk(
|
|
676
|
+
walk(s, d);
|
|
702
677
|
} else {
|
|
703
|
-
fs.copyFileSync(
|
|
678
|
+
fs.copyFileSync(s, d);
|
|
704
679
|
count++;
|
|
705
680
|
}
|
|
706
681
|
}
|
|
707
682
|
})(publicDir, destDir);
|
|
708
|
-
if (count > 0)
|
|
683
|
+
if (count > 0)
|
|
709
684
|
console.log(` copied ${count} public file(s) \u2192 ${path.relative(process.cwd(), destDir)}/`);
|
|
710
|
-
}
|
|
711
685
|
}
|
|
712
686
|
export {
|
|
713
687
|
analyzeFile,
|