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.
@@ -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
- buildPages,
8
- bundleApiHandler,
9
- buildReactBundle,
10
- buildNukeBundle,
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(funcPath, bundleText) {
23
- const funcDir = path.join(FUNCTIONS_DIR, funcPath.slice(1) + ".func");
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 makeVercelRoute(srcRegex, paramNames, funcPath) {
32
- let dest = funcPath;
33
- if (paramNames.length > 0) {
34
- dest += "?" + paramNames.map((name, i) => `${name}=$${i + 1}`).join("&");
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
- return { src: srcRegex, dest };
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
- const vercelRoutes = [];
42
- for (const { srcRegex, paramNames, funcPath, absPath } of apiRoutes) {
43
- console.log(` building ${path.relative(SERVER_DIR, absPath)} \u2192 ${funcPath}`);
44
- emitVercelFunction(funcPath, await bundleApiHandler(absPath));
45
- vercelRoutes.push(makeVercelRoute(srcRegex, paramNames, funcPath));
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 builtPages = await buildPages(PAGES_DIR, STATIC_DIR);
48
- for (const { srcRegex, paramNames, funcPath, bundleText } of builtPages) {
49
- emitVercelFunction(funcPath, bundleText);
50
- vercelRoutes.push(makeVercelRoute(srcRegex, paramNames, funcPath));
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 buildReactBundle(STATIC_DIR);
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 ${vercelRoutes.length} function(s) \u2192 .vercel/output`);
273
+ \u2713 Vercel build complete \u2014 ${fnCount} function(s) \u2192 .vercel/output`);
65
274
  //# sourceMappingURL=build-vercel.js.map
@@ -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;AAEjB,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAIP,MAAM,aAAgB,KAAK,QAAQ,gBAAgB;AACnD,MAAM,gBAAgB,KAAK,KAAK,YAAY,WAAW;AACvD,MAAM,aAAgB,KAAK,KAAK,YAAY,QAAQ;AAEpD,GAAG,UAAU,eAAe,EAAE,WAAW,KAAK,CAAC;AAC/C,GAAG,UAAU,YAAe,EAAE,WAAW,KAAK,CAAC;AAI/C,MAAM,SAAa,MAAM,WAAW;AACpC,MAAM,aAAa,KAAK,QAAQ,OAAO,SAAS;AAChD,MAAM,YAAa,KAAK,QAAQ,aAAa;AAC7C,MAAM,aAAa,KAAK,QAAQ,cAAc;AAK9C,SAAS,mBAAmB,UAAkB,YAA0B;AACtE,QAAM,UAAU,KAAK,KAAK,eAAe,SAAS,MAAM,CAAC,IAAI,OAAO;AACpE,KAAG,UAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AACzC,KAAG,cAAc,KAAK,KAAK,SAAS,WAAW,GAAG,UAAU;AAC5D,KAAG;AAAA,IACD,KAAK,KAAK,SAAS,iBAAiB;AAAA,IACpC,KAAK,UAAU,EAAE,SAAS,cAAc,SAAS,aAAa,cAAc,SAAS,GAAG,MAAM,CAAC;AAAA,EACjG;AACF;AAIA,SAAS,gBAAgB,UAAkB,YAAsB,UAA+B;AAC9F,MAAI,OAAO;AACX,MAAI,WAAW,SAAS,GAAG;AACzB,YAAQ,MAAM,WAAW,IAAI,CAAC,MAAM,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC,EAAE,EAAE,KAAK,GAAG;AAAA,EACzE;AACA,SAAO,EAAE,KAAK,UAAU,KAAK;AAC/B;AAIA,MAAM,WAAW,UAAU,UAAU;AACrC,IAAI,SAAS,WAAW,EAAG,SAAQ,KAAK,oCAA+B,UAAU,EAAE;AAEnF,MAAM,YAAY,SACf,IAAI,cAAY,EAAE,GAAG,YAAY,SAAS,KAAK,GAAG,SAAS,KAAK,KAAK,YAAY,OAAO,EAAE,EAAE,EAC5F,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,EAAE,WAAW;AAE/C,MAAM,eAA8B,CAAC;AAErC,WAAW,EAAE,UAAU,YAAY,UAAU,QAAQ,KAAK,WAAW;AACnE,UAAQ,IAAI,eAAe,KAAK,SAAS,YAAY,OAAO,CAAC,aAAQ,QAAQ,EAAE;AAC/E,qBAAmB,UAAU,MAAM,iBAAiB,OAAO,CAAC;AAC5D,eAAa,KAAK,gBAAgB,UAAU,YAAY,QAAQ,CAAC;AACnE;AAIA,MAAM,aAAa,MAAM,WAAW,WAAW,UAAU;AAEzD,WAAW,EAAE,UAAU,YAAY,UAAU,WAAW,KAAK,YAAY;AACvE,qBAAmB,UAAU,UAAU;AACvC,eAAa,KAAK,gBAAgB,UAAU,YAAY,QAAQ,CAAC;AACnE;AAIA,GAAG;AAAA,EACD,KAAK,KAAK,YAAY,aAAa;AAAA,EACnC,KAAK,UAAU,EAAE,SAAS,GAAG,QAAQ,aAAa,GAAG,MAAM,CAAC;AAC9D;AAEA,GAAG;AAAA,EACD,KAAK,QAAQ,aAAa;AAAA,EAC1B,KAAK,UAAU,EAAE,SAAS,aAAa,GAAG,MAAM,CAAC;AACnD;AAIA,MAAM,iBAAiB,UAAU;AACjC,MAAM,gBAAgB,UAAU;AAChC,gBAAgB,YAAY,UAAU;AAEtC,QAAQ,IAAI;AAAA,sCAA+B,aAAa,MAAM,oCAA+B;",
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
- * This must be called after initRuntime sets up the navigation listener so
33
- * there's no race between the event firing and the listener being registered.
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, isNavigation) {
81
- const { hydrateRoot, createRoot } = await import("react-dom/client");
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 = isNavigation ? createRoot(node) : hydrateRoot(node, element);
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
- activeRoots.splice(0).forEach((r) => r.unmount());
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
- const newTitle = doc.querySelector("title");
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, true);
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, false);
199
+ await mountNodes(mods, log);
159
200
  log.info("\u{1F389} Done!");
160
201
  setupLocationChangeMonitor();
161
202
  }