nukejs 0.0.3 → 0.0.5
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/dist/build-common.d.ts +13 -10
- package/dist/build-common.js +19 -26
- package/dist/build-common.js.map +2 -2
- package/dist/build-node.js +3 -6
- package/dist/build-node.js.map +2 -2
- package/dist/build-vercel.js +233 -24
- package/dist/build-vercel.js.map +2 -2
- package/dist/bundle.d.ts +12 -2
- package/dist/bundle.js +56 -15
- package/dist/bundle.js.map +2 -2
- package/dist/bundler.js.map +1 -1
- package/dist/component-analyzer.js +20 -5
- package/dist/component-analyzer.js.map +2 -2
- package/dist/middleware.js.map +1 -1
- package/dist/router.d.ts +7 -2
- package/dist/router.js +4 -1
- package/dist/router.js.map +2 -2
- package/dist/ssr.d.ts +10 -4
- package/dist/ssr.js +11 -6
- package/dist/ssr.js.map +2 -2
- package/package.json +1 -1
package/dist/build-vercel.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { build } from "esbuild";
|
|
3
5
|
import { loadConfig } from "./config.js";
|
|
4
6
|
import {
|
|
5
|
-
analyzeFile,
|
|
6
7
|
walkFiles,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
analyzeFile,
|
|
9
|
+
collectServerPages,
|
|
10
|
+
collectGlobalClientRegistry,
|
|
11
|
+
bundleClientComponents,
|
|
12
|
+
findPageLayouts,
|
|
13
|
+
buildPerPageRegistry,
|
|
14
|
+
makePageAdapterSource,
|
|
15
|
+
buildCombinedBundle,
|
|
11
16
|
copyPublicFiles
|
|
12
17
|
} from "./build-common.js";
|
|
13
18
|
const OUTPUT_DIR = path.resolve(".vercel/output");
|
|
@@ -19,8 +24,8 @@ const config = await loadConfig();
|
|
|
19
24
|
const SERVER_DIR = path.resolve(config.serverDir);
|
|
20
25
|
const PAGES_DIR = path.resolve("./app/pages");
|
|
21
26
|
const PUBLIC_DIR = path.resolve("./app/public");
|
|
22
|
-
function emitVercelFunction(
|
|
23
|
-
const funcDir = path.join(FUNCTIONS_DIR,
|
|
27
|
+
function emitVercelFunction(name, bundleText) {
|
|
28
|
+
const funcDir = path.join(FUNCTIONS_DIR, name + ".func");
|
|
24
29
|
fs.mkdirSync(funcDir, { recursive: true });
|
|
25
30
|
fs.writeFileSync(path.join(funcDir, "index.mjs"), bundleText);
|
|
26
31
|
fs.writeFileSync(
|
|
@@ -28,26 +33,230 @@ function emitVercelFunction(funcPath, bundleText) {
|
|
|
28
33
|
JSON.stringify({ runtime: "nodejs20.x", handler: "index.mjs", launcherType: "Nodejs" }, null, 2)
|
|
29
34
|
);
|
|
30
35
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
function makeApiDispatcherSource(routes) {
|
|
37
|
+
const imports = routes.map((r, i) => `import * as __api_${i}__ from ${JSON.stringify(r.absPath)};`).join("\n");
|
|
38
|
+
const routeEntries = routes.map(
|
|
39
|
+
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, mod: __api_${i}__ },`
|
|
40
|
+
).join("\n");
|
|
41
|
+
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
42
|
+
${imports}
|
|
43
|
+
|
|
44
|
+
function enhance(res: ServerResponse) {
|
|
45
|
+
(res as any).json = function(data: any, status = 200) {
|
|
46
|
+
this.statusCode = status;
|
|
47
|
+
this.setHeader('Content-Type', 'application/json');
|
|
48
|
+
this.end(JSON.stringify(data));
|
|
49
|
+
};
|
|
50
|
+
(res as any).status = function(code: number) { this.statusCode = code; return this; };
|
|
51
|
+
return res;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function parseBody(req: IncomingMessage): Promise<any> {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
let body = '';
|
|
57
|
+
req.on('data', (chunk: any) => { body += chunk.toString(); });
|
|
58
|
+
req.on('end', () => {
|
|
59
|
+
try {
|
|
60
|
+
resolve(
|
|
61
|
+
body && req.headers['content-type']?.includes('application/json')
|
|
62
|
+
? JSON.parse(body)
|
|
63
|
+
: body,
|
|
64
|
+
);
|
|
65
|
+
} catch (e) { reject(e); }
|
|
66
|
+
});
|
|
67
|
+
req.on('error', reject);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ROUTES = [
|
|
72
|
+
${routeEntries}
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
76
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
77
|
+
const pathname = url.pathname;
|
|
78
|
+
|
|
79
|
+
for (const route of ROUTES) {
|
|
80
|
+
const m = pathname.match(new RegExp(route.regex));
|
|
81
|
+
if (!m) continue;
|
|
82
|
+
|
|
83
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
84
|
+
const apiRes = enhance(res);
|
|
85
|
+
const apiReq = req as any;
|
|
86
|
+
|
|
87
|
+
apiReq.body = await parseBody(req);
|
|
88
|
+
apiReq.query = Object.fromEntries(url.searchParams);
|
|
89
|
+
apiReq.params = {};
|
|
90
|
+
route.params.forEach((name: string, i: number) => { apiReq.params[name] = m[i + 1]; });
|
|
91
|
+
|
|
92
|
+
const fn = (route.mod as any)[method] ?? (route.mod as any)['default'];
|
|
93
|
+
if (typeof fn !== 'function') {
|
|
94
|
+
(apiRes as any).json({ error: \`Method \${method} not allowed\` }, 405);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await fn(apiReq, apiRes);
|
|
98
|
+
return;
|
|
35
99
|
}
|
|
36
|
-
|
|
100
|
+
|
|
101
|
+
res.statusCode = 404;
|
|
102
|
+
res.setHeader('Content-Type', 'application/json');
|
|
103
|
+
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
function makePagesDispatcherSource(routes) {
|
|
108
|
+
const imports = routes.map((r, i) => `import __page_${i}__ from ${JSON.stringify(r.adapterPath)};`).join("\n");
|
|
109
|
+
const routeEntries = routes.map(
|
|
110
|
+
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, handler: __page_${i}__ },`
|
|
111
|
+
).join("\n");
|
|
112
|
+
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
113
|
+
${imports}
|
|
114
|
+
|
|
115
|
+
const ROUTES: Array<{
|
|
116
|
+
regex: string;
|
|
117
|
+
params: string[];
|
|
118
|
+
handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
119
|
+
}> = [
|
|
120
|
+
${routeEntries}
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
124
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
125
|
+
const pathname = url.pathname;
|
|
126
|
+
|
|
127
|
+
for (const route of ROUTES) {
|
|
128
|
+
const m = pathname.match(new RegExp(route.regex));
|
|
129
|
+
if (!m) continue;
|
|
130
|
+
|
|
131
|
+
// Inject dynamic params as query-string values so page handlers can read
|
|
132
|
+
// them via new URL(req.url).searchParams \u2014 the same way they always have.
|
|
133
|
+
route.params.forEach((name, i) => url.searchParams.set(name, m[i + 1]));
|
|
134
|
+
req.url = pathname + (url.search || '');
|
|
135
|
+
|
|
136
|
+
return route.handler(req, res);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
res.statusCode = 404;
|
|
140
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
141
|
+
res.end('Not Found');
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
37
144
|
}
|
|
145
|
+
const vercelRoutes = [];
|
|
38
146
|
const apiFiles = walkFiles(SERVER_DIR);
|
|
39
147
|
if (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);
|
|
40
148
|
const apiRoutes = apiFiles.map((relPath) => ({ ...analyzeFile(relPath, "api"), absPath: path.join(SERVER_DIR, relPath) })).sort((a, b) => b.specificity - a.specificity);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
149
|
+
if (apiRoutes.length > 0) {
|
|
150
|
+
const dispatcherSource = makeApiDispatcherSource(apiRoutes);
|
|
151
|
+
const dispatcherPath = path.join(SERVER_DIR, `_api_dispatcher_${crypto.randomBytes(4).toString("hex")}.ts`);
|
|
152
|
+
fs.writeFileSync(dispatcherPath, dispatcherSource);
|
|
153
|
+
try {
|
|
154
|
+
const result = await build({
|
|
155
|
+
entryPoints: [dispatcherPath],
|
|
156
|
+
bundle: true,
|
|
157
|
+
format: "esm",
|
|
158
|
+
platform: "node",
|
|
159
|
+
target: "node20",
|
|
160
|
+
packages: "external",
|
|
161
|
+
write: false
|
|
162
|
+
});
|
|
163
|
+
emitVercelFunction("api", result.outputFiles[0].text);
|
|
164
|
+
console.log(` built API dispatcher \u2192 api.func (${apiRoutes.length} route(s))`);
|
|
165
|
+
} finally {
|
|
166
|
+
fs.unlinkSync(dispatcherPath);
|
|
167
|
+
}
|
|
168
|
+
for (const { srcRegex } of apiRoutes) {
|
|
169
|
+
vercelRoutes.push({ src: srcRegex, dest: "/api" });
|
|
170
|
+
}
|
|
46
171
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
172
|
+
const serverPages = collectServerPages(PAGES_DIR);
|
|
173
|
+
if (serverPages.length > 0) {
|
|
174
|
+
const globalClientRegistry = collectGlobalClientRegistry(serverPages, PAGES_DIR);
|
|
175
|
+
const prerenderedHtml = await bundleClientComponents(globalClientRegistry, PAGES_DIR, STATIC_DIR);
|
|
176
|
+
const prerenderedHtmlRecord = Object.fromEntries(prerenderedHtml);
|
|
177
|
+
const tempAdapterPaths = [];
|
|
178
|
+
for (const page of serverPages) {
|
|
179
|
+
const { absPath } = page;
|
|
180
|
+
const adapterDir = path.dirname(absPath);
|
|
181
|
+
const adapterPath = path.join(adapterDir, `_page_adapter_${crypto.randomBytes(4).toString("hex")}.ts`);
|
|
182
|
+
const layoutPaths = findPageLayouts(absPath, PAGES_DIR);
|
|
183
|
+
const { registry, clientComponentNames } = buildPerPageRegistry(absPath, layoutPaths, PAGES_DIR);
|
|
184
|
+
const layoutImports = layoutPaths.map((lp, i) => {
|
|
185
|
+
const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
|
|
186
|
+
return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
|
|
187
|
+
}).join("\n");
|
|
188
|
+
fs.writeFileSync(
|
|
189
|
+
adapterPath,
|
|
190
|
+
makePageAdapterSource({
|
|
191
|
+
pageImport: JSON.stringify("./" + path.basename(absPath)),
|
|
192
|
+
layoutImports,
|
|
193
|
+
clientComponentNames,
|
|
194
|
+
allClientIds: [...registry.keys()],
|
|
195
|
+
layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
|
|
196
|
+
prerenderedHtml: prerenderedHtmlRecord
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
tempAdapterPaths.push(adapterPath);
|
|
200
|
+
console.log(` prepared ${path.relative(PAGES_DIR, absPath)} \u2192 ${page.funcPath} [page]`);
|
|
201
|
+
}
|
|
202
|
+
const dispatcherRoutes = serverPages.map((page, i) => ({
|
|
203
|
+
adapterPath: tempAdapterPaths[i],
|
|
204
|
+
srcRegex: page.srcRegex,
|
|
205
|
+
paramNames: page.paramNames
|
|
206
|
+
}));
|
|
207
|
+
const dispatcherPath = path.join(PAGES_DIR, `_pages_dispatcher_${crypto.randomBytes(4).toString("hex")}.ts`);
|
|
208
|
+
fs.writeFileSync(dispatcherPath, makePagesDispatcherSource(dispatcherRoutes));
|
|
209
|
+
try {
|
|
210
|
+
const result = await build({
|
|
211
|
+
entryPoints: [dispatcherPath],
|
|
212
|
+
bundle: true,
|
|
213
|
+
format: "esm",
|
|
214
|
+
platform: "node",
|
|
215
|
+
target: "node20",
|
|
216
|
+
jsx: "automatic",
|
|
217
|
+
external: [
|
|
218
|
+
"node:*",
|
|
219
|
+
"http",
|
|
220
|
+
"https",
|
|
221
|
+
"fs",
|
|
222
|
+
"path",
|
|
223
|
+
"url",
|
|
224
|
+
"crypto",
|
|
225
|
+
"stream",
|
|
226
|
+
"buffer",
|
|
227
|
+
"events",
|
|
228
|
+
"util",
|
|
229
|
+
"os",
|
|
230
|
+
"net",
|
|
231
|
+
"tls",
|
|
232
|
+
"child_process",
|
|
233
|
+
"worker_threads",
|
|
234
|
+
"cluster",
|
|
235
|
+
"dgram",
|
|
236
|
+
"dns",
|
|
237
|
+
"readline",
|
|
238
|
+
"zlib",
|
|
239
|
+
"assert",
|
|
240
|
+
"module",
|
|
241
|
+
"perf_hooks",
|
|
242
|
+
"string_decoder",
|
|
243
|
+
"timers",
|
|
244
|
+
"async_hooks",
|
|
245
|
+
"v8",
|
|
246
|
+
"vm"
|
|
247
|
+
],
|
|
248
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
249
|
+
write: false
|
|
250
|
+
});
|
|
251
|
+
emitVercelFunction("pages", result.outputFiles[0].text);
|
|
252
|
+
console.log(` built Pages dispatcher \u2192 pages.func (${serverPages.length} page(s))`);
|
|
253
|
+
} finally {
|
|
254
|
+
fs.unlinkSync(dispatcherPath);
|
|
255
|
+
for (const p of tempAdapterPaths) if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
256
|
+
}
|
|
257
|
+
for (const { srcRegex } of serverPages) {
|
|
258
|
+
vercelRoutes.push({ src: srcRegex, dest: "/pages" });
|
|
259
|
+
}
|
|
51
260
|
}
|
|
52
261
|
fs.writeFileSync(
|
|
53
262
|
path.join(OUTPUT_DIR, "config.json"),
|
|
@@ -57,9 +266,9 @@ fs.writeFileSync(
|
|
|
57
266
|
path.resolve("vercel.json"),
|
|
58
267
|
JSON.stringify({ runtime: "nodejs20.x" }, null, 2)
|
|
59
268
|
);
|
|
60
|
-
await
|
|
61
|
-
await buildNukeBundle(STATIC_DIR);
|
|
269
|
+
await buildCombinedBundle(STATIC_DIR);
|
|
62
270
|
copyPublicFiles(PUBLIC_DIR, STATIC_DIR);
|
|
271
|
+
const fnCount = (apiRoutes.length > 0 ? 1 : 0) + (serverPages.length > 0 ? 1 : 0);
|
|
63
272
|
console.log(`
|
|
64
|
-
\u2713 Vercel build complete \u2014 ${
|
|
273
|
+
\u2713 Vercel build complete \u2014 ${fnCount} function(s) \u2192 .vercel/output`);
|
|
65
274
|
//# sourceMappingURL=build-vercel.js.map
|
package/dist/build-vercel.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/build-vercel.ts"],
|
|
4
|
-
"sourcesContent": ["import fs from 'fs';\r\nimport path from 'path';\r\n\r\nimport { loadConfig } from './config';\r\nimport {\r\n analyzeFile,\r\n walkFiles,\r\n buildPages,\r\n bundleApiHandler,\r\n buildReactBundle,\r\n buildNukeBundle,\r\n copyPublicFiles,\r\n} from './build-common';\r\n\r\n// \u2500\u2500\u2500 Output directories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst OUTPUT_DIR = path.resolve('.vercel/output');\r\nconst FUNCTIONS_DIR = path.join(OUTPUT_DIR, 'functions');\r\nconst STATIC_DIR = path.join(OUTPUT_DIR, 'static');\r\n\r\nfs.mkdirSync(FUNCTIONS_DIR, { recursive: true });\r\nfs.mkdirSync(STATIC_DIR, { recursive: true });\r\n\r\n// \u2500\u2500\u2500 Config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst config = await loadConfig();\r\nconst SERVER_DIR = path.resolve(config.serverDir);\r\nconst PAGES_DIR = path.resolve('./app/pages');\r\nconst PUBLIC_DIR = path.resolve('./app/public');\r\n\r\n// \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Writes a bundled handler into a Vercel .func directory. */\r\nfunction emitVercelFunction(funcPath: string, bundleText: string): void {\r\n const funcDir = path.join(FUNCTIONS_DIR, funcPath.slice(1) + '.func');\r\n fs.mkdirSync(funcDir, { recursive: true });\r\n fs.writeFileSync(path.join(funcDir, 'index.mjs'), bundleText);\r\n fs.writeFileSync(\r\n path.join(funcDir, '.vc-config.json'),\r\n JSON.stringify({ runtime: 'nodejs20.x', handler: 'index.mjs', launcherType: 'Nodejs' }, null, 2),\r\n );\r\n}\r\n\r\ntype VercelRoute = { src: string; dest: string };\r\n\r\nfunction makeVercelRoute(srcRegex: string, paramNames: string[], funcPath: string): VercelRoute {\r\n let dest = funcPath;\r\n if (paramNames.length > 0) {\r\n dest += '?' + paramNames.map((name, i) => `${name}=$${i + 1}`).join('&');\r\n }\r\n return { src: srcRegex, dest };\r\n}\r\n\r\n// \u2500\u2500\u2500 API routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst apiFiles = walkFiles(SERVER_DIR);\r\nif (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);\r\n\r\nconst apiRoutes = apiFiles\r\n .map(relPath => ({ ...analyzeFile(relPath, 'api'), absPath: path.join(SERVER_DIR, relPath) }))\r\n .sort((a, b) => b.specificity - a.specificity);\r\n\r\nconst vercelRoutes: VercelRoute[] = [];\r\n\r\nfor (const { srcRegex, paramNames, funcPath, absPath } of apiRoutes) {\r\n console.log(` building ${path.relative(SERVER_DIR, absPath)} \u2192 ${funcPath}`);\r\n emitVercelFunction(funcPath, await bundleApiHandler(absPath));\r\n vercelRoutes.push(makeVercelRoute(srcRegex, paramNames, funcPath));\r\n}\r\n\r\n// \u2500\u2500\u2500 Page routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst builtPages = await buildPages(PAGES_DIR, STATIC_DIR);\r\n\r\nfor (const { srcRegex, paramNames, funcPath, bundleText } of builtPages) {\r\n emitVercelFunction(funcPath, bundleText);\r\n vercelRoutes.push(makeVercelRoute(srcRegex, paramNames, funcPath));\r\n}\r\n\r\n// \u2500\u2500\u2500 Vercel config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfs.writeFileSync(\r\n path.join(OUTPUT_DIR, 'config.json'),\r\n JSON.stringify({ version: 3, routes: vercelRoutes }, null, 2),\r\n);\r\n\r\nfs.writeFileSync(\r\n path.resolve('vercel.json'),\r\n JSON.stringify({ runtime: 'nodejs20.x' }, null, 2),\r\n);\r\n\r\n// \u2500\u2500\u2500 Static assets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nawait buildReactBundle(STATIC_DIR);\r\nawait buildNukeBundle(STATIC_DIR);\r\ncopyPublicFiles(PUBLIC_DIR, STATIC_DIR);\r\n\r\nconsole.log(`\\n\u2713 Vercel build complete \u2014 ${vercelRoutes.length} function(s) \u2192 .vercel/output`);"],
|
|
5
|
-
"mappings": "AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;
|
|
4
|
+
"sourcesContent": ["import fs from 'fs';\r\nimport path from 'path';\r\nimport crypto from 'crypto';\r\nimport { build } from 'esbuild';\r\n\r\nimport { loadConfig } from './config';\r\nimport {\r\n walkFiles,\r\n analyzeFile,\r\n collectServerPages,\r\n collectGlobalClientRegistry,\r\n bundleClientComponents,\r\n findPageLayouts,\r\n buildPerPageRegistry,\r\n makePageAdapterSource,\r\n buildCombinedBundle,\r\n copyPublicFiles,\r\n} from './build-common';\r\n\r\n// \u2500\u2500\u2500 Output directories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst OUTPUT_DIR = path.resolve('.vercel/output');\r\nconst FUNCTIONS_DIR = path.join(OUTPUT_DIR, 'functions');\r\nconst STATIC_DIR = path.join(OUTPUT_DIR, 'static');\r\n\r\nfs.mkdirSync(FUNCTIONS_DIR, { recursive: true });\r\nfs.mkdirSync(STATIC_DIR, { recursive: true });\r\n\r\n// \u2500\u2500\u2500 Config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst config = await loadConfig();\r\nconst SERVER_DIR = path.resolve(config.serverDir);\r\nconst PAGES_DIR = path.resolve('./app/pages');\r\nconst PUBLIC_DIR = path.resolve('./app/public');\r\n\r\n// \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ntype VercelRoute = { src: string; dest: string };\r\n\r\n/** Writes a bundled dispatcher into a Vercel .func directory. */\r\nfunction emitVercelFunction(name: string, bundleText: string): void {\r\n const funcDir = path.join(FUNCTIONS_DIR, name + '.func');\r\n fs.mkdirSync(funcDir, { recursive: true });\r\n fs.writeFileSync(path.join(funcDir, 'index.mjs'), bundleText);\r\n fs.writeFileSync(\r\n path.join(funcDir, '.vc-config.json'),\r\n JSON.stringify({ runtime: 'nodejs20.x', handler: 'index.mjs', launcherType: 'Nodejs' }, null, 2),\r\n );\r\n}\r\n\r\n// \u2500\u2500\u2500 API dispatcher source \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Generates a single dispatcher that imports every API route module directly,\r\n * matches the incoming URL against each route's regex, injects captured params,\r\n * and calls the right HTTP-method export (GET, POST, \u2026) or default export.\r\n *\r\n * enhance / parseBody helpers are included once rather than once per route.\r\n */\r\nfunction makeApiDispatcherSource(\r\n routes: Array<{ absPath: string; srcRegex: string; paramNames: string[] }>,\r\n): string {\r\n const imports = routes\r\n .map((r, i) => `import * as __api_${i}__ from ${JSON.stringify(r.absPath)};`)\r\n .join('\\n');\r\n\r\n const routeEntries = routes\r\n .map((r, i) =>\r\n ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, mod: __api_${i}__ },`,\r\n )\r\n .join('\\n');\r\n\r\n return `\\\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\n${imports}\r\n\r\nfunction enhance(res: ServerResponse) {\r\n (res as any).json = function(data: any, status = 200) {\r\n this.statusCode = status;\r\n this.setHeader('Content-Type', 'application/json');\r\n this.end(JSON.stringify(data));\r\n };\r\n (res as any).status = function(code: number) { this.statusCode = code; return this; };\r\n return res;\r\n}\r\n\r\nasync function parseBody(req: IncomingMessage): Promise<any> {\r\n return new Promise((resolve, reject) => {\r\n let body = '';\r\n req.on('data', (chunk: any) => { body += chunk.toString(); });\r\n req.on('end', () => {\r\n try {\r\n resolve(\r\n body && req.headers['content-type']?.includes('application/json')\r\n ? JSON.parse(body)\r\n : body,\r\n );\r\n } catch (e) { reject(e); }\r\n });\r\n req.on('error', reject);\r\n });\r\n}\r\n\r\nconst ROUTES = [\r\n${routeEntries}\r\n];\r\n\r\nexport default async function handler(req: IncomingMessage, res: ServerResponse) {\r\n const url = new URL(req.url || '/', 'http://localhost');\r\n const pathname = url.pathname;\r\n\r\n for (const route of ROUTES) {\r\n const m = pathname.match(new RegExp(route.regex));\r\n if (!m) continue;\r\n\r\n const method = (req.method || 'GET').toUpperCase();\r\n const apiRes = enhance(res);\r\n const apiReq = req as any;\r\n\r\n apiReq.body = await parseBody(req);\r\n apiReq.query = Object.fromEntries(url.searchParams);\r\n apiReq.params = {};\r\n route.params.forEach((name: string, i: number) => { apiReq.params[name] = m[i + 1]; });\r\n\r\n const fn = (route.mod as any)[method] ?? (route.mod as any)['default'];\r\n if (typeof fn !== 'function') {\r\n (apiRes as any).json({ error: \\`Method \\${method} not allowed\\` }, 405);\r\n return;\r\n }\r\n await fn(apiReq, apiRes);\r\n return;\r\n }\r\n\r\n res.statusCode = 404;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: 'Not Found' }));\r\n}\r\n`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Pages dispatcher source \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Generates a dispatcher that imports each page's pre-generated adapter by its\r\n * temp file path, matches the incoming URL, injects captured dynamic params as\r\n * query-string values (page handlers read params from req.url searchParams),\r\n * then delegates to the matching handler.\r\n */\r\nfunction makePagesDispatcherSource(\r\n routes: Array<{ adapterPath: string; srcRegex: string; paramNames: string[] }>,\r\n): string {\r\n const imports = routes\r\n .map((r, i) => `import __page_${i}__ from ${JSON.stringify(r.adapterPath)};`)\r\n .join('\\n');\r\n\r\n const routeEntries = routes\r\n .map((r, i) =>\r\n ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, handler: __page_${i}__ },`,\r\n )\r\n .join('\\n');\r\n\r\n return `\\\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\n${imports}\r\n\r\nconst ROUTES: Array<{\r\n regex: string;\r\n params: string[];\r\n handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;\r\n}> = [\r\n${routeEntries}\r\n];\r\n\r\nexport default async function handler(req: IncomingMessage, res: ServerResponse) {\r\n const url = new URL(req.url || '/', 'http://localhost');\r\n const pathname = url.pathname;\r\n\r\n for (const route of ROUTES) {\r\n const m = pathname.match(new RegExp(route.regex));\r\n if (!m) continue;\r\n\r\n // Inject dynamic params as query-string values so page handlers can read\r\n // them via new URL(req.url).searchParams \u2014 the same way they always have.\r\n route.params.forEach((name, i) => url.searchParams.set(name, m[i + 1]));\r\n req.url = pathname + (url.search || '');\r\n\r\n return route.handler(req, res);\r\n }\r\n\r\n res.statusCode = 404;\r\n res.setHeader('Content-Type', 'text/plain; charset=utf-8');\r\n res.end('Not Found');\r\n}\r\n`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Build API function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst vercelRoutes: VercelRoute[] = [];\r\n\r\nconst apiFiles = walkFiles(SERVER_DIR);\r\nif (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);\r\n\r\nconst apiRoutes = apiFiles\r\n .map(relPath => ({ ...analyzeFile(relPath, 'api'), absPath: path.join(SERVER_DIR, relPath) }))\r\n .sort((a, b) => b.specificity - a.specificity);\r\n\r\nif (apiRoutes.length > 0) {\r\n const dispatcherSource = makeApiDispatcherSource(apiRoutes);\r\n const dispatcherPath = path.join(SERVER_DIR, `_api_dispatcher_${crypto.randomBytes(4).toString('hex')}.ts`);\r\n fs.writeFileSync(dispatcherPath, dispatcherSource);\r\n\r\n try {\r\n const result = await build({\r\n entryPoints: [dispatcherPath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'node',\r\n target: 'node20',\r\n packages: 'external',\r\n write: false,\r\n });\r\n emitVercelFunction('api', result.outputFiles[0].text);\r\n console.log(` built API dispatcher \u2192 api.func (${apiRoutes.length} route(s))`);\r\n } finally {\r\n fs.unlinkSync(dispatcherPath);\r\n }\r\n\r\n // API routes are listed first \u2014 they win on any URL collision with pages.\r\n for (const { srcRegex } of apiRoutes) {\r\n vercelRoutes.push({ src: srcRegex, dest: '/api' });\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Build Pages function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst serverPages = collectServerPages(PAGES_DIR);\r\n\r\nif (serverPages.length > 0) {\r\n // Pass 1 \u2014 bundle all client components to static files.\r\n const globalClientRegistry = collectGlobalClientRegistry(serverPages, PAGES_DIR);\r\n const prerenderedHtml = await bundleClientComponents(globalClientRegistry, PAGES_DIR, STATIC_DIR);\r\n const prerenderedHtmlRecord = Object.fromEntries(prerenderedHtml);\r\n\r\n // Pass 2 \u2014 write one temp adapter per page next to its source file (so\r\n // relative imports inside the component resolve correctly), then\r\n // bundle everything in one esbuild pass via the dispatcher.\r\n const tempAdapterPaths: string[] = [];\r\n\r\n for (const page of serverPages) {\r\n const { absPath } = page;\r\n const adapterDir = path.dirname(absPath);\r\n const adapterPath = path.join(adapterDir, `_page_adapter_${crypto.randomBytes(4).toString('hex')}.ts`);\r\n\r\n const layoutPaths = findPageLayouts(absPath, PAGES_DIR);\r\n const { registry, clientComponentNames } = buildPerPageRegistry(absPath, layoutPaths, PAGES_DIR);\r\n\r\n const layoutImports = layoutPaths\r\n .map((lp, i) => {\r\n const rel = path.relative(adapterDir, lp).replace(/\\\\/g, '/');\r\n return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith('.') ? rel : './' + rel)};`;\r\n })\r\n .join('\\n');\r\n\r\n fs.writeFileSync(\r\n adapterPath,\r\n makePageAdapterSource({\r\n pageImport: JSON.stringify('./' + path.basename(absPath)),\r\n layoutImports,\r\n clientComponentNames,\r\n allClientIds: [...registry.keys()],\r\n layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(', '),\r\n prerenderedHtml: prerenderedHtmlRecord,\r\n }),\r\n );\r\n\r\n tempAdapterPaths.push(adapterPath);\r\n console.log(` prepared ${path.relative(PAGES_DIR, absPath)} \u2192 ${page.funcPath} [page]`);\r\n }\r\n\r\n // Write the dispatcher and let esbuild bundle all adapters in one pass.\r\n const dispatcherRoutes = serverPages.map((page, i) => ({\r\n adapterPath: tempAdapterPaths[i],\r\n srcRegex: page.srcRegex,\r\n paramNames: page.paramNames,\r\n }));\r\n\r\n const dispatcherPath = path.join(PAGES_DIR, `_pages_dispatcher_${crypto.randomBytes(4).toString('hex')}.ts`);\r\n fs.writeFileSync(dispatcherPath, makePagesDispatcherSource(dispatcherRoutes));\r\n\r\n try {\r\n const result = await build({\r\n entryPoints: [dispatcherPath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'node',\r\n target: 'node20',\r\n jsx: 'automatic',\r\n external: [\r\n 'node:*',\r\n 'http', 'https', 'fs', 'path', 'url', 'crypto', 'stream', 'buffer',\r\n 'events', 'util', 'os', 'net', 'tls', 'child_process', 'worker_threads',\r\n 'cluster', 'dgram', 'dns', 'readline', 'zlib', 'assert', 'module',\r\n 'perf_hooks', 'string_decoder', 'timers', 'async_hooks', 'v8', 'vm',\r\n ],\r\n define: { 'process.env.NODE_ENV': '\"production\"' },\r\n write: false,\r\n });\r\n emitVercelFunction('pages', result.outputFiles[0].text);\r\n console.log(` built Pages dispatcher \u2192 pages.func (${serverPages.length} page(s))`);\r\n } finally {\r\n fs.unlinkSync(dispatcherPath);\r\n for (const p of tempAdapterPaths) if (fs.existsSync(p)) fs.unlinkSync(p);\r\n }\r\n\r\n for (const { srcRegex } of serverPages) {\r\n vercelRoutes.push({ src: srcRegex, dest: '/pages' });\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Vercel config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfs.writeFileSync(\r\n path.join(OUTPUT_DIR, 'config.json'),\r\n JSON.stringify({ version: 3, routes: vercelRoutes }, null, 2),\r\n);\r\n\r\nfs.writeFileSync(\r\n path.resolve('vercel.json'),\r\n JSON.stringify({ runtime: 'nodejs20.x' }, null, 2),\r\n);\r\n\r\n// \u2500\u2500\u2500 Static assets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nawait buildCombinedBundle(STATIC_DIR);\r\ncopyPublicFiles(PUBLIC_DIR, STATIC_DIR);\r\n\r\nconst fnCount = (apiRoutes.length > 0 ? 1 : 0) + (serverPages.length > 0 ? 1 : 0);\r\nconsole.log(`\\n\u2713 Vercel build complete \u2014 ${fnCount} function(s) \u2192 .vercel/output`);"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/bundle.d.ts
CHANGED
|
@@ -23,14 +23,24 @@
|
|
|
23
23
|
* - The handler fetches the target URL as HTML, diffs the #app container,
|
|
24
24
|
* unmounts the old React roots, and re-hydrates the new ones.
|
|
25
25
|
* - HMR navigations add ?__hmr=1 so the server skips client-SSR (faster).
|
|
26
|
+
*
|
|
27
|
+
* Head tag management:
|
|
28
|
+
* - The SSR renderer wraps every useHtml()-generated <meta>, <link>, <style>,
|
|
29
|
+
* and <script> tag in <!--n-head-->…<!--/n-head--> sentinel comments.
|
|
30
|
+
* - On each navigation the client diffs the live sentinel block against the
|
|
31
|
+
* incoming one by fingerprint, adding new tags and removing gone ones.
|
|
32
|
+
* Tags shared between pages (e.g. a layout stylesheet) are left untouched
|
|
33
|
+
* so there is no removal/re-insertion flash.
|
|
34
|
+
* - New tags are always inserted before <!--/n-head--> so they stay inside
|
|
35
|
+
* the tracked block and remain visible to the diff on subsequent navigations.
|
|
26
36
|
*/
|
|
27
37
|
/**
|
|
28
38
|
* Patches history.pushState and history.replaceState to fire a custom
|
|
29
39
|
* 'locationchange' event on window. Also listens to 'popstate' for
|
|
30
40
|
* back/forward navigation.
|
|
31
41
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
42
|
+
* Called after initRuntime sets up the navigation listener so there is no
|
|
43
|
+
* race between the event firing and the listener being registered.
|
|
34
44
|
*/
|
|
35
45
|
export declare function setupLocationChangeMonitor(): void;
|
|
36
46
|
type ClientDebugLevel = 'silent' | 'error' | 'info' | 'verbose';
|
package/dist/bundle.js
CHANGED
|
@@ -77,8 +77,8 @@ async function loadModules(ids, log, bust = "") {
|
|
|
77
77
|
return mods;
|
|
78
78
|
}
|
|
79
79
|
const activeRoots = [];
|
|
80
|
-
async function mountNodes(mods, log
|
|
81
|
-
const { hydrateRoot
|
|
80
|
+
async function mountNodes(mods, log) {
|
|
81
|
+
const { hydrateRoot } = await import("react-dom/client");
|
|
82
82
|
const React = await import("react");
|
|
83
83
|
const nodes = document.querySelectorAll("[data-hydrate-id]");
|
|
84
84
|
log.verbose("Found", nodes.length, "hydration point(s)");
|
|
@@ -98,8 +98,7 @@ async function mountNodes(mods, log, isNavigation) {
|
|
|
98
98
|
}
|
|
99
99
|
try {
|
|
100
100
|
const element = React.default.createElement(Comp, await reconstructProps(rawProps, mods));
|
|
101
|
-
const root =
|
|
102
|
-
if (isNavigation) root.render(element);
|
|
101
|
+
const root = hydrateRoot(node, element);
|
|
103
102
|
activeRoots.push(root);
|
|
104
103
|
log.verbose("\u2713 Mounted:", id);
|
|
105
104
|
} catch (err) {
|
|
@@ -107,13 +106,54 @@ async function mountNodes(mods, log, isNavigation) {
|
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
108
|
}
|
|
109
|
+
function headBlock(head) {
|
|
110
|
+
const nodes = [];
|
|
111
|
+
let closeComment = null;
|
|
112
|
+
let inside = false;
|
|
113
|
+
for (const child of Array.from(head.childNodes)) {
|
|
114
|
+
if (child.nodeType === Node.COMMENT_NODE) {
|
|
115
|
+
const text = child.data.trim();
|
|
116
|
+
if (text === "n-head") {
|
|
117
|
+
inside = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (text === "/n-head") {
|
|
121
|
+
closeComment = child;
|
|
122
|
+
inside = false;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (inside && child.nodeType === Node.ELEMENT_NODE)
|
|
127
|
+
nodes.push(child);
|
|
128
|
+
}
|
|
129
|
+
return { nodes, closeComment };
|
|
130
|
+
}
|
|
131
|
+
function fingerprint(el) {
|
|
132
|
+
return el.tagName + "|" + Array.from(el.attributes).sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}=${a.value}`).join("&");
|
|
133
|
+
}
|
|
134
|
+
function syncHeadTags(doc) {
|
|
135
|
+
const live = headBlock(document.head);
|
|
136
|
+
const next = headBlock(doc.head);
|
|
137
|
+
const liveMap = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const el of live.nodes) liveMap.set(fingerprint(el), el);
|
|
139
|
+
const nextMap = /* @__PURE__ */ new Map();
|
|
140
|
+
for (const el of next.nodes) nextMap.set(fingerprint(el), el);
|
|
141
|
+
let anchor = live.closeComment;
|
|
142
|
+
if (!anchor) {
|
|
143
|
+
document.head.appendChild(document.createComment("n-head"));
|
|
144
|
+
anchor = document.createComment("/n-head");
|
|
145
|
+
document.head.appendChild(anchor);
|
|
146
|
+
}
|
|
147
|
+
for (const [fp, el] of nextMap)
|
|
148
|
+
if (!liveMap.has(fp)) document.head.insertBefore(el, anchor);
|
|
149
|
+
for (const [fp, el] of liveMap)
|
|
150
|
+
if (!nextMap.has(fp)) el.remove();
|
|
151
|
+
}
|
|
110
152
|
function syncAttrs(live, next) {
|
|
111
|
-
for (const { name, value } of Array.from(next.attributes))
|
|
153
|
+
for (const { name, value } of Array.from(next.attributes))
|
|
112
154
|
live.setAttribute(name, value);
|
|
113
|
-
}
|
|
114
|
-
for (const { name } of Array.from(live.attributes)) {
|
|
155
|
+
for (const { name } of Array.from(live.attributes))
|
|
115
156
|
if (!next.hasAttribute(name)) live.removeAttribute(name);
|
|
116
|
-
}
|
|
117
157
|
}
|
|
118
158
|
function setupNavigation(log) {
|
|
119
159
|
window.addEventListener("locationchange", async ({ detail: { href, hmr } }) => {
|
|
@@ -129,19 +169,20 @@ function setupNavigation(log) {
|
|
|
129
169
|
const newApp = doc.getElementById("app");
|
|
130
170
|
const currApp = document.getElementById("app");
|
|
131
171
|
if (!newApp || !currApp) return;
|
|
132
|
-
|
|
172
|
+
syncHeadTags(doc);
|
|
173
|
+
syncAttrs(document.documentElement, doc.documentElement);
|
|
174
|
+
syncAttrs(document.body, doc.body);
|
|
133
175
|
currApp.innerHTML = newApp.innerHTML;
|
|
176
|
+
const newTitle = doc.querySelector("title");
|
|
177
|
+
if (newTitle) document.title = newTitle.textContent ?? "";
|
|
134
178
|
const newDataEl = doc.getElementById("__n_data");
|
|
135
179
|
const currDataEl = document.getElementById("__n_data");
|
|
136
180
|
if (newDataEl && currDataEl) currDataEl.textContent = newDataEl.textContent;
|
|
137
|
-
|
|
138
|
-
if (newTitle) document.title = newTitle.textContent ?? "";
|
|
139
|
-
syncAttrs(document.documentElement, doc.documentElement);
|
|
140
|
-
syncAttrs(document.body, doc.body);
|
|
181
|
+
activeRoots.splice(0).forEach((r) => r.unmount());
|
|
141
182
|
const navData = JSON.parse(currDataEl?.textContent ?? "{}");
|
|
142
183
|
log.info("\u{1F504} Route \u2192", href, "\u2014 mounting", navData.hydrateIds?.length ?? 0, "component(s)");
|
|
143
184
|
const mods = await loadModules(navData.allIds ?? [], log, String(Date.now()));
|
|
144
|
-
await mountNodes(mods, log
|
|
185
|
+
await mountNodes(mods, log);
|
|
145
186
|
window.scrollTo(0, 0);
|
|
146
187
|
log.info("\u{1F389} Navigation complete:", href);
|
|
147
188
|
} catch (err) {
|
|
@@ -155,7 +196,7 @@ async function initRuntime(data) {
|
|
|
155
196
|
log.info("\u{1F680} Partial hydration:", data.hydrateIds.length, "root component(s)");
|
|
156
197
|
setupNavigation(log);
|
|
157
198
|
const mods = await loadModules(data.allIds, log);
|
|
158
|
-
await mountNodes(mods, log
|
|
199
|
+
await mountNodes(mods, log);
|
|
159
200
|
log.info("\u{1F389} Done!");
|
|
160
201
|
setupLocationChangeMonitor();
|
|
161
202
|
}
|