nukejs 0.0.10 → 0.0.12

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.
Files changed (80) hide show
  1. package/README.md +283 -1
  2. package/dist/Link.d.ts +8 -2
  3. package/dist/Link.js +2 -3
  4. package/dist/app.js +1 -2
  5. package/dist/build-common.js +141 -24
  6. package/dist/build-node.js +67 -5
  7. package/dist/build-vercel.js +81 -9
  8. package/dist/builder.js +30 -4
  9. package/dist/bundle.d.ts +7 -0
  10. package/dist/bundle.js +47 -4
  11. package/dist/bundler.js +0 -1
  12. package/dist/component-analyzer.js +0 -1
  13. package/dist/config.js +0 -1
  14. package/dist/hmr-bundle.js +0 -1
  15. package/dist/hmr.js +4 -1
  16. package/dist/html-store.js +0 -1
  17. package/dist/http-server.js +0 -1
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.js +5 -1
  20. package/dist/logger.js +0 -1
  21. package/dist/metadata.js +0 -1
  22. package/dist/middleware-loader.js +0 -1
  23. package/dist/middleware.example.js +0 -1
  24. package/dist/middleware.js +0 -1
  25. package/dist/renderer.js +3 -9
  26. package/dist/request-store.d.ts +84 -0
  27. package/dist/request-store.js +46 -0
  28. package/dist/router.js +0 -1
  29. package/dist/ssr.js +91 -19
  30. package/dist/use-html.js +0 -1
  31. package/dist/use-request.d.ts +74 -0
  32. package/dist/use-request.js +48 -0
  33. package/dist/use-router.js +0 -1
  34. package/dist/utils.js +0 -1
  35. package/package.json +1 -1
  36. package/dist/Link.js.map +0 -7
  37. package/dist/app.d.ts +0 -19
  38. package/dist/app.js.map +0 -7
  39. package/dist/build-common.d.ts +0 -172
  40. package/dist/build-common.js.map +0 -7
  41. package/dist/build-node.d.ts +0 -15
  42. package/dist/build-node.js.map +0 -7
  43. package/dist/build-vercel.d.ts +0 -19
  44. package/dist/build-vercel.js.map +0 -7
  45. package/dist/builder.d.ts +0 -11
  46. package/dist/builder.js.map +0 -7
  47. package/dist/bundle.js.map +0 -7
  48. package/dist/bundler.d.ts +0 -58
  49. package/dist/bundler.js.map +0 -7
  50. package/dist/component-analyzer.d.ts +0 -75
  51. package/dist/component-analyzer.js.map +0 -7
  52. package/dist/config.d.ts +0 -35
  53. package/dist/config.js.map +0 -7
  54. package/dist/hmr-bundle.d.ts +0 -25
  55. package/dist/hmr-bundle.js.map +0 -7
  56. package/dist/hmr.d.ts +0 -55
  57. package/dist/hmr.js.map +0 -7
  58. package/dist/html-store.d.ts +0 -128
  59. package/dist/html-store.js.map +0 -7
  60. package/dist/http-server.d.ts +0 -92
  61. package/dist/http-server.js.map +0 -7
  62. package/dist/index.js.map +0 -7
  63. package/dist/logger.js.map +0 -7
  64. package/dist/metadata.d.ts +0 -50
  65. package/dist/metadata.js.map +0 -7
  66. package/dist/middleware-loader.d.ts +0 -50
  67. package/dist/middleware-loader.js.map +0 -7
  68. package/dist/middleware.d.ts +0 -22
  69. package/dist/middleware.example.d.ts +0 -8
  70. package/dist/middleware.example.js.map +0 -7
  71. package/dist/middleware.js.map +0 -7
  72. package/dist/renderer.d.ts +0 -44
  73. package/dist/renderer.js.map +0 -7
  74. package/dist/router.d.ts +0 -89
  75. package/dist/router.js.map +0 -7
  76. package/dist/ssr.d.ts +0 -45
  77. package/dist/ssr.js.map +0 -7
  78. package/dist/use-html.js.map +0 -7
  79. package/dist/use-router.js.map +0 -7
  80. package/dist/utils.js.map +0 -7
@@ -13,6 +13,10 @@ const OUT_DIR = path.resolve("dist");
13
13
  const API_DIR = path.join(OUT_DIR, "api");
14
14
  const PAGES_DIR_ = path.join(OUT_DIR, "pages");
15
15
  const STATIC_DIR = path.join(OUT_DIR, "static");
16
+ if (fs.existsSync(OUT_DIR)) {
17
+ fs.rmSync(OUT_DIR, { recursive: true, force: true });
18
+ console.log("\u{1F5D1}\uFE0F Cleaned dist/");
19
+ }
16
20
  for (const dir of [API_DIR, PAGES_DIR_, STATIC_DIR])
17
21
  fs.mkdirSync(dir, { recursive: true });
18
22
  const config = await loadConfig();
@@ -34,7 +38,7 @@ for (const { srcRegex, paramNames, catchAllNames, funcPath, absPath } of apiRout
34
38
  fs.writeFileSync(outPath, await bundleApiHandler(absPath));
35
39
  manifest.push({ srcRegex, paramNames, catchAllNames, handler: path.join("api", filename), type: "api" });
36
40
  }
37
- const builtPages = await buildPages(PAGES_DIR, STATIC_DIR);
41
+ const { pages: builtPages, has404, has500 } = await buildPages(PAGES_DIR, STATIC_DIR, PAGES_DIR_);
38
42
  for (const { srcRegex, paramNames, catchAllNames, funcPath, bundleText } of builtPages) {
39
43
  const filename = funcPathToFilename(funcPath, "page");
40
44
  const outPath = path.join(PAGES_DIR_, filename);
@@ -42,6 +46,8 @@ for (const { srcRegex, paramNames, catchAllNames, funcPath, bundleText } of buil
42
46
  fs.writeFileSync(outPath, bundleText);
43
47
  manifest.push({ srcRegex, paramNames, catchAllNames, handler: path.join("pages", filename), type: "page" });
44
48
  }
49
+ if (has404) console.log(" built _404.tsx \u2192 pages/_404.mjs");
50
+ if (has500) console.log(" built _500.tsx \u2192 pages/_500.mjs");
45
51
  fs.writeFileSync(
46
52
  path.join(OUT_DIR, "manifest.json"),
47
53
  JSON.stringify({ routes: manifest }, null, 2)
@@ -117,7 +123,31 @@ const server = http.createServer(async (req, res) => {
117
123
  }
118
124
  }
119
125
 
120
- // 2. Route dispatch \u2014 API routes appear before page routes in the manifest
126
+ // 2. Client-side error \u2014 navigate directly to _500 page.
127
+ {
128
+ const qs = new URL(url, 'http://localhost').searchParams;
129
+ if (qs.has('__clientError')) {
130
+ const e500 = path.join(__dirname, 'pages', '_500.mjs');
131
+ if (fs.existsSync(e500)) {
132
+ try {
133
+ const m500 = await import(pathToFileURL(e500).href);
134
+ const eq = new URLSearchParams();
135
+ eq.set('__errorMessage', qs.get('__clientError') || 'Client error');
136
+ const stack = qs.get('__clientStack');
137
+ if (stack) eq.set('__errorStack', stack);
138
+ req.url = '/_500?' + eq.toString();
139
+ await m500.default(req, res);
140
+ return;
141
+ } catch (e) { console.error('[_500 render error]', e); }
142
+ }
143
+ res.statusCode = 500;
144
+ res.setHeader('Content-Type', 'text/plain');
145
+ res.end('Internal Server Error');
146
+ return;
147
+ }
148
+ }
149
+
150
+ // 3. Route dispatch \u2014 API routes appear before page routes in the manifest
121
151
  // (built in build-node.ts), so they are matched first.
122
152
  for (const { regex, paramNames, catchAllNames, handler } of compiled) {
123
153
  const m = clean.match(regex);
@@ -135,11 +165,44 @@ const server = http.createServer(async (req, res) => {
135
165
  });
136
166
  req.url = clean + (qs.toString() ? '?' + qs.toString() : '');
137
167
 
138
- const mod = await import(pathToFileURL(path.join(__dirname, handler)).href);
139
- await mod.default(req, res);
168
+ try {
169
+ const mod = await import(pathToFileURL(path.join(__dirname, handler)).href);
170
+ await mod.default(req, res);
171
+ } catch (err) {
172
+ console.error('[handler error]', err);
173
+ const e500 = path.join(__dirname, 'pages', '_500.mjs');
174
+ if (fs.existsSync(e500)) {
175
+ try {
176
+ const m500 = await import(pathToFileURL(e500).href);
177
+ // Inject error info as query params so _500 page receives them as props.
178
+ const errMsg = err instanceof Error ? err.message : String(err);
179
+ const errStack = err instanceof Error ? err.stack : undefined;
180
+ const errStatus = err?.status ?? err?.statusCode;
181
+ const eq = new URLSearchParams();
182
+ eq.set('__errorMessage', errMsg);
183
+ if (errStack) eq.set('__errorStack', errStack);
184
+ if (errStatus) eq.set('__errorStatus', String(errStatus));
185
+ req.url = '/_500?' + eq.toString();
186
+ await m500.default(req, res);
187
+ return;
188
+ } catch (e) { console.error('[_500 render error]', e); }
189
+ }
190
+ res.statusCode = 500;
191
+ res.setHeader('Content-Type', 'text/plain');
192
+ res.end('Internal Server Error');
193
+ }
140
194
  return;
141
195
  }
142
196
 
197
+ // 3. 404 \u2014 serve _404.mjs if built, otherwise plain text.
198
+ const e404 = path.join(__dirname, 'pages', '_404.mjs');
199
+ if (fs.existsSync(e404)) {
200
+ try {
201
+ const m404 = await import(pathToFileURL(e404).href);
202
+ await m404.default(req, res);
203
+ return;
204
+ } catch (err) { console.error('[_404 render error]', err); }
205
+ }
143
206
  res.statusCode = 404;
144
207
  res.setHeader('Content-Type', 'text/plain');
145
208
  res.end('Not found');
@@ -152,4 +215,3 @@ fs.writeFileSync(path.join(OUT_DIR, "index.mjs"), serverEntry);
152
215
  console.log(`
153
216
  \u2713 Node build complete \u2014 ${manifest.length} route(s) \u2192 dist/`);
154
217
  console.log(" run with: node dist/index.mjs");
155
- //# sourceMappingURL=build-node.js.map
@@ -18,6 +18,10 @@ import {
18
18
  const OUTPUT_DIR = path.resolve(".vercel/output");
19
19
  const FUNCTIONS_DIR = path.join(OUTPUT_DIR, "functions");
20
20
  const STATIC_DIR = path.join(OUTPUT_DIR, "static");
21
+ if (fs.existsSync(OUTPUT_DIR)) {
22
+ fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
23
+ console.log("\u{1F5D1}\uFE0F Cleaned .vercel/output/");
24
+ }
21
25
  for (const dir of [FUNCTIONS_DIR, STATIC_DIR])
22
26
  fs.mkdirSync(dir, { recursive: true });
23
27
  const config = await loadConfig();
@@ -139,13 +143,43 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
139
143
  }
140
144
  `;
141
145
  }
142
- function makePagesDispatcherSource(routes) {
146
+ function makePagesDispatcherSource(routes, errorAdapters = {}) {
143
147
  const imports = routes.map((r, i) => `import __page_${i}__ from ${JSON.stringify(r.adapterPath)};`).join("\n");
144
148
  const routeEntries = routes.map(
145
149
  (r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, catchAll: ${JSON.stringify(r.catchAllNames)}, handler: __page_${i}__ },`
146
150
  ).join("\n");
151
+ const error404Import = errorAdapters.adapter404 ? `import __error_404__ from ${JSON.stringify(errorAdapters.adapter404)};` : "";
152
+ const error500Import = errorAdapters.adapter500 ? `import __error_500__ from ${JSON.stringify(errorAdapters.adapter500)};` : "";
153
+ const notFoundHandler = errorAdapters.adapter404 ? ` try { return await __error_404__(req, res); } catch(e) { console.error('[_404 error]', e); }` : ` res.statusCode = 404;
154
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
155
+ res.end('Not Found');`;
156
+ const clientErrorHandler = errorAdapters.adapter500 ? ` try {
157
+ const eq = new URLSearchParams();
158
+ eq.set('__errorMessage', url.searchParams.get('__clientError') || 'Client error');
159
+ const stack = url.searchParams.get('__clientStack');
160
+ if (stack) eq.set('__errorStack', stack);
161
+ req.url = '/_500?' + eq.toString();
162
+ return await __error_500__(req, res);
163
+ } catch(e) { console.error('[_500 client error]', e); }` : ` res.statusCode = 500;
164
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
165
+ res.end('Internal Server Error');`;
166
+ const errorHandler = errorAdapters.adapter500 ? ` try {
167
+ const errMsg = err instanceof Error ? err.message : String(err);
168
+ const errStack = err instanceof Error ? err.stack : undefined;
169
+ const errStatus = err?.status ?? err?.statusCode;
170
+ const eq = new URLSearchParams();
171
+ eq.set('__errorMessage', errMsg);
172
+ if (errStack) eq.set('__errorStack', errStack);
173
+ if (errStatus) eq.set('__errorStatus', String(errStatus));
174
+ req.url = '/_500?' + eq.toString();
175
+ return await __error_500__(req, res);
176
+ } catch(e) { console.error('[_500 error]', e); }` : ` res.statusCode = 500;
177
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
178
+ res.end('Internal Server Error');`;
147
179
  return `import type { IncomingMessage, ServerResponse } from 'http';
148
180
  ${imports}
181
+ ${error404Import}
182
+ ${error500Import}
149
183
 
150
184
  const ROUTES: Array<{
151
185
  regex: string;
@@ -160,6 +194,11 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
160
194
  const url = new URL(req.url || '/', 'http://localhost');
161
195
  const pathname = url.pathname;
162
196
 
197
+ // Client-side error \u2014 navigate directly to _500 handler.
198
+ if (url.searchParams.has('__clientError')) {
199
+ ${clientErrorHandler}
200
+ }
201
+
163
202
  for (const route of ROUTES) {
164
203
  const m = pathname.match(new RegExp(route.regex));
165
204
  if (!m) continue;
@@ -168,7 +207,6 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
168
207
  route.params.forEach((name, i) => {
169
208
  const raw = m[i + 1] ?? '';
170
209
  if (catchAllSet.has(name)) {
171
- // Encode catch-all as repeated keys so the handler can getAll() \u2192 string[]
172
210
  raw.split('/').filter(Boolean).forEach(seg => url.searchParams.append(name, seg));
173
211
  } else {
174
212
  url.searchParams.set(name, raw);
@@ -176,12 +214,16 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
176
214
  });
177
215
  req.url = pathname + (url.search || '');
178
216
 
179
- return route.handler(req, res);
217
+ try {
218
+ return await route.handler(req, res);
219
+ } catch (err) {
220
+ console.error('[handler error]', err);
221
+ ${errorHandler}
222
+ return;
223
+ }
180
224
  }
181
225
 
182
- res.statusCode = 404;
183
- res.setHeader('Content-Type', 'text/plain; charset=utf-8');
184
- res.end('Not Found');
226
+ ${notFoundHandler}
185
227
  }
186
228
  `;
187
229
  }
@@ -212,7 +254,8 @@ if (apiRoutes.length > 0) {
212
254
  vercelRoutes.push({ src: srcRegex, dest: "/api" });
213
255
  }
214
256
  const serverPages = collectServerPages(PAGES_DIR);
215
- if (serverPages.length > 0) {
257
+ const hasErrorPages = ["_404.tsx", "_500.tsx"].some((f) => fs.existsSync(path.join(PAGES_DIR, f)));
258
+ if (serverPages.length > 0 || hasErrorPages) {
216
259
  const globalRegistry = collectGlobalClientRegistry(serverPages, PAGES_DIR);
217
260
  const prerenderedHtml = await bundleClientComponents(globalRegistry, PAGES_DIR, STATIC_DIR);
218
261
  const prerenderedRecord = Object.fromEntries(prerenderedHtml);
@@ -235,6 +278,7 @@ if (serverPages.length > 0) {
235
278
  allClientIds: [...registry.keys()],
236
279
  layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
237
280
  prerenderedHtml: prerenderedRecord,
281
+ routeParamNames: page.paramNames,
238
282
  catchAllNames: page.catchAllNames
239
283
  })
240
284
  );
@@ -247,8 +291,36 @@ if (serverPages.length > 0) {
247
291
  paramNames: page.paramNames,
248
292
  catchAllNames: page.catchAllNames
249
293
  }));
294
+ const errorAdapters = {};
295
+ const errorAdapterPaths = [];
296
+ for (const [statusCode, key] of [[404, "adapter404"], [500, "adapter500"]]) {
297
+ const src = path.join(PAGES_DIR, `_${statusCode}.tsx`);
298
+ if (!fs.existsSync(src)) continue;
299
+ console.log(` building _${statusCode}.tsx \u2192 pages.func [error page]`);
300
+ const adapterDir = path.dirname(src);
301
+ const adapterPath = path.join(adapterDir, `_error_adapter_${randomBytes(4).toString("hex")}.ts`);
302
+ const layoutPaths = findPageLayouts(src, PAGES_DIR);
303
+ const { registry, clientComponentNames } = buildPerPageRegistry(src, layoutPaths, PAGES_DIR);
304
+ const layoutImports = layoutPaths.map((lp, i) => {
305
+ const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
306
+ return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
307
+ }).join("\n");
308
+ fs.writeFileSync(adapterPath, makePageAdapterSource({
309
+ pageImport: JSON.stringify("./" + path.basename(src)),
310
+ layoutImports,
311
+ clientComponentNames,
312
+ allClientIds: [...registry.keys()],
313
+ layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
314
+ prerenderedHtml: prerenderedRecord,
315
+ routeParamNames: [],
316
+ catchAllNames: [],
317
+ statusCode
318
+ }));
319
+ errorAdapters[key] = adapterPath;
320
+ errorAdapterPaths.push(adapterPath);
321
+ }
250
322
  const dispatcherPath = path.join(PAGES_DIR, `_pages_dispatcher_${randomBytes(4).toString("hex")}.ts`);
251
- fs.writeFileSync(dispatcherPath, makePagesDispatcherSource(dispatcherRoutes));
323
+ fs.writeFileSync(dispatcherPath, makePagesDispatcherSource(dispatcherRoutes, errorAdapters));
252
324
  try {
253
325
  const result = await build({
254
326
  entryPoints: [dispatcherPath],
@@ -267,6 +339,7 @@ if (serverPages.length > 0) {
267
339
  } finally {
268
340
  fs.unlinkSync(dispatcherPath);
269
341
  for (const p of tempAdapterPaths) if (fs.existsSync(p)) fs.unlinkSync(p);
342
+ for (const p of errorAdapterPaths) if (fs.existsSync(p)) fs.unlinkSync(p);
270
343
  }
271
344
  for (const { srcRegex } of serverPages)
272
345
  vercelRoutes.push({ src: srcRegex, dest: "/pages" });
@@ -284,4 +357,3 @@ copyPublicFiles(PUBLIC_DIR, STATIC_DIR);
284
357
  const fnCount = (apiRoutes.length > 0 ? 1 : 0) + (serverPages.length > 0 ? 1 : 0);
285
358
  console.log(`
286
359
  \u2713 Vercel build complete \u2014 ${fnCount} function(s) \u2192 .vercel/output`);
287
- //# sourceMappingURL=build-vercel.js.map
package/dist/builder.js CHANGED
@@ -39,6 +39,33 @@ function processDist(dir) {
39
39
  })(dir);
40
40
  console.log("\u{1F527} Post-processing done: relative imports \u2192 .js extensions.");
41
41
  }
42
+ const PUBLIC_STEMS = /* @__PURE__ */ new Set([
43
+ "index",
44
+ "use-html",
45
+ "use-router",
46
+ "use-request",
47
+ "request-store",
48
+ "Link",
49
+ "bundle",
50
+ "utils",
51
+ "logger"
52
+ ]);
53
+ function prunePrivateDeclarations(dir) {
54
+ (function walk(currentDir) {
55
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
56
+ const fullPath = path.join(currentDir, entry.name);
57
+ if (entry.isDirectory()) {
58
+ walk(fullPath);
59
+ } else if (entry.name.endsWith(".d.ts") || entry.name.endsWith(".d.ts.map")) {
60
+ const stem = entry.name.replace(/\.d\.ts(\.map)?$/, "");
61
+ if (!PUBLIC_STEMS.has(stem)) {
62
+ fs.rmSync(fullPath);
63
+ }
64
+ }
65
+ }
66
+ })(dir);
67
+ console.log("\u2702\uFE0F Pruned private .d.ts files (kept public API only).");
68
+ }
42
69
  async function runBuild() {
43
70
  try {
44
71
  cleanDist(outDir);
@@ -49,13 +76,13 @@ async function runBuild() {
49
76
  platform: "node",
50
77
  format: "esm",
51
78
  target: ["node20"],
52
- packages: "external",
53
- sourcemap: true
79
+ packages: "external"
54
80
  });
55
81
  console.log("\u2705 Build done.");
56
82
  processDist(outDir);
57
83
  console.log("\u{1F4C4} Generating TypeScript declarations\u2026");
58
- execSync("tsc --emitDeclarationOnly --declaration --outDir dist", { stdio: "inherit" });
84
+ execSync("tsc --emitDeclarationOnly --declaration --declarationMap false --outDir dist", { stdio: "inherit" });
85
+ prunePrivateDeclarations(outDir);
59
86
  console.log("\n\u{1F389} Build complete \u2192 dist/");
60
87
  } catch (err) {
61
88
  console.error("\u274C Build failed:", err);
@@ -63,4 +90,3 @@ async function runBuild() {
63
90
  }
64
91
  }
65
92
  runBuild();
66
- //# sourceMappingURL=builder.js.map
package/dist/bundle.d.ts CHANGED
@@ -53,6 +53,13 @@ export interface RuntimeData {
53
53
  allIds: string[];
54
54
  url: string;
55
55
  params: Record<string, any>;
56
+ /** Query string parameters parsed from the URL. Multi-value keys are arrays. */
57
+ query: Record<string, string | string[]>;
58
+ /**
59
+ * Safe subset of the incoming request headers (cookie, authorization, and
60
+ * proxy-authorization are stripped before embedding in the HTML document).
61
+ */
62
+ headers: Record<string, string>;
56
63
  debug: ClientDebugLevel;
57
64
  }
58
65
  /**
package/dist/bundle.js CHANGED
@@ -77,9 +77,38 @@ async function loadModules(ids, log, bust = "") {
77
77
  return mods;
78
78
  }
79
79
  const activeRoots = [];
80
+ let clientErrorPending = false;
81
+ function navigateToClientError(err) {
82
+ if (clientErrorPending) return;
83
+ if (window.location.search.includes("__clientError")) return;
84
+ clientErrorPending = true;
85
+ const message = err instanceof Error ? err.message : String(err);
86
+ const stack = err instanceof Error && err.stack ? err.stack : void 0;
87
+ const params = new URLSearchParams();
88
+ params.set("__clientError", message);
89
+ if (stack) params.set("__clientStack", stack);
90
+ window.dispatchEvent(new CustomEvent("locationchange", {
91
+ detail: { href: window.location.pathname + "?" + params.toString() }
92
+ }));
93
+ }
80
94
  async function mountNodes(mods, log) {
81
95
  const { hydrateRoot, createRoot } = await import("react-dom/client");
82
96
  const React = await import("react");
97
+ class NukeErrorBoundary extends React.default.Component {
98
+ constructor(props) {
99
+ super(props);
100
+ this.state = { caught: false };
101
+ }
102
+ static getDerivedStateFromError() {
103
+ return { caught: true };
104
+ }
105
+ componentDidCatch(error) {
106
+ navigateToClientError(error);
107
+ }
108
+ render() {
109
+ return this.state.caught ? null : this.props.children;
110
+ }
111
+ }
83
112
  const nodes = document.querySelectorAll("[data-hydrate-id]");
84
113
  log.verbose("Found", nodes.length, "hydration point(s)");
85
114
  for (const node of nodes) {
@@ -97,7 +126,8 @@ async function mountNodes(mods, log) {
97
126
  log.error("Props parse error for", id, e);
98
127
  }
99
128
  try {
100
- const element = React.default.createElement(Comp, await reconstructProps(rawProps, mods));
129
+ const inner = React.default.createElement(Comp, await reconstructProps(rawProps, mods));
130
+ const element = React.default.createElement(NukeErrorBoundary, null, inner);
101
131
  let root;
102
132
  if (node.innerHTML.trim()) {
103
133
  root = hydrateRoot(node, element);
@@ -223,8 +253,13 @@ function setupNavigation(log) {
223
253
  const fetchUrl = hmr ? href + (href.includes("?") ? "&" : "?") + "__hmr=1" : href;
224
254
  const response = await fetch(fetchUrl, { headers: { Accept: "text/html" } });
225
255
  if (!response.ok) {
226
- log.error("Navigation fetch failed:", response.status);
227
- return;
256
+ const ct = response.headers.get("content-type") ?? "";
257
+ if (!ct.includes("text/html")) {
258
+ log.error("Navigation fetch failed:", response.status, "\u2014 falling back to full reload");
259
+ window.location.href = href;
260
+ return;
261
+ }
262
+ log.info("Navigation returned", response.status, "\u2014 rendering error page in-place");
228
263
  }
229
264
  const parser = new DOMParser();
230
265
  const doc = parser.parseFromString(await response.text(), "text/html");
@@ -251,6 +286,8 @@ function setupNavigation(log) {
251
286
  } catch (err) {
252
287
  log.error("Navigation error, falling back to full reload:", err);
253
288
  window.location.href = href;
289
+ } finally {
290
+ clientErrorPending = false;
254
291
  }
255
292
  });
256
293
  }
@@ -258,6 +295,13 @@ async function initRuntime(data) {
258
295
  const log = makeLogger(data.debug ?? "silent");
259
296
  log.info("\u{1F680} Partial hydration:", data.hydrateIds.length, "root component(s)");
260
297
  setupNavigation(log);
298
+ window.onerror = (_msg, _src, _line, _col, err) => {
299
+ navigateToClientError(err ?? _msg);
300
+ return true;
301
+ };
302
+ window.onunhandledrejection = (e) => {
303
+ navigateToClientError(e.reason);
304
+ };
261
305
  const mods = await loadModules(data.allIds, log);
262
306
  await mountNodes(mods, log);
263
307
  log.info("\u{1F389} Done!");
@@ -267,4 +311,3 @@ export {
267
311
  initRuntime,
268
312
  setupLocationChangeMonitor
269
313
  };
270
- //# sourceMappingURL=bundle.js.map
package/dist/bundler.js CHANGED
@@ -97,4 +97,3 @@ export {
97
97
  serveNukeBundle,
98
98
  serveReactBundle
99
99
  };
100
- //# sourceMappingURL=bundler.js.map
@@ -123,4 +123,3 @@ export {
123
123
  getComponentCache,
124
124
  invalidateComponentCache
125
125
  };
126
- //# sourceMappingURL=component-analyzer.js.map
package/dist/config.js CHANGED
@@ -27,4 +27,3 @@ async function loadConfig() {
27
27
  export {
28
28
  loadConfig
29
29
  };
30
- //# sourceMappingURL=config.js.map
@@ -86,4 +86,3 @@ hmr();
86
86
  export {
87
87
  hmr as default
88
88
  };
89
- //# sourceMappingURL=hmr-bundle.js.map
package/dist/hmr.js CHANGED
@@ -24,6 +24,10 @@ function pageFileToUrl(filename) {
24
24
  function buildPayload(filename) {
25
25
  const normalized = filename.replace(/\\/g, "/");
26
26
  if (normalized.startsWith("pages/")) {
27
+ const stem = path.basename(normalized, path.extname(normalized));
28
+ if (stem === "_404" || stem === "_500") {
29
+ return { type: "replace", component: stem };
30
+ }
27
31
  const url = pageFileToUrl(normalized);
28
32
  return { type: "reload", url };
29
33
  }
@@ -59,4 +63,3 @@ export {
59
63
  hmrClients,
60
64
  watchDir
61
65
  };
62
- //# sourceMappingURL=hmr.js.map
@@ -39,4 +39,3 @@ export {
39
39
  resolveTitle,
40
40
  runWithHtmlStore
41
41
  };
42
- //# sourceMappingURL=html-store.js.map
@@ -170,4 +170,3 @@ export {
170
170
  parseBody,
171
171
  parseQuery
172
172
  };
173
- //# sourceMappingURL=http-server.js.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export { useHtml } from './use-html';
2
2
  export type { HtmlOptions, TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag, } from './use-html';
3
3
  export { default as useRouter } from './use-router';
4
+ export { useRequest } from './use-request';
5
+ export type { RequestContext } from './use-request';
6
+ export { normaliseHeaders, sanitiseHeaders } from './request-store';
4
7
  export { default as Link } from './Link';
5
8
  export { setupLocationChangeMonitor, initRuntime } from './bundle';
6
9
  export type { RuntimeData } from './bundle';
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { useHtml } from "./use-html.js";
2
2
  import { default as default2 } from "./use-router.js";
3
+ import { useRequest } from "./use-request.js";
4
+ import { normaliseHeaders, sanitiseHeaders } from "./request-store.js";
3
5
  import { default as default3 } from "./Link.js";
4
6
  import { setupLocationChangeMonitor, initRuntime } from "./bundle.js";
5
7
  import { escapeHtml } from "./utils.js";
@@ -12,9 +14,11 @@ export {
12
14
  getDebugLevel,
13
15
  initRuntime,
14
16
  log,
17
+ normaliseHeaders,
18
+ sanitiseHeaders,
15
19
  setDebugLevel,
16
20
  setupLocationChangeMonitor,
17
21
  useHtml,
22
+ useRequest,
18
23
  default2 as useRouter
19
24
  };
20
- //# sourceMappingURL=index.js.map
package/dist/logger.js CHANGED
@@ -50,4 +50,3 @@ export {
50
50
  log,
51
51
  setDebugLevel
52
52
  };
53
- //# sourceMappingURL=logger.js.map
package/dist/metadata.js CHANGED
@@ -40,4 +40,3 @@ export {
40
40
  renderScriptTag,
41
41
  renderStyleTag
42
42
  };
43
- //# sourceMappingURL=metadata.js.map
@@ -47,4 +47,3 @@ export {
47
47
  loadMiddleware,
48
48
  runMiddleware
49
49
  };
50
- //# sourceMappingURL=middleware-loader.js.map
@@ -55,4 +55,3 @@ function isRateLimited(ip) {
55
55
  export {
56
56
  middleware as default
57
57
  };
58
- //# sourceMappingURL=middleware.example.js.map
@@ -69,4 +69,3 @@ async function middleware(req, res) {
69
69
  export {
70
70
  middleware as default
71
71
  };
72
- //# sourceMappingURL=middleware.js.map
package/dist/renderer.js CHANGED
@@ -96,14 +96,9 @@ async function renderFunctionComponent(type, props, ctx) {
96
96
  return `<div style="color:red">Error rendering client component: ${escapeHtml(String(err))}</div>`;
97
97
  }
98
98
  }
99
- try {
100
- const result = type(props);
101
- const resolved = result?.then ? await result : result;
102
- return renderElementToHtml(resolved, ctx);
103
- } catch (err) {
104
- log.error("Error rendering component:", err);
105
- return `<div style="color:red">Error rendering component: ${escapeHtml(String(err))}</div>`;
106
- }
99
+ const result = type(props);
100
+ const resolved = result?.then ? await result : result;
101
+ return renderElementToHtml(resolved, ctx);
107
102
  }
108
103
  function serializePropsForHydration(props, registry) {
109
104
  if (!props || typeof props !== "object") return props;
@@ -153,4 +148,3 @@ function serializeReactElement(element, registry) {
153
148
  export {
154
149
  renderElementToHtml
155
150
  };
156
- //# sourceMappingURL=renderer.js.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * request-store.ts — Per-Request Server Context Store
3
+ *
4
+ * Provides a request-scoped store that server components can read via
5
+ * `useRequest()` during SSR. The store is populated by the SSR pipeline
6
+ * before rendering and cleared in a `finally` block after — preventing any
7
+ * cross-request contamination.
8
+ *
9
+ * Why globalThis?
10
+ * Node's module system may import this file multiple times when the page
11
+ * module and the nukejs package resolve to different copies (common in dev
12
+ * with tsx/tsImport). Using a well-known Symbol on globalThis guarantees
13
+ * all copies share the same store instance, exactly like html-store.ts.
14
+ *
15
+ * Request isolation:
16
+ * runWithRequestStore() creates a fresh store before rendering and clears
17
+ * it in the `finally` block, so concurrent requests cannot bleed into each
18
+ * other even if rendering throws.
19
+ *
20
+ * Headers in __n_data:
21
+ * A safe subset of headers is embedded in the HTML `__n_data` blob so
22
+ * client components can read them after hydration. Sensitive headers
23
+ * (cookie, authorization, proxy-authorization) are intentionally excluded
24
+ * from the client payload. The server-side store always has ALL headers.
25
+ */
26
+ export interface RequestContext {
27
+ /** Full URL with query string (e.g. '/blog/hello?lang=en'). */
28
+ url: string;
29
+ /** Pathname only, no query string (e.g. '/blog/hello'). */
30
+ pathname: string;
31
+ /**
32
+ * Dynamic route segments matched by the file-system router.
33
+ * e.g. for `/blog/[slug]` → `{ slug: 'hello' }`
34
+ */
35
+ params: Record<string, string | string[]>;
36
+ /**
37
+ * Query string parameters, parsed from the URL.
38
+ * Multi-value params (e.g. `?tag=a&tag=b`) become arrays.
39
+ * e.g. `{ lang: 'en', tag: ['a', 'b'] }`
40
+ */
41
+ query: Record<string, string | string[]>;
42
+ /**
43
+ * Incoming request headers.
44
+ *
45
+ * Server-side (SSR): all headers from IncomingMessage.headers.
46
+ * Client-side: safe subset embedded in __n_data (cookie, authorization,
47
+ * proxy-authorization are stripped before serialisation).
48
+ *
49
+ * Multi-value headers are joined with ', '.
50
+ */
51
+ headers: Record<string, string>;
52
+ }
53
+ /**
54
+ * Normalises raw Node `IncomingMessage.headers` into a flat `Record<string,string>`.
55
+ * Array values (multi-value headers) are joined with `', '`.
56
+ * Undefined values are dropped.
57
+ *
58
+ * Used server-side so all headers — including cookies and auth tokens — are
59
+ * available to server components that need them.
60
+ */
61
+ export declare function normaliseHeaders(raw: Record<string, string | string[] | undefined>): Record<string, string>;
62
+ /**
63
+ * Same as `normaliseHeaders` but additionally strips headers that must never
64
+ * appear in a serialised HTML document. Used when embedding headers in the
65
+ * `__n_data` blob so credentials cannot leak into cached or logged HTML pages.
66
+ */
67
+ export declare function sanitiseHeaders(raw: Record<string, string | string[] | undefined>): Record<string, string>;
68
+ /**
69
+ * Runs `fn` inside the context of the given request, then clears the store.
70
+ *
71
+ * Usage in the SSR pipeline:
72
+ * ```ts
73
+ * const store = await runWithRequestStore(ctx, async () => {
74
+ * appHtml = await renderElementToHtml(element, renderCtx);
75
+ * });
76
+ * ```
77
+ */
78
+ export declare function runWithRequestStore<T>(ctx: RequestContext, fn: () => Promise<T>): Promise<T>;
79
+ /**
80
+ * Returns the current request context, or `null` when called outside of
81
+ * an active `runWithRequestStore` scope (e.g. in the browser, in tests,
82
+ * or in a client component).
83
+ */
84
+ export declare function getRequestStore(): RequestContext | null;