nukejs 0.0.11 → 0.0.13
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 +147 -0
- package/dist/Link.js +0 -1
- package/dist/app.js +0 -1
- package/dist/build-common.js +64 -14
- package/dist/build-node.js +63 -5
- package/dist/build-vercel.js +76 -9
- package/dist/builder.js +32 -4
- 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 +10 -1
- package/dist/hmr.js +7 -1
- package/dist/html-store.js +0 -1
- package/dist/http-server.js +0 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +0 -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.js +0 -1
- package/dist/router.js +0 -1
- package/dist/ssr.js +73 -16
- package/dist/use-html.js +0 -1
- package/dist/use-request.js +0 -1
- 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 -178
- 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.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 -51
- 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/request-store.js.map +0 -7
- package/dist/router.d.ts +0 -92
- package/dist/router.js.map +0 -7
- package/dist/ssr.d.ts +0 -46
- package/dist/ssr.js.map +0 -7
- package/dist/use-html.js.map +0 -7
- package/dist/use-request.js.map +0 -7
- package/dist/use-router.js.map +0 -7
- package/dist/utils.js.map +0 -7
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ npm create nuke@latest
|
|
|
24
24
|
- [Configuration](#configuration)
|
|
25
25
|
- [Link Component & Navigation](#link-component--navigation)
|
|
26
26
|
- [useRequest() — URL Params, Query & Headers](#userequest--url-params-query--headers)
|
|
27
|
+
- [Error Pages](#error-pages)
|
|
27
28
|
- [Building & Deploying](#building--deploying)
|
|
28
29
|
|
|
29
30
|
## Overview
|
|
@@ -714,6 +715,152 @@ Changing `?lang=fr` in the URL re-renders client components automatically.
|
|
|
714
715
|
|
|
715
716
|
---
|
|
716
717
|
|
|
718
|
+
## Error Pages
|
|
719
|
+
|
|
720
|
+
NukeJS supports custom error pages for **404 Not Found** and **500 Internal Server Error**. Place them directly in `app/pages/` — they are standard server components and support everything regular pages do: layouts, `useHtml()`, client components, and HMR in dev.
|
|
721
|
+
|
|
722
|
+
### `_404.tsx` — Page Not Found
|
|
723
|
+
|
|
724
|
+
Rendered when no route matches the requested URL.
|
|
725
|
+
|
|
726
|
+
```tsx
|
|
727
|
+
// app/pages/_404.tsx
|
|
728
|
+
import { useHtml } from 'nukejs';
|
|
729
|
+
import { Link } from 'nukejs';
|
|
730
|
+
|
|
731
|
+
export default function NotFound() {
|
|
732
|
+
useHtml({ title: 'Page Not Found' });
|
|
733
|
+
|
|
734
|
+
return (
|
|
735
|
+
<main>
|
|
736
|
+
<h1>404 — Page Not Found</h1>
|
|
737
|
+
<p>The page you're looking for doesn't exist.</p>
|
|
738
|
+
<Link href="/">Go home</Link>
|
|
739
|
+
</main>
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### `_500.tsx` — Internal Server Error
|
|
745
|
+
|
|
746
|
+
Rendered when a page handler throws an unhandled error. The error is passed as optional props so you can display details in development.
|
|
747
|
+
|
|
748
|
+
```tsx
|
|
749
|
+
// app/pages/_500.tsx
|
|
750
|
+
import { useHtml } from 'nukejs';
|
|
751
|
+
import { Link } from 'nukejs';
|
|
752
|
+
|
|
753
|
+
interface ErrorProps {
|
|
754
|
+
errorMessage?: string; // human-readable error description
|
|
755
|
+
errorStatus?: string; // HTTP status code if present on the thrown error
|
|
756
|
+
errorStack?: string; // stack trace — only populated in development
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export default function ServerError({ errorMessage, errorStack }: ErrorProps) {
|
|
760
|
+
useHtml({ title: 'Something went wrong' });
|
|
761
|
+
|
|
762
|
+
return (
|
|
763
|
+
<main>
|
|
764
|
+
<h1>500 — Server Error</h1>
|
|
765
|
+
<p>Something went wrong on our end. Please try again.</p>
|
|
766
|
+
{errorMessage && <p><strong>{errorMessage}</strong></p>}
|
|
767
|
+
{errorStack && <pre>{errorStack}</pre>}
|
|
768
|
+
<Link href="/">Go home</Link>
|
|
769
|
+
</main>
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### Server errors
|
|
775
|
+
|
|
776
|
+
Any unhandled throw inside a server page component (including async data fetching) routes to `_500.tsx`. The error message and stack trace are forwarded as props automatically.
|
|
777
|
+
|
|
778
|
+
```tsx
|
|
779
|
+
// app/pages/dashboard.tsx
|
|
780
|
+
export default async function Dashboard() {
|
|
781
|
+
const data = await fetchData(); // throws → _500.tsx is rendered
|
|
782
|
+
return <main>{data.name}</main>;
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
You can also attach a `status` property to a thrown error to control the HTTP status code sent with the response:
|
|
787
|
+
|
|
788
|
+
```tsx
|
|
789
|
+
export default async function Post({ id }: { id: string }) {
|
|
790
|
+
const post = await db.getPost(id);
|
|
791
|
+
|
|
792
|
+
if (!post) {
|
|
793
|
+
const err = new Error('Post not found');
|
|
794
|
+
(err as any).status = 404;
|
|
795
|
+
throw err; // _500.tsx receives errorMessage="Post not found", errorStatus="404"
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return <article>{post.title}</article>;
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
### Client errors
|
|
803
|
+
|
|
804
|
+
Unhandled errors in client components and async code are automatically caught and routed to `_500.tsx` via an in-place SPA navigation — no full page reload. Three mechanisms cover all cases:
|
|
805
|
+
|
|
806
|
+
- **React Error Boundary** — wraps every hydrated `"use client"` component; catches render and lifecycle errors
|
|
807
|
+
- **`window.onerror`** — catches synchronous throws in event handlers and other non-React code
|
|
808
|
+
- **`window.onunhandledrejection`** — catches unhandled `Promise` rejections from `async` functions
|
|
809
|
+
|
|
810
|
+
```tsx
|
|
811
|
+
// app/components/FaultyButton.tsx
|
|
812
|
+
"use client";
|
|
813
|
+
|
|
814
|
+
export default function FaultyButton() {
|
|
815
|
+
const handleClick = () => {
|
|
816
|
+
throw new Error('Something broke!'); // caught by window.onerror → _500.tsx
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
return <button onClick={handleClick}>Click me</button>;
|
|
820
|
+
}
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
```tsx
|
|
824
|
+
// app/components/FaultyFetch.tsx
|
|
825
|
+
"use client";
|
|
826
|
+
import { useEffect } from 'react';
|
|
827
|
+
|
|
828
|
+
export default function FaultyFetch() {
|
|
829
|
+
useEffect(() => {
|
|
830
|
+
// Unhandled rejection caught by window.onunhandledrejection → _500.tsx
|
|
831
|
+
fetch('/api/broken').then(res => {
|
|
832
|
+
if (!res.ok) throw new Error(`API error ${res.status}`);
|
|
833
|
+
});
|
|
834
|
+
}, []);
|
|
835
|
+
|
|
836
|
+
return <div>Loading...</div>;
|
|
837
|
+
}
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
The `_500.tsx` page receives `errorMessage` and `errorStack` props from client errors just like server errors, so a single error page handles both origins consistently.
|
|
841
|
+
|
|
842
|
+
### Behaviour
|
|
843
|
+
|
|
844
|
+
| Scenario | Without `_500.tsx` | With `_500.tsx` |
|
|
845
|
+
|---|---|---|
|
|
846
|
+
| Server page throws | Plain-text `Internal Server Error` (500) | `_500.tsx` rendered with error props |
|
|
847
|
+
| Client component render error | React crashes the component subtree | `_500.tsx` rendered in-place, no reload |
|
|
848
|
+
| Unhandled event handler throw | Browser console error only | `_500.tsx` rendered in-place, no reload |
|
|
849
|
+
| Unhandled promise rejection | Browser console error only | `_500.tsx` rendered in-place, no reload |
|
|
850
|
+
| Unknown URL | Plain-text `Page not found` (404) | `_404.tsx` rendered with 404 status |
|
|
851
|
+
| `<Link>` to unknown URL | Full page reload | In-place SPA navigation, no reload |
|
|
852
|
+
| HMR save of `_404.tsx` / `_500.tsx` | — | Current page re-fetches immediately |
|
|
853
|
+
|
|
854
|
+
### Notes
|
|
855
|
+
|
|
856
|
+
- Error pages are **excluded from routing** — `/_404` and `/_500` are not reachable as URLs.
|
|
857
|
+
- They participate in the root `layout.tsx` like any other page.
|
|
858
|
+
- Both are fully bundled into the production output (Node.js and Vercel) — no runtime file-system access required.
|
|
859
|
+
- The correct HTTP status code (404 or 500) is always sent in the response.
|
|
860
|
+
- `errorStack` is only populated in development (`NODE_ENV !== 'production'`).
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
717
864
|
## Building & Deploying
|
|
718
865
|
|
|
719
866
|
### Node.js server
|
package/dist/Link.js
CHANGED
package/dist/app.js
CHANGED
package/dist/build-common.js
CHANGED
|
@@ -147,7 +147,8 @@ function extractDefaultExportName(filePath) {
|
|
|
147
147
|
function collectServerPages(pagesDir) {
|
|
148
148
|
if (!fs.existsSync(pagesDir)) return [];
|
|
149
149
|
return walkFiles(pagesDir).filter((relPath) => {
|
|
150
|
-
|
|
150
|
+
const stem = path.basename(relPath, path.extname(relPath));
|
|
151
|
+
if (stem === "layout" || stem === "_404" || stem === "_500") return false;
|
|
151
152
|
return isServerComponent(path.join(pagesDir, relPath));
|
|
152
153
|
}).map((relPath) => ({
|
|
153
154
|
...analyzeFile(relPath, "page"),
|
|
@@ -163,6 +164,15 @@ function collectGlobalClientRegistry(serverPages, pagesDir) {
|
|
|
163
164
|
for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))
|
|
164
165
|
registry.set(id, p);
|
|
165
166
|
}
|
|
167
|
+
for (const stem of ["_404", "_500"]) {
|
|
168
|
+
const errorFile = path.join(pagesDir, `${stem}.tsx`);
|
|
169
|
+
if (!fs.existsSync(errorFile)) continue;
|
|
170
|
+
for (const [id, p] of findClientComponentsInTree(errorFile, pagesDir))
|
|
171
|
+
registry.set(id, p);
|
|
172
|
+
for (const layoutPath of findPageLayouts(errorFile, pagesDir))
|
|
173
|
+
for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))
|
|
174
|
+
registry.set(id, p);
|
|
175
|
+
}
|
|
166
176
|
return registry;
|
|
167
177
|
}
|
|
168
178
|
function buildPerPageRegistry(absPath, layoutPaths, pagesDir) {
|
|
@@ -179,12 +189,15 @@ function buildPerPageRegistry(absPath, layoutPaths, pagesDir) {
|
|
|
179
189
|
}
|
|
180
190
|
return { registry, clientComponentNames };
|
|
181
191
|
}
|
|
182
|
-
async function buildPages(pagesDir, staticDir) {
|
|
192
|
+
async function buildPages(pagesDir, staticDir, outPagesDir) {
|
|
183
193
|
const serverPages = collectServerPages(pagesDir);
|
|
184
194
|
if (fs.existsSync(pagesDir) && walkFiles(pagesDir).length > 0 && serverPages.length === 0) {
|
|
185
195
|
console.warn(`\u26A0 Pages found in ${pagesDir} but none are server components`);
|
|
186
196
|
}
|
|
187
|
-
if (serverPages.length === 0)
|
|
197
|
+
if (serverPages.length === 0) {
|
|
198
|
+
const errorResult2 = outPagesDir ? await buildErrorPages(pagesDir, outPagesDir, {}) : { has404: false, has500: false };
|
|
199
|
+
return { pages: [], ...errorResult2 };
|
|
200
|
+
}
|
|
188
201
|
const globalRegistry = collectGlobalClientRegistry(serverPages, pagesDir);
|
|
189
202
|
const prerenderedHtml = await bundleClientComponents(globalRegistry, pagesDir, staticDir);
|
|
190
203
|
const prerenderedRecord = Object.fromEntries(prerenderedHtml);
|
|
@@ -205,7 +218,8 @@ async function buildPages(pagesDir, staticDir) {
|
|
|
205
218
|
});
|
|
206
219
|
builtPages.push({ ...page, bundleText });
|
|
207
220
|
}
|
|
208
|
-
|
|
221
|
+
const errorResult = outPagesDir ? await buildErrorPages(pagesDir, outPagesDir, prerenderedRecord) : { has404: false, has500: false };
|
|
222
|
+
return { pages: builtPages, ...errorResult };
|
|
209
223
|
}
|
|
210
224
|
function makeApiAdapterSource(handlerFilename) {
|
|
211
225
|
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
@@ -265,7 +279,8 @@ function makePageAdapterSource(opts) {
|
|
|
265
279
|
layoutArrayItems,
|
|
266
280
|
prerenderedHtml,
|
|
267
281
|
routeParamNames,
|
|
268
|
-
catchAllNames
|
|
282
|
+
catchAllNames,
|
|
283
|
+
statusCode = 200
|
|
269
284
|
} = opts;
|
|
270
285
|
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
271
286
|
import { createElement as __createElement__ } from 'react';
|
|
@@ -547,7 +562,15 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
547
562
|
|
|
548
563
|
const hydrated = new Set<string>();
|
|
549
564
|
// Merge query params into page props to match dev behaviour (ssr.ts mergedParams).
|
|
550
|
-
|
|
565
|
+
// Error props (__errorMessage, __errorStack, __errorStatus) are injected by the
|
|
566
|
+
// server entry when routing to _500.mjs after a handler failure.
|
|
567
|
+
const errorProps: Record<string, string | undefined> = {};
|
|
568
|
+
const ep = parsed.searchParams;
|
|
569
|
+
if (ep.has('__errorMessage')) errorProps.errorMessage = ep.get('__errorMessage') ?? undefined;
|
|
570
|
+
if (ep.has('__errorStack')) errorProps.errorStack = ep.get('__errorStack') ?? undefined;
|
|
571
|
+
if (ep.has('__errorStatus')) errorProps.errorStatus = ep.get('__errorStatus') ?? undefined;
|
|
572
|
+
|
|
573
|
+
const merged = { ...query, ...params, ...errorProps } as any;
|
|
551
574
|
const wrapped = wrapWithLayouts({ type: __page__.default, props: merged, key: null, ref: null });
|
|
552
575
|
|
|
553
576
|
let appHtml = '';
|
|
@@ -611,14 +634,13 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
611
634
|
\${bodyScriptsHtml}</body>
|
|
612
635
|
</html>\`;
|
|
613
636
|
|
|
614
|
-
res.statusCode =
|
|
637
|
+
res.statusCode = ${statusCode};
|
|
615
638
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
616
639
|
res.end(html);
|
|
617
640
|
} catch (err: any) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
res.end('Internal Server Error');
|
|
641
|
+
// Re-throw so the server entry (build-node / build-vercel) can route to
|
|
642
|
+
// the _500 page handler. Do not swallow the error here.
|
|
643
|
+
throw err;
|
|
622
644
|
}
|
|
623
645
|
}
|
|
624
646
|
`;
|
|
@@ -652,7 +674,8 @@ async function bundlePageHandler(opts) {
|
|
|
652
674
|
layoutPaths,
|
|
653
675
|
prerenderedHtml,
|
|
654
676
|
routeParamNames,
|
|
655
|
-
catchAllNames
|
|
677
|
+
catchAllNames,
|
|
678
|
+
statusCode
|
|
656
679
|
} = opts;
|
|
657
680
|
const adapterDir = path.dirname(absPath);
|
|
658
681
|
const adapterPath = path.join(adapterDir, `_page_adapter_${randomBytes(4).toString("hex")}.ts`);
|
|
@@ -668,7 +691,8 @@ async function bundlePageHandler(opts) {
|
|
|
668
691
|
layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
|
|
669
692
|
prerenderedHtml,
|
|
670
693
|
routeParamNames,
|
|
671
|
-
catchAllNames
|
|
694
|
+
catchAllNames,
|
|
695
|
+
statusCode
|
|
672
696
|
}));
|
|
673
697
|
let text;
|
|
674
698
|
try {
|
|
@@ -740,6 +764,32 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
|
|
|
740
764
|
console.log(` bundled ${globalRegistry.size} client component(s) \u2192 ${path.relative(process.cwd(), outDir)}/`);
|
|
741
765
|
return prerendered;
|
|
742
766
|
}
|
|
767
|
+
async function buildErrorPages(pagesDir, outPagesDir, prerenderedHtml) {
|
|
768
|
+
const result = { has404: false, has500: false };
|
|
769
|
+
for (const statusCode of [404, 500]) {
|
|
770
|
+
const src = path.join(pagesDir, `_${statusCode}.tsx`);
|
|
771
|
+
if (!fs.existsSync(src)) continue;
|
|
772
|
+
console.log(` building _${statusCode}.tsx \u2192 pages/_${statusCode}.mjs`);
|
|
773
|
+
const layoutPaths = findPageLayouts(src, pagesDir);
|
|
774
|
+
const { registry, clientComponentNames } = buildPerPageRegistry(src, layoutPaths, pagesDir);
|
|
775
|
+
const bundleText = await bundlePageHandler({
|
|
776
|
+
absPath: src,
|
|
777
|
+
pagesDir,
|
|
778
|
+
clientComponentNames,
|
|
779
|
+
allClientIds: [...registry.keys()],
|
|
780
|
+
layoutPaths,
|
|
781
|
+
prerenderedHtml,
|
|
782
|
+
routeParamNames: [],
|
|
783
|
+
catchAllNames: [],
|
|
784
|
+
statusCode
|
|
785
|
+
});
|
|
786
|
+
fs.mkdirSync(outPagesDir, { recursive: true });
|
|
787
|
+
fs.writeFileSync(path.join(outPagesDir, `_${statusCode}.mjs`), bundleText);
|
|
788
|
+
if (statusCode === 404) result.has404 = true;
|
|
789
|
+
if (statusCode === 500) result.has500 = true;
|
|
790
|
+
}
|
|
791
|
+
return result;
|
|
792
|
+
}
|
|
743
793
|
async function buildCombinedBundle(staticDir) {
|
|
744
794
|
const nukeDir = path.dirname(fileURLToPath(import.meta.url));
|
|
745
795
|
const bundleFile = nukeDir.endsWith("dist") ? "bundle" : "bundle.ts";
|
|
@@ -806,6 +856,7 @@ function copyPublicFiles(publicDir, destDir) {
|
|
|
806
856
|
export {
|
|
807
857
|
analyzeFile,
|
|
808
858
|
buildCombinedBundle,
|
|
859
|
+
buildErrorPages,
|
|
809
860
|
buildPages,
|
|
810
861
|
buildPerPageRegistry,
|
|
811
862
|
bundleApiHandler,
|
|
@@ -821,4 +872,3 @@ export {
|
|
|
821
872
|
makePageAdapterSource,
|
|
822
873
|
walkFiles
|
|
823
874
|
};
|
|
824
|
-
//# sourceMappingURL=build-common.js.map
|
package/dist/build-node.js
CHANGED
|
@@ -38,7 +38,7 @@ for (const { srcRegex, paramNames, catchAllNames, funcPath, absPath } of apiRout
|
|
|
38
38
|
fs.writeFileSync(outPath, await bundleApiHandler(absPath));
|
|
39
39
|
manifest.push({ srcRegex, paramNames, catchAllNames, handler: path.join("api", filename), type: "api" });
|
|
40
40
|
}
|
|
41
|
-
const builtPages = await buildPages(PAGES_DIR, STATIC_DIR);
|
|
41
|
+
const { pages: builtPages, has404, has500 } = await buildPages(PAGES_DIR, STATIC_DIR, PAGES_DIR_);
|
|
42
42
|
for (const { srcRegex, paramNames, catchAllNames, funcPath, bundleText } of builtPages) {
|
|
43
43
|
const filename = funcPathToFilename(funcPath, "page");
|
|
44
44
|
const outPath = path.join(PAGES_DIR_, filename);
|
|
@@ -46,6 +46,8 @@ for (const { srcRegex, paramNames, catchAllNames, funcPath, bundleText } of buil
|
|
|
46
46
|
fs.writeFileSync(outPath, bundleText);
|
|
47
47
|
manifest.push({ srcRegex, paramNames, catchAllNames, handler: path.join("pages", filename), type: "page" });
|
|
48
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");
|
|
49
51
|
fs.writeFileSync(
|
|
50
52
|
path.join(OUT_DIR, "manifest.json"),
|
|
51
53
|
JSON.stringify({ routes: manifest }, null, 2)
|
|
@@ -121,7 +123,31 @@ const server = http.createServer(async (req, res) => {
|
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
|
|
124
|
-
// 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
|
|
125
151
|
// (built in build-node.ts), so they are matched first.
|
|
126
152
|
for (const { regex, paramNames, catchAllNames, handler } of compiled) {
|
|
127
153
|
const m = clean.match(regex);
|
|
@@ -139,11 +165,44 @@ const server = http.createServer(async (req, res) => {
|
|
|
139
165
|
});
|
|
140
166
|
req.url = clean + (qs.toString() ? '?' + qs.toString() : '');
|
|
141
167
|
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
}
|
|
144
194
|
return;
|
|
145
195
|
}
|
|
146
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
|
+
}
|
|
147
206
|
res.statusCode = 404;
|
|
148
207
|
res.setHeader('Content-Type', 'text/plain');
|
|
149
208
|
res.end('Not found');
|
|
@@ -156,4 +215,3 @@ fs.writeFileSync(path.join(OUT_DIR, "index.mjs"), serverEntry);
|
|
|
156
215
|
console.log(`
|
|
157
216
|
\u2713 Node build complete \u2014 ${manifest.length} route(s) \u2192 dist/`);
|
|
158
217
|
console.log(" run with: node dist/index.mjs");
|
|
159
|
-
//# sourceMappingURL=build-node.js.map
|
package/dist/build-vercel.js
CHANGED
|
@@ -143,13 +143,43 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
143
143
|
}
|
|
144
144
|
`;
|
|
145
145
|
}
|
|
146
|
-
function makePagesDispatcherSource(routes) {
|
|
146
|
+
function makePagesDispatcherSource(routes, errorAdapters = {}) {
|
|
147
147
|
const imports = routes.map((r, i) => `import __page_${i}__ from ${JSON.stringify(r.adapterPath)};`).join("\n");
|
|
148
148
|
const routeEntries = routes.map(
|
|
149
149
|
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, catchAll: ${JSON.stringify(r.catchAllNames)}, handler: __page_${i}__ },`
|
|
150
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');`;
|
|
151
179
|
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
152
180
|
${imports}
|
|
181
|
+
${error404Import}
|
|
182
|
+
${error500Import}
|
|
153
183
|
|
|
154
184
|
const ROUTES: Array<{
|
|
155
185
|
regex: string;
|
|
@@ -164,6 +194,11 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
164
194
|
const url = new URL(req.url || '/', 'http://localhost');
|
|
165
195
|
const pathname = url.pathname;
|
|
166
196
|
|
|
197
|
+
// Client-side error \u2014 navigate directly to _500 handler.
|
|
198
|
+
if (url.searchParams.has('__clientError')) {
|
|
199
|
+
${clientErrorHandler}
|
|
200
|
+
}
|
|
201
|
+
|
|
167
202
|
for (const route of ROUTES) {
|
|
168
203
|
const m = pathname.match(new RegExp(route.regex));
|
|
169
204
|
if (!m) continue;
|
|
@@ -172,7 +207,6 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
172
207
|
route.params.forEach((name, i) => {
|
|
173
208
|
const raw = m[i + 1] ?? '';
|
|
174
209
|
if (catchAllSet.has(name)) {
|
|
175
|
-
// Encode catch-all as repeated keys so the handler can getAll() \u2192 string[]
|
|
176
210
|
raw.split('/').filter(Boolean).forEach(seg => url.searchParams.append(name, seg));
|
|
177
211
|
} else {
|
|
178
212
|
url.searchParams.set(name, raw);
|
|
@@ -180,12 +214,16 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
180
214
|
});
|
|
181
215
|
req.url = pathname + (url.search || '');
|
|
182
216
|
|
|
183
|
-
|
|
217
|
+
try {
|
|
218
|
+
return await route.handler(req, res);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error('[handler error]', err);
|
|
221
|
+
${errorHandler}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
184
224
|
}
|
|
185
225
|
|
|
186
|
-
|
|
187
|
-
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
188
|
-
res.end('Not Found');
|
|
226
|
+
${notFoundHandler}
|
|
189
227
|
}
|
|
190
228
|
`;
|
|
191
229
|
}
|
|
@@ -216,7 +254,8 @@ if (apiRoutes.length > 0) {
|
|
|
216
254
|
vercelRoutes.push({ src: srcRegex, dest: "/api" });
|
|
217
255
|
}
|
|
218
256
|
const serverPages = collectServerPages(PAGES_DIR);
|
|
219
|
-
|
|
257
|
+
const hasErrorPages = ["_404.tsx", "_500.tsx"].some((f) => fs.existsSync(path.join(PAGES_DIR, f)));
|
|
258
|
+
if (serverPages.length > 0 || hasErrorPages) {
|
|
220
259
|
const globalRegistry = collectGlobalClientRegistry(serverPages, PAGES_DIR);
|
|
221
260
|
const prerenderedHtml = await bundleClientComponents(globalRegistry, PAGES_DIR, STATIC_DIR);
|
|
222
261
|
const prerenderedRecord = Object.fromEntries(prerenderedHtml);
|
|
@@ -252,8 +291,36 @@ if (serverPages.length > 0) {
|
|
|
252
291
|
paramNames: page.paramNames,
|
|
253
292
|
catchAllNames: page.catchAllNames
|
|
254
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
|
+
}
|
|
255
322
|
const dispatcherPath = path.join(PAGES_DIR, `_pages_dispatcher_${randomBytes(4).toString("hex")}.ts`);
|
|
256
|
-
fs.writeFileSync(dispatcherPath, makePagesDispatcherSource(dispatcherRoutes));
|
|
323
|
+
fs.writeFileSync(dispatcherPath, makePagesDispatcherSource(dispatcherRoutes, errorAdapters));
|
|
257
324
|
try {
|
|
258
325
|
const result = await build({
|
|
259
326
|
entryPoints: [dispatcherPath],
|
|
@@ -272,6 +339,7 @@ if (serverPages.length > 0) {
|
|
|
272
339
|
} finally {
|
|
273
340
|
fs.unlinkSync(dispatcherPath);
|
|
274
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);
|
|
275
343
|
}
|
|
276
344
|
for (const { srcRegex } of serverPages)
|
|
277
345
|
vercelRoutes.push({ src: srcRegex, dest: "/pages" });
|
|
@@ -289,4 +357,3 @@ copyPublicFiles(PUBLIC_DIR, STATIC_DIR);
|
|
|
289
357
|
const fnCount = (apiRoutes.length > 0 ? 1 : 0) + (serverPages.length > 0 ? 1 : 0);
|
|
290
358
|
console.log(`
|
|
291
359
|
\u2713 Vercel build complete \u2014 ${fnCount} function(s) \u2192 .vercel/output`);
|
|
292
|
-
//# sourceMappingURL=build-vercel.js.map
|
package/dist/builder.js
CHANGED
|
@@ -39,6 +39,35 @@ 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
|
+
"html-store",
|
|
45
|
+
// exported types (TitleValue, LinkTag, MetaTag, …) live here
|
|
46
|
+
"use-html",
|
|
47
|
+
"use-router",
|
|
48
|
+
"use-request",
|
|
49
|
+
"request-store",
|
|
50
|
+
"Link",
|
|
51
|
+
"bundle",
|
|
52
|
+
"utils",
|
|
53
|
+
"logger"
|
|
54
|
+
]);
|
|
55
|
+
function prunePrivateDeclarations(dir) {
|
|
56
|
+
(function walk(currentDir) {
|
|
57
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
58
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
walk(fullPath);
|
|
61
|
+
} else if (entry.name.endsWith(".d.ts") || entry.name.endsWith(".d.ts.map")) {
|
|
62
|
+
const stem = entry.name.replace(/\.d\.ts(\.map)?$/, "");
|
|
63
|
+
if (!PUBLIC_STEMS.has(stem)) {
|
|
64
|
+
fs.rmSync(fullPath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})(dir);
|
|
69
|
+
console.log("\u2702\uFE0F Pruned private .d.ts files (kept public API only).");
|
|
70
|
+
}
|
|
42
71
|
async function runBuild() {
|
|
43
72
|
try {
|
|
44
73
|
cleanDist(outDir);
|
|
@@ -49,13 +78,13 @@ async function runBuild() {
|
|
|
49
78
|
platform: "node",
|
|
50
79
|
format: "esm",
|
|
51
80
|
target: ["node20"],
|
|
52
|
-
packages: "external"
|
|
53
|
-
sourcemap: true
|
|
81
|
+
packages: "external"
|
|
54
82
|
});
|
|
55
83
|
console.log("\u2705 Build done.");
|
|
56
84
|
processDist(outDir);
|
|
57
85
|
console.log("\u{1F4C4} Generating TypeScript declarations\u2026");
|
|
58
|
-
execSync("tsc --emitDeclarationOnly --declaration --outDir dist", { stdio: "inherit" });
|
|
86
|
+
execSync("tsc --emitDeclarationOnly --declaration --declarationMap false --outDir dist", { stdio: "inherit" });
|
|
87
|
+
prunePrivateDeclarations(outDir);
|
|
59
88
|
console.log("\n\u{1F389} Build complete \u2192 dist/");
|
|
60
89
|
} catch (err) {
|
|
61
90
|
console.error("\u274C Build failed:", err);
|
|
@@ -63,4 +92,3 @@ async function runBuild() {
|
|
|
63
92
|
}
|
|
64
93
|
}
|
|
65
94
|
runBuild();
|
|
66
|
-
//# sourceMappingURL=builder.js.map
|