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.
- package/README.md +283 -1
- package/dist/Link.d.ts +8 -2
- package/dist/Link.js +2 -3
- package/dist/app.js +1 -2
- package/dist/build-common.js +141 -24
- package/dist/build-node.js +67 -5
- package/dist/build-vercel.js +81 -9
- package/dist/builder.js +30 -4
- package/dist/bundle.d.ts +7 -0
- package/dist/bundle.js +47 -4
- package/dist/bundler.js +0 -1
- package/dist/component-analyzer.js +0 -1
- package/dist/config.js +0 -1
- package/dist/hmr-bundle.js +0 -1
- package/dist/hmr.js +4 -1
- package/dist/html-store.js +0 -1
- package/dist/http-server.js +0 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/logger.js +0 -1
- package/dist/metadata.js +0 -1
- package/dist/middleware-loader.js +0 -1
- package/dist/middleware.example.js +0 -1
- package/dist/middleware.js +0 -1
- package/dist/renderer.js +3 -9
- package/dist/request-store.d.ts +84 -0
- package/dist/request-store.js +46 -0
- package/dist/router.js +0 -1
- package/dist/ssr.js +91 -19
- package/dist/use-html.js +0 -1
- package/dist/use-request.d.ts +74 -0
- package/dist/use-request.js +48 -0
- package/dist/use-router.js +0 -1
- package/dist/utils.js +0 -1
- package/package.json +1 -1
- package/dist/Link.js.map +0 -7
- package/dist/app.d.ts +0 -19
- package/dist/app.js.map +0 -7
- package/dist/build-common.d.ts +0 -172
- package/dist/build-common.js.map +0 -7
- package/dist/build-node.d.ts +0 -15
- package/dist/build-node.js.map +0 -7
- package/dist/build-vercel.d.ts +0 -19
- package/dist/build-vercel.js.map +0 -7
- package/dist/builder.d.ts +0 -11
- package/dist/builder.js.map +0 -7
- package/dist/bundle.js.map +0 -7
- package/dist/bundler.d.ts +0 -58
- package/dist/bundler.js.map +0 -7
- package/dist/component-analyzer.d.ts +0 -75
- package/dist/component-analyzer.js.map +0 -7
- package/dist/config.d.ts +0 -35
- package/dist/config.js.map +0 -7
- package/dist/hmr-bundle.d.ts +0 -25
- package/dist/hmr-bundle.js.map +0 -7
- package/dist/hmr.d.ts +0 -55
- package/dist/hmr.js.map +0 -7
- package/dist/html-store.d.ts +0 -128
- package/dist/html-store.js.map +0 -7
- package/dist/http-server.d.ts +0 -92
- package/dist/http-server.js.map +0 -7
- package/dist/index.js.map +0 -7
- package/dist/logger.js.map +0 -7
- package/dist/metadata.d.ts +0 -50
- package/dist/metadata.js.map +0 -7
- package/dist/middleware-loader.d.ts +0 -50
- package/dist/middleware-loader.js.map +0 -7
- package/dist/middleware.d.ts +0 -22
- package/dist/middleware.example.d.ts +0 -8
- package/dist/middleware.example.js.map +0 -7
- package/dist/middleware.js.map +0 -7
- package/dist/renderer.d.ts +0 -44
- package/dist/renderer.js.map +0 -7
- package/dist/router.d.ts +0 -89
- package/dist/router.js.map +0 -7
- package/dist/ssr.d.ts +0 -45
- package/dist/ssr.js.map +0 -7
- package/dist/use-html.js.map +0 -7
- package/dist/use-router.js.map +0 -7
- package/dist/utils.js.map +0 -7
package/dist/build-node.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
139
|
-
|
|
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
|
package/dist/build-vercel.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
227
|
-
|
|
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
package/dist/config.js
CHANGED
package/dist/hmr-bundle.js
CHANGED
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
|
package/dist/html-store.js
CHANGED
package/dist/http-server.js
CHANGED
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
package/dist/metadata.js
CHANGED
package/dist/middleware.js
CHANGED
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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;
|