nukejs 0.0.9 → 0.0.11
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 +136 -1
- package/dist/Link.d.ts +8 -2
- package/dist/Link.js +2 -2
- package/dist/Link.js.map +2 -2
- package/dist/app.js +1 -1
- package/dist/app.js.map +2 -2
- package/dist/build-common.d.ts +7 -1
- package/dist/build-common.js +120 -14
- package/dist/build-common.js.map +2 -2
- package/dist/build-node.js +4 -0
- package/dist/build-node.js.map +2 -2
- package/dist/build-vercel.js +5 -0
- package/dist/build-vercel.js.map +2 -2
- package/dist/bundle.d.ts +7 -0
- package/dist/bundle.js.map +2 -2
- package/dist/hmr-bundle.js.map +1 -1
- package/dist/hmr.js.map +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +2 -2
- package/dist/metadata.d.ts +8 -7
- package/dist/metadata.js.map +2 -2
- package/dist/renderer.js +34 -3
- package/dist/renderer.js.map +2 -2
- package/dist/request-store.d.ts +84 -0
- package/dist/request-store.js +47 -0
- package/dist/request-store.js.map +7 -0
- package/dist/router.d.ts +8 -5
- package/dist/router.js.map +2 -2
- package/dist/ssr.d.ts +2 -1
- package/dist/ssr.js +19 -4
- package/dist/ssr.js.map +2 -2
- package/dist/use-request.d.ts +74 -0
- package/dist/use-request.js +49 -0
- package/dist/use-request.js.map +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ npm create nuke@latest
|
|
|
23
23
|
- [useHtml() — Head Management](#usehtml--head-management)
|
|
24
24
|
- [Configuration](#configuration)
|
|
25
25
|
- [Link Component & Navigation](#link-component--navigation)
|
|
26
|
+
- [useRequest() — URL Params, Query & Headers](#userequest--url-params-query--headers)
|
|
26
27
|
- [Building & Deploying](#building--deploying)
|
|
27
28
|
|
|
28
29
|
## Overview
|
|
@@ -81,7 +82,7 @@ export default function LikeButton({ postId }: { postId: string }) {
|
|
|
81
82
|
### Installation
|
|
82
83
|
|
|
83
84
|
```bash
|
|
84
|
-
npm create nuke
|
|
85
|
+
npm create nuke@latest
|
|
85
86
|
```
|
|
86
87
|
|
|
87
88
|
### Running the dev server
|
|
@@ -579,6 +580,140 @@ export default function SearchForm() {
|
|
|
579
580
|
|
|
580
581
|
---
|
|
581
582
|
|
|
583
|
+
## useRequest() — URL Params, Query & Headers
|
|
584
|
+
|
|
585
|
+
`useRequest()` is a universal hook that exposes the current request's URL parameters, query string, and headers to any component — **server or client, dev or production**.
|
|
586
|
+
|
|
587
|
+
```tsx
|
|
588
|
+
import { useRequest } from 'nukejs';
|
|
589
|
+
|
|
590
|
+
const { params, query, headers, pathname, url } = useRequest();
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
| Field | Type | Description |
|
|
594
|
+
|---|---|---|
|
|
595
|
+
| `url` | `string` | Full URL with query string, e.g. `/blog/hello?lang=en` |
|
|
596
|
+
| `pathname` | `string` | Path only, e.g. `/blog/hello` |
|
|
597
|
+
| `params` | `Record<string, string \| string[]>` | Dynamic route segments |
|
|
598
|
+
| `query` | `Record<string, string \| string[]>` | Query-string params (multi-value keys become arrays) |
|
|
599
|
+
| `headers` | `Record<string, string>` | Request headers |
|
|
600
|
+
|
|
601
|
+
### Where data comes from
|
|
602
|
+
|
|
603
|
+
| Environment | Source |
|
|
604
|
+
|---|---|
|
|
605
|
+
| Server (SSR) | Live `IncomingMessage` — all headers including `cookie` |
|
|
606
|
+
| Client (browser) | `__n_data` blob embedded in the page + `window.location` (reactive) |
|
|
607
|
+
|
|
608
|
+
On the client the hook is **reactive**: it re-reads on every SPA navigation so `query`, `pathname`, and `params` stay current without a page reload.
|
|
609
|
+
|
|
610
|
+
> **Security:** `headers` on the client never contains `cookie`, `authorization`, `proxy-authorization`, `set-cookie`, or `x-api-key`. These are stripped before embedding in the HTML document so credentials cannot leak into cached or logged pages.
|
|
611
|
+
|
|
612
|
+
### Reading route params and query string
|
|
613
|
+
|
|
614
|
+
```tsx
|
|
615
|
+
// app/pages/blog/[slug].tsx
|
|
616
|
+
// URL: /blog/hello-world?tab=comments
|
|
617
|
+
import { useRequest } from 'nukejs';
|
|
618
|
+
|
|
619
|
+
export default function BlogPost() {
|
|
620
|
+
const { params, query } = useRequest();
|
|
621
|
+
const slug = params.slug as string;
|
|
622
|
+
const tab = (query.tab as string) ?? 'overview';
|
|
623
|
+
|
|
624
|
+
return (
|
|
625
|
+
<article>
|
|
626
|
+
<h1>{slug}</h1>
|
|
627
|
+
<p>Active tab: {tab}</p>
|
|
628
|
+
</article>
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Catch-all routes
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
// app/pages/docs/[...path].tsx
|
|
637
|
+
// URL: /docs/api/hooks → path = ['api', 'hooks']
|
|
638
|
+
import { useRequest } from 'nukejs';
|
|
639
|
+
|
|
640
|
+
export default function Docs() {
|
|
641
|
+
const { params } = useRequest();
|
|
642
|
+
const segments = params.path as string[];
|
|
643
|
+
|
|
644
|
+
return <nav>{segments.join(' › ')}</nav>;
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Reading headers in a server component
|
|
649
|
+
|
|
650
|
+
```tsx
|
|
651
|
+
// app/pages/dashboard.tsx
|
|
652
|
+
import { useRequest } from 'nukejs';
|
|
653
|
+
|
|
654
|
+
export default async function Dashboard() {
|
|
655
|
+
const { headers } = useRequest();
|
|
656
|
+
|
|
657
|
+
// Forward the session cookie to an internal API call
|
|
658
|
+
const data = await fetch('http://localhost:3000/api/me', {
|
|
659
|
+
headers: { cookie: headers['cookie'] ?? '' },
|
|
660
|
+
}).then(r => r.json());
|
|
661
|
+
|
|
662
|
+
return <main>{data.name}</main>;
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Building `useI18n` on top
|
|
667
|
+
|
|
668
|
+
`useRequest` is designed as a primitive for higher-level hooks. Here is a complete `useI18n` implementation that works in both server and client components:
|
|
669
|
+
|
|
670
|
+
```tsx
|
|
671
|
+
// app/hooks/useI18n.ts
|
|
672
|
+
import { useRequest } from 'nukejs';
|
|
673
|
+
|
|
674
|
+
const translations = {
|
|
675
|
+
en: { welcome: 'Welcome', signIn: 'Sign in' },
|
|
676
|
+
fr: { welcome: 'Bienvenue', signIn: 'Se connecter' },
|
|
677
|
+
de: { welcome: 'Willkommen', signIn: 'Anmelden' },
|
|
678
|
+
} as const;
|
|
679
|
+
type Locale = keyof typeof translations;
|
|
680
|
+
|
|
681
|
+
function detectLocale(
|
|
682
|
+
query: Record<string, string | string[]>,
|
|
683
|
+
acceptLanguage = '',
|
|
684
|
+
): Locale {
|
|
685
|
+
// ?lang=fr in the URL takes priority over the browser header
|
|
686
|
+
const fromQuery = query.lang as string | undefined;
|
|
687
|
+
if (fromQuery && fromQuery in translations) return fromQuery as Locale;
|
|
688
|
+
|
|
689
|
+
const fromHeader = acceptLanguage
|
|
690
|
+
.split(',')[0]?.split('-')[0]?.trim().toLowerCase();
|
|
691
|
+
if (fromHeader && fromHeader in translations) return fromHeader as Locale;
|
|
692
|
+
|
|
693
|
+
return 'en';
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export function useI18n() {
|
|
697
|
+
const { query, headers } = useRequest();
|
|
698
|
+
const locale = detectLocale(query, headers['accept-language']);
|
|
699
|
+
return { t: translations[locale], locale };
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
```tsx
|
|
704
|
+
// app/pages/index.tsx
|
|
705
|
+
import { useI18n } from '../hooks/useI18n';
|
|
706
|
+
|
|
707
|
+
export default function Home() {
|
|
708
|
+
const { t } = useI18n();
|
|
709
|
+
return <h1>{t.welcome}</h1>;
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Changing `?lang=fr` in the URL re-renders client components automatically.
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
582
717
|
## Building & Deploying
|
|
583
718
|
|
|
584
719
|
### Node.js server
|
package/dist/Link.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
interface LinkProps {
|
|
2
2
|
href: string;
|
|
3
3
|
children: React.ReactNode;
|
|
4
4
|
className?: string;
|
|
5
|
-
}
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Client-side navigation link.
|
|
8
|
+
* Intercepts clicks and delegates to useRouter().push() so the SPA router
|
|
9
|
+
* handles the transition without a full page reload.
|
|
10
|
+
*/
|
|
11
|
+
declare const Link: ({ href, children, className }: LinkProps) => import("react/jsx-runtime").JSX.Element;
|
|
6
12
|
export default Link;
|
package/dist/Link.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import useRouter from "./use-router.js";
|
|
4
4
|
const Link = ({ href, children, className }) => {
|
|
5
|
-
const
|
|
5
|
+
const { push } = useRouter();
|
|
6
6
|
const handleClick = (e) => {
|
|
7
7
|
e.preventDefault();
|
|
8
|
-
|
|
8
|
+
push(href);
|
|
9
9
|
};
|
|
10
10
|
return /* @__PURE__ */ jsx("a", { href, onClick: handleClick, className, children });
|
|
11
11
|
};
|
package/dist/Link.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/Link.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\";\r\nimport useRouter from \"./use-router\";\r\n\r\ninterface LinkProps {\r\n href: string;\r\n children: React.ReactNode;\r\n className?: string;\r\n}\r\n\r\n/**\r\n * Client-side navigation link.\r\n * Intercepts clicks and delegates to useRouter().push() so the SPA router\r\n * handles the transition without a full page reload.\r\n */\r\nconst Link = ({ href, children, className }: LinkProps) => {\r\n const { push } = useRouter();\r\n\r\n const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\r\n e.preventDefault();\r\n push(href);\r\n };\r\n\r\n return (\r\n <a href={href} onClick={handleClick} className={className}>\r\n {children}\r\n </a>\r\n );\r\n};\r\n\r\nexport default Link;"],
|
|
5
|
+
"mappings": ";AAuBI;AAtBJ,OAAO,eAAe;AAatB,MAAM,OAAO,CAAC,EAAE,MAAM,UAAU,UAAU,MAAiB;AACzD,QAAM,EAAE,KAAK,IAAI,UAAU;AAE3B,QAAM,cAAc,CAAC,MAA2C;AAC9D,MAAE,eAAe;AACjB,SAAK,IAAI;AAAA,EACX;AAEA,SACE,oBAAC,OAAE,MAAY,SAAS,aAAa,WAClC,UACH;AAEJ;AAEA,IAAO,eAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/app.js
CHANGED
|
@@ -68,7 +68,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
68
68
|
);
|
|
69
69
|
if (matchApiPrefix(url, apiPrefixes))
|
|
70
70
|
return await handleApiRoute(url, req, res);
|
|
71
|
-
return await serverSideRender(url, res, PAGES_DIR, isDev);
|
|
71
|
+
return await serverSideRender(url, res, PAGES_DIR, isDev, req);
|
|
72
72
|
} catch (error) {
|
|
73
73
|
log.error("Server error:", error);
|
|
74
74
|
res.statusCode = 500;
|
package/dist/app.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/app.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * app.ts \u2014 NukeJS Dev Server Entry Point\r\n *\r\n * This is the runtime that powers `nuke dev`. It:\r\n * 1. Loads your nuke.config.ts (or uses sensible defaults)\r\n * 2. Discovers API route prefixes from your server directory\r\n * 3. Starts an HTTP server that handles:\r\n * app/public/** \u2014 static files (highest priority, via middleware)\r\n * /__hmr_ping \u2014 heartbeat for HMR reconnect polling\r\n * /__react.js \u2014 bundled React + ReactDOM (resolved via importmap)\r\n * /__n.js \u2014 NukeJS client runtime bundle\r\n * /__client-component/* \u2014 on-demand \"use client\" component bundles\r\n * server/** \u2014 API route handlers from serverDir\r\n * /** \u2014 SSR pages from app/pages (lowest priority)\r\n * 4. Watches for file changes and broadcasts HMR events to connected browsers\r\n *\r\n * In production (ENVIRONMENT=production), HMR and all file watching are skipped.\r\n */\r\n\r\nimport http from 'http';\r\nimport path from 'path';\r\nimport { existsSync, watch } from 'fs';\r\n\r\nimport { ansi, c, log, setDebugLevel, getDebugLevel } from './logger';\r\nimport { loadConfig } from './config';\r\nimport { discoverApiPrefixes, matchApiPrefix, createApiHandler } from './http-server';\r\nimport { loadMiddleware, runMiddleware } from './middleware-loader';\r\nimport { serveReactBundle, serveNukeBundle, serveClientComponentBundle } from './bundler';\r\nimport { serverSideRender } from './ssr';\r\nimport { watchDir, broadcastRestart } from './hmr';\r\n\r\n// \u2500\u2500\u2500 Environment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst isDev = process.env.ENVIRONMENT !== 'production';\r\n\r\n// React must live on globalThis so dynamically-imported page modules can share\r\n// the same React instance without each bundling their own copy.\r\nif (isDev) {\r\n const React = await import('react');\r\n (global as any).React = React;\r\n}\r\n\r\n// \u2500\u2500\u2500 Config & paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst config = await loadConfig();\r\nsetDebugLevel(config.debug ?? false);\r\n\r\nconst PAGES_DIR = path.resolve('./app/pages');\r\nconst SERVER_DIR = path.resolve(config.serverDir);\r\nconst PORT = config.port;\r\n\r\nlog.info('Configuration loaded:');\r\nlog.info(` - Pages directory: ${PAGES_DIR}`);\r\nlog.info(` - Server directory: ${SERVER_DIR}`);\r\nlog.info(` - Port: ${PORT}`);\r\nlog.info(` - Debug level: ${String(getDebugLevel())}`);\r\nlog.info(` - Dev mode: ${String(isDev)}`);\r\n\r\n// \u2500\u2500\u2500 API route discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n// Start watching the app directory for HMR.\r\nif (isDev) watchDir(path.resolve('./app'), 'App');\r\n\r\n// apiPrefixes is a live, mutable array. In dev, we splice it in-place whenever\r\n// the server directory changes so handlers always see the latest routes without\r\n// a full restart.\r\nconst apiPrefixes = discoverApiPrefixes(SERVER_DIR);\r\nconst handleApiRoute = createApiHandler({ apiPrefixes, port: PORT, isDev });\r\n\r\n\r\nlog.info(`API prefixes discovered: ${apiPrefixes.length === 0 ? 'none' : ''}`);\r\napiPrefixes.forEach(p => {\r\n log.info(` - ${p.prefix || '/'} -> ${path.relative(process.cwd(), p.directory)}`);\r\n});\r\n\r\n// \u2500\u2500\u2500 Full-restart file watchers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n// Some changes can't be hot-patched: middleware exports change the request\r\n// pipeline, and nuke.config.ts may change the port or serverDir. On change we\r\n// broadcast a 'restart' SSE event so browsers reconnect automatically, then\r\n// exit with code 75 \u2014 the CLI watches for this to respawn the process.\r\nif (isDev) {\r\n const RESTART_EXIT_CODE = 75;\r\n const restartFiles = [\r\n path.resolve('./middleware.ts'),\r\n path.resolve('./nuke.config.ts'),\r\n ];\r\n\r\n for (const filePath of restartFiles) {\r\n if (!existsSync(filePath)) continue;\r\n watch(filePath, async () => {\r\n log.info(`[Server] ${path.basename(filePath)} changed \u2014 restarting...`);\r\n await broadcastRestart();\r\n process.exit(RESTART_EXIT_CODE);\r\n });\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Middleware \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n// Loads built-in middleware (HMR SSE/JS endpoints) and the user-supplied\r\n// middleware.ts from the project root (if it exists).\r\nawait loadMiddleware();\r\n\r\n// \u2500\u2500\u2500 Request handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst server = http.createServer(async (req, res) => {\r\n try {\r\n // Middleware runs first. If it calls res.end() the request is fully\r\n // handled and we bail out immediately.\r\n const middlewareHandled = await runMiddleware(req, res);\r\n if (middlewareHandled) return;\r\n\r\n const url = req.url || '/';\r\n\r\n // \u2500\u2500 Internal NukeJS routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Framework files are checked before server routes so a user route can\r\n // never accidentally shadow /__n.js, /__react.js, etc.\r\n\r\n // Heartbeat polled by the HMR client to know when the server is back up.\r\n if (url === '/__hmr_ping') {\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('ok');\r\n return;\r\n }\r\n\r\n // Unified React bundle (react + react-dom/client + react/jsx-runtime).\r\n // Resolved by the importmap injected into every SSR page, so client\r\n // components never bundle React themselves.\r\n if (url === '/__react.js')\r\n return await serveReactBundle(res);\r\n\r\n // NukeJS browser runtime: initRuntime, SPA navigation, partial hydration.\r\n if (url === '/__n.js')\r\n return await serveNukeBundle(res);\r\n\r\n // On-demand bundles for individual \"use client\" components.\r\n // Strip the prefix, the .js extension, and any query string (cache buster).\r\n if (url.startsWith('/__client-component/'))\r\n return await serveClientComponentBundle(\r\n url.slice(20).split('?')[0].replace('.js', ''),\r\n res,\r\n );\r\n\r\n // \u2500\u2500 Server routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // API routes from serverDir \u2014 checked after framework files, before pages.\r\n if (matchApiPrefix(url, apiPrefixes))\r\n return await handleApiRoute(url, req, res);\r\n\r\n // \u2500\u2500 Page SSR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Nothing above matched \u2014 render a page from app/pages.\r\n return await serverSideRender(url, res, PAGES_DIR, isDev);\r\n\r\n } catch (error) {\r\n log.error('Server error:', error);\r\n res.statusCode = 500;\r\n res.end('Internal server error');\r\n }\r\n});\r\n\r\n// \u2500\u2500\u2500 Port binding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Tries to listen on `port`. If the port is already in use (EADDRINUSE),\r\n * increments and tries the next port until one is free.\r\n *\r\n * Returns the port that was actually bound.\r\n */\r\nfunction tryListen(port: number): Promise<number> {\r\n return new Promise((resolve, reject) => {\r\n server.once('error', (err: NodeJS.ErrnoException) => {\r\n if (err.code === 'EADDRINUSE') resolve(tryListen(port + 1));\r\n else reject(err);\r\n });\r\n server.listen(port, () => resolve(port));\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Startup banner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders the \u2622\uFE0F NukeJS startup box to stdout.\r\n * Uses box-drawing characters and ANSI colour codes for a clean terminal UI.\r\n */\r\nfunction printStartupBanner(port: number, isDev: boolean): void {\r\n const url = `http://localhost:${port}`;\r\n const level = getDebugLevel();\r\n const debugStr = String(level);\r\n const innerWidth = 42;\r\n const line = '\u2500'.repeat(innerWidth);\r\n\r\n /** Right-pads `text` to `width` columns, ignoring invisible ANSI sequences. */\r\n const pad = (text: string, width: number) => {\r\n const visibleLen = text.replace(/\\x1b\\[[0-9;]*m/g, '').length;\r\n return text + ' '.repeat(Math.max(0, width - visibleLen));\r\n };\r\n\r\n const row = (content: string, w = 2) =>\r\n `${ansi.gray}\u2502${ansi.reset} ${pad(content, innerWidth - w)} ${ansi.gray}\u2502${ansi.reset}`;\r\n const label = (key: string, val: string) =>\r\n row(`${c('gray', key)} ${val}`);\r\n\r\n console.log('');\r\n console.log(`${ansi.gray}\u250C${line}\u2510${ansi.reset}`);\r\n console.log(row(` ${c('red', '\u2622\uFE0F nukejs ', true)}`, 1));\r\n console.log(`${ansi.gray}\u251C${line}\u2524${ansi.reset}`);\r\n console.log(label(' Local ', c('cyan', url, true)));\r\n console.log(`${ansi.gray}\u251C${line}\u2524${ansi.reset}`);\r\n console.log(label(' Pages ', c('white', path.relative(process.cwd(), PAGES_DIR))));\r\n console.log(label(' Server ', c('white', path.relative(process.cwd(), SERVER_DIR))));\r\n console.log(label(' Dev ', isDev ? c('green', 'yes') : c('gray', 'no')));\r\n console.log(label(' Debug ', level === false\r\n ? c('gray', 'off')\r\n : level === true\r\n ? c('green', 'verbose')\r\n : c('yellow', debugStr)));\r\n console.log(`${ansi.gray}\u2514${line}\u2518${ansi.reset}`);\r\n console.log('');\r\n}\r\n\r\nconst actualPort = await tryListen(PORT);\r\nprintStartupBanner(actualPort, isDev);"],
|
|
5
|
-
"mappings": "AAmBA,OAAO,UAAU;AACjB,OAAO,UAAU;AACjB,SAAS,YAAY,aAAa;AAElC,SAAS,MAAM,GAAG,KAAK,eAAe,qBAAqB;AAC3D,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB,gBAAgB,wBAAwB;AACtE,SAAS,gBAAgB,qBAAqB;AAC9C,SAAS,kBAAkB,iBAAiB,kCAAkC;AAC9E,SAAS,wBAAwB;AACjC,SAAS,UAAU,wBAAwB;AAI3C,MAAM,QAAQ,QAAQ,IAAI,gBAAgB;AAI1C,IAAI,OAAO;AACT,QAAM,QAAQ,MAAM,OAAO,OAAO;AAClC,EAAC,OAAe,QAAQ;AAC1B;AAIA,MAAM,SAAS,MAAM,WAAW;AAChC,cAAc,OAAO,SAAS,KAAK;AAEnC,MAAM,YAAY,KAAK,QAAQ,aAAa;AAC5C,MAAM,aAAa,KAAK,QAAQ,OAAO,SAAS;AAChD,MAAM,OAAa,OAAO;AAE1B,IAAI,KAAK,uBAAuB;AAChC,IAAI,KAAK,wBAAwB,SAAS,EAAE;AAC5C,IAAI,KAAK,yBAAyB,UAAU,EAAE;AAC9C,IAAI,KAAK,aAAa,IAAI,EAAE;AAC5B,IAAI,KAAK,oBAAoB,OAAO,cAAc,CAAC,CAAC,EAAE;AACtD,IAAI,KAAK,iBAAiB,OAAO,KAAK,CAAC,EAAE;AAKzC,IAAI,MAAO,UAAS,KAAK,QAAQ,OAAO,GAAG,KAAK;AAKhD,MAAM,cAAiB,oBAAoB,UAAU;AACrD,MAAM,iBAAiB,iBAAiB,EAAE,aAAa,MAAM,MAAM,MAAM,CAAC;
|
|
4
|
+
"sourcesContent": ["/**\r\n * app.ts \u2014 NukeJS Dev Server Entry Point\r\n *\r\n * This is the runtime that powers `nuke dev`. It:\r\n * 1. Loads your nuke.config.ts (or uses sensible defaults)\r\n * 2. Discovers API route prefixes from your server directory\r\n * 3. Starts an HTTP server that handles:\r\n * app/public/** \u2014 static files (highest priority, via middleware)\r\n * /__hmr_ping \u2014 heartbeat for HMR reconnect polling\r\n * /__react.js \u2014 bundled React + ReactDOM (resolved via importmap)\r\n * /__n.js \u2014 NukeJS client runtime bundle\r\n * /__client-component/* \u2014 on-demand \"use client\" component bundles\r\n * server/** \u2014 API route handlers from serverDir\r\n * /** \u2014 SSR pages from app/pages (lowest priority)\r\n * 4. Watches for file changes and broadcasts HMR events to connected browsers\r\n *\r\n * In production (ENVIRONMENT=production), HMR and all file watching are skipped.\r\n */\r\n\r\nimport http from 'http';\r\nimport path from 'path';\r\nimport { existsSync, watch } from 'fs';\r\n\r\nimport { ansi, c, log, setDebugLevel, getDebugLevel } from './logger';\r\nimport { loadConfig } from './config';\r\nimport { discoverApiPrefixes, matchApiPrefix, createApiHandler } from './http-server';\r\nimport { loadMiddleware, runMiddleware } from './middleware-loader';\r\nimport { serveReactBundle, serveNukeBundle, serveClientComponentBundle } from './bundler';\r\nimport { serverSideRender } from './ssr';\r\nimport { watchDir, broadcastRestart } from './hmr';\r\n\r\n// \u2500\u2500\u2500 Environment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst isDev = process.env.ENVIRONMENT !== 'production';\r\n\r\n// React must live on globalThis so dynamically-imported page modules can share\r\n// the same React instance without each bundling their own copy.\r\nif (isDev) {\r\n const React = await import('react');\r\n (global as any).React = React;\r\n}\r\n\r\n// \u2500\u2500\u2500 Config & paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst config = await loadConfig();\r\nsetDebugLevel(config.debug ?? false);\r\n\r\nconst PAGES_DIR = path.resolve('./app/pages');\r\nconst SERVER_DIR = path.resolve(config.serverDir);\r\nconst PORT = config.port;\r\n\r\nlog.info('Configuration loaded:');\r\nlog.info(` - Pages directory: ${PAGES_DIR}`);\r\nlog.info(` - Server directory: ${SERVER_DIR}`);\r\nlog.info(` - Port: ${PORT}`);\r\nlog.info(` - Debug level: ${String(getDebugLevel())}`);\r\nlog.info(` - Dev mode: ${String(isDev)}`);\r\n\r\n// \u2500\u2500\u2500 API route discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n// Start watching the app directory for HMR.\r\nif (isDev) watchDir(path.resolve('./app'), 'App');\r\n\r\n// apiPrefixes is a live, mutable array. In dev, we splice it in-place whenever\r\n// the server directory changes so handlers always see the latest routes without\r\n// a full restart.\r\nconst apiPrefixes = discoverApiPrefixes(SERVER_DIR);\r\nconst handleApiRoute = createApiHandler({ apiPrefixes, port: PORT, isDev });\r\n\r\nlog.info(`API prefixes discovered: ${apiPrefixes.length === 0 ? 'none' : ''}`);\r\napiPrefixes.forEach(p => {\r\n log.info(` - ${p.prefix || '/'} -> ${path.relative(process.cwd(), p.directory)}`);\r\n});\r\n\r\n// \u2500\u2500\u2500 Full-restart file watchers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n// Some changes can't be hot-patched: middleware exports change the request\r\n// pipeline, and nuke.config.ts may change the port or serverDir. On change we\r\n// broadcast a 'restart' SSE event so browsers reconnect automatically, then\r\n// exit with code 75 \u2014 the CLI watches for this to respawn the process.\r\nif (isDev) {\r\n const RESTART_EXIT_CODE = 75;\r\n const restartFiles = [\r\n path.resolve('./middleware.ts'),\r\n path.resolve('./nuke.config.ts'),\r\n ];\r\n\r\n for (const filePath of restartFiles) {\r\n if (!existsSync(filePath)) continue;\r\n watch(filePath, async () => {\r\n log.info(`[Server] ${path.basename(filePath)} changed \u2014 restarting...`);\r\n await broadcastRestart();\r\n process.exit(RESTART_EXIT_CODE);\r\n });\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Middleware \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n// Loads built-in middleware (HMR SSE/JS endpoints) and the user-supplied\r\n// middleware.ts from the project root (if it exists).\r\nawait loadMiddleware();\r\n\r\n// \u2500\u2500\u2500 Request handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst server = http.createServer(async (req, res) => {\r\n try {\r\n // Middleware runs first. If it calls res.end() the request is fully\r\n // handled and we bail out immediately.\r\n const middlewareHandled = await runMiddleware(req, res);\r\n if (middlewareHandled) return;\r\n\r\n const url = req.url || '/';\r\n\r\n // \u2500\u2500 Internal NukeJS routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Framework files are checked before server routes so a user route can\r\n // never accidentally shadow /__n.js, /__react.js, etc.\r\n\r\n // Heartbeat polled by the HMR client to know when the server is back up.\r\n if (url === '/__hmr_ping') {\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('ok');\r\n return;\r\n }\r\n\r\n // Unified React bundle (react + react-dom/client + react/jsx-runtime).\r\n // Resolved by the importmap injected into every SSR page, so client\r\n // components never bundle React themselves.\r\n if (url === '/__react.js')\r\n return await serveReactBundle(res);\r\n\r\n // NukeJS browser runtime: initRuntime, SPA navigation, partial hydration.\r\n if (url === '/__n.js')\r\n return await serveNukeBundle(res);\r\n\r\n // On-demand bundles for individual \"use client\" components.\r\n // Strip the prefix, the .js extension, and any query string (cache buster).\r\n if (url.startsWith('/__client-component/'))\r\n return await serveClientComponentBundle(\r\n url.slice(20).split('?')[0].replace('.js', ''),\r\n res,\r\n );\r\n\r\n // \u2500\u2500 Server routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // API routes from serverDir \u2014 checked after framework files, before pages.\r\n if (matchApiPrefix(url, apiPrefixes))\r\n return await handleApiRoute(url, req, res);\r\n\r\n // \u2500\u2500 Page SSR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Nothing above matched \u2014 render a page from app/pages.\r\n return await serverSideRender(url, res, PAGES_DIR, isDev, req);\r\n\r\n } catch (error) {\r\n log.error('Server error:', error);\r\n res.statusCode = 500;\r\n res.end('Internal server error');\r\n }\r\n});\r\n\r\n// \u2500\u2500\u2500 Port binding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Tries to listen on `port`. If the port is already in use (EADDRINUSE),\r\n * increments and tries the next port until one is free.\r\n *\r\n * Returns the port that was actually bound.\r\n */\r\nfunction tryListen(port: number): Promise<number> {\r\n return new Promise((resolve, reject) => {\r\n server.once('error', (err: NodeJS.ErrnoException) => {\r\n if (err.code === 'EADDRINUSE') resolve(tryListen(port + 1));\r\n else reject(err);\r\n });\r\n server.listen(port, () => resolve(port));\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Startup banner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders the \u2622\uFE0F NukeJS startup box to stdout.\r\n * Uses box-drawing characters and ANSI colour codes for a clean terminal UI.\r\n */\r\nfunction printStartupBanner(port: number, isDev: boolean): void {\r\n const url = `http://localhost:${port}`;\r\n const level = getDebugLevel();\r\n const debugStr = String(level);\r\n const innerWidth = 42;\r\n const line = '\u2500'.repeat(innerWidth);\r\n\r\n /** Right-pads `text` to `width` columns, ignoring invisible ANSI sequences. */\r\n const pad = (text: string, width: number) => {\r\n const visibleLen = text.replace(/\\x1b\\[[0-9;]*m/g, '').length;\r\n return text + ' '.repeat(Math.max(0, width - visibleLen));\r\n };\r\n\r\n const row = (content: string, w = 2) =>\r\n `${ansi.gray}\u2502${ansi.reset} ${pad(content, innerWidth - w)} ${ansi.gray}\u2502${ansi.reset}`;\r\n const label = (key: string, val: string) =>\r\n row(`${c('gray', key)} ${val}`);\r\n\r\n console.log('');\r\n console.log(`${ansi.gray}\u250C${line}\u2510${ansi.reset}`);\r\n console.log(row(` ${c('red', '\u2622\uFE0F nukejs ', true)}`, 1));\r\n console.log(`${ansi.gray}\u251C${line}\u2524${ansi.reset}`);\r\n console.log(label(' Local ', c('cyan', url, true)));\r\n console.log(`${ansi.gray}\u251C${line}\u2524${ansi.reset}`);\r\n console.log(label(' Pages ', c('white', path.relative(process.cwd(), PAGES_DIR))));\r\n console.log(label(' Server ', c('white', path.relative(process.cwd(), SERVER_DIR))));\r\n console.log(label(' Dev ', isDev ? c('green', 'yes') : c('gray', 'no')));\r\n console.log(label(' Debug ', level === false\r\n ? c('gray', 'off')\r\n : level === true\r\n ? c('green', 'verbose')\r\n : c('yellow', debugStr)));\r\n console.log(`${ansi.gray}\u2514${line}\u2518${ansi.reset}`);\r\n console.log('');\r\n}\r\n\r\nconst actualPort = await tryListen(PORT);\r\nprintStartupBanner(actualPort, isDev);"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["isDev"]
|
|
7
7
|
}
|
package/dist/build-common.d.ts
CHANGED
|
@@ -120,7 +120,12 @@ export interface PageAdapterOptions {
|
|
|
120
120
|
layoutArrayItems: string;
|
|
121
121
|
/** Pre-rendered HTML per client component ID, computed at build time */
|
|
122
122
|
prerenderedHtml: Record<string, string>;
|
|
123
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* All dynamic route param names for this page (e.g. ['id', 'slug']).
|
|
125
|
+
* Used to distinguish route segments from real query-string params at runtime.
|
|
126
|
+
*/
|
|
127
|
+
routeParamNames: string[];
|
|
128
|
+
/** Subset of routeParamNames whose values are string[] (catch-all segments) */
|
|
124
129
|
catchAllNames: string[];
|
|
125
130
|
}
|
|
126
131
|
/**
|
|
@@ -147,6 +152,7 @@ export interface PageBundleOptions {
|
|
|
147
152
|
allClientIds: string[];
|
|
148
153
|
layoutPaths: string[];
|
|
149
154
|
prerenderedHtml: Record<string, string>;
|
|
155
|
+
routeParamNames: string[];
|
|
150
156
|
catchAllNames: string[];
|
|
151
157
|
}
|
|
152
158
|
/**
|
package/dist/build-common.js
CHANGED
|
@@ -200,6 +200,7 @@ async function buildPages(pagesDir, staticDir) {
|
|
|
200
200
|
allClientIds: [...registry.keys()],
|
|
201
201
|
layoutPaths,
|
|
202
202
|
prerenderedHtml: prerenderedRecord,
|
|
203
|
+
routeParamNames: page.paramNames,
|
|
203
204
|
catchAllNames: page.catchAllNames
|
|
204
205
|
});
|
|
205
206
|
builtPages.push({ ...page, bundleText });
|
|
@@ -240,8 +241,11 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
240
241
|
const apiReq = req as any;
|
|
241
242
|
|
|
242
243
|
apiReq.body = await parseBody(req);
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
// In production, route dynamic segments are injected as query-string keys by
|
|
245
|
+
// the server entry, so params and query share the same parsed URL values.
|
|
246
|
+
const qs = Object.fromEntries(new URL(req.url || '/', 'http://localhost').searchParams);
|
|
247
|
+
apiReq.query = qs;
|
|
248
|
+
apiReq.params = qs;
|
|
245
249
|
|
|
246
250
|
const fn = (mod as any)[method] ?? (mod as any).default;
|
|
247
251
|
if (typeof fn !== 'function') {
|
|
@@ -260,6 +264,7 @@ function makePageAdapterSource(opts) {
|
|
|
260
264
|
allClientIds,
|
|
261
265
|
layoutArrayItems,
|
|
262
266
|
prerenderedHtml,
|
|
267
|
+
routeParamNames,
|
|
263
268
|
catchAllNames
|
|
264
269
|
} = opts;
|
|
265
270
|
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
@@ -271,7 +276,10 @@ ${layoutImports}
|
|
|
271
276
|
const CLIENT_COMPONENTS: Record<string, string> = ${JSON.stringify(clientComponentNames)};
|
|
272
277
|
const ALL_CLIENT_IDS: string[] = ${JSON.stringify(allClientIds)};
|
|
273
278
|
const PRERENDERED_HTML: Record<string, string> = ${JSON.stringify(prerenderedHtml)};
|
|
274
|
-
|
|
279
|
+
// ROUTE_PARAM_NAMES: the dynamic segments baked into this page's URL pattern.
|
|
280
|
+
// Used to separate them from real user-supplied query params at runtime.
|
|
281
|
+
const ROUTE_PARAM_NAMES = new Set<string>(${JSON.stringify(routeParamNames)});
|
|
282
|
+
const CATCH_ALL_NAMES = new Set<string>(${JSON.stringify(catchAllNames)});
|
|
275
283
|
|
|
276
284
|
// \u2500\u2500\u2500 html-store (inlined) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
277
285
|
type TitleValue = string | ((prev: string) => string);
|
|
@@ -302,6 +310,36 @@ function resolveTitle(ops: TitleValue[], fallback = ''): string {
|
|
|
302
310
|
return t;
|
|
303
311
|
}
|
|
304
312
|
|
|
313
|
+
// \u2500\u2500\u2500 request-store (inlined) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
314
|
+
const SENSITIVE_HEADERS = new Set([
|
|
315
|
+
'cookie','authorization','proxy-authorization','set-cookie','x-api-key',
|
|
316
|
+
]);
|
|
317
|
+
// Flattens multi-value headers to strings; keeps all headers including credentials.
|
|
318
|
+
function normaliseHeaders(raw: Record<string, string | string[] | undefined>): Record<string, string> {
|
|
319
|
+
const out: Record<string, string> = {};
|
|
320
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
321
|
+
if (v === undefined) continue;
|
|
322
|
+
out[k] = Array.isArray(v) ? v.join(', ') : v;
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
// Same as normaliseHeaders but strips credentials before embedding in HTML.
|
|
327
|
+
function sanitiseHeaders(raw: Record<string, string | string[] | undefined>): Record<string, string> {
|
|
328
|
+
const out: Record<string, string> = {};
|
|
329
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
330
|
+
if (SENSITIVE_HEADERS.has(k.toLowerCase()) || v === undefined) continue;
|
|
331
|
+
out[k] = Array.isArray(v) ? v.join(', ') : v;
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
const __REQ_KEY__ = Symbol.for('__nukejs_request_store__');
|
|
336
|
+
const __getReq = () => (globalThis as any)[__REQ_KEY__] ?? null;
|
|
337
|
+
const __setReq = (v: any) => { (globalThis as any)[__REQ_KEY__] = v; };
|
|
338
|
+
async function runWithRequestStore<T>(ctx: any, fn: () => Promise<T>): Promise<T> {
|
|
339
|
+
__setReq(ctx);
|
|
340
|
+
try { return await fn(); } finally { __setReq(null); }
|
|
341
|
+
}
|
|
342
|
+
|
|
305
343
|
// \u2500\u2500\u2500 HTML helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
306
344
|
function escapeHtml(s: string): string {
|
|
307
345
|
return String(s)
|
|
@@ -349,9 +387,46 @@ const VOID_TAGS = new Set([
|
|
|
349
387
|
'link','meta','param','source','track','wbr',
|
|
350
388
|
]);
|
|
351
389
|
|
|
390
|
+
// \u2500\u2500\u2500 Wrapper attribute helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
391
|
+
function isWrapperAttr(key: string): boolean {
|
|
392
|
+
return (
|
|
393
|
+
key === 'className' ||
|
|
394
|
+
key === 'style' ||
|
|
395
|
+
key === 'id' ||
|
|
396
|
+
key.startsWith('data-') ||
|
|
397
|
+
key.startsWith('aria-')
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
function splitWrapperAttrs(props: any): { wrapperAttrs: Record<string, any>; componentProps: Record<string, any> } {
|
|
401
|
+
const wrapperAttrs: Record<string, any> = {};
|
|
402
|
+
const componentProps: Record<string, any> = {};
|
|
403
|
+
for (const [key, value] of Object.entries((props || {}) as Record<string, any>)) {
|
|
404
|
+
if (isWrapperAttr(key)) wrapperAttrs[key] = value;
|
|
405
|
+
else componentProps[key] = value;
|
|
406
|
+
}
|
|
407
|
+
return { wrapperAttrs, componentProps };
|
|
408
|
+
}
|
|
409
|
+
function buildWrapperAttrString(attrs: Record<string, any>): string {
|
|
410
|
+
const parts = Object.entries(attrs)
|
|
411
|
+
.map(([key, value]) => {
|
|
412
|
+
if (key === 'className') key = 'class';
|
|
413
|
+
if (key === 'style' && typeof value === 'object') {
|
|
414
|
+
const css = Object.entries(value as Record<string, any>)
|
|
415
|
+
.map(([p, val]) => \`\${p.replace(/[A-Z]/g, m => \`-\${m.toLowerCase()}\`)}:\${escapeHtml(String(val))}\`)
|
|
416
|
+
.join(';');
|
|
417
|
+
return \`style="\${css}"\`;
|
|
418
|
+
}
|
|
419
|
+
if (typeof value === 'boolean') return value ? key : '';
|
|
420
|
+
if (value == null) return '';
|
|
421
|
+
return \`\${key}="\${escapeHtml(String(value))}"\`;
|
|
422
|
+
})
|
|
423
|
+
.filter(Boolean);
|
|
424
|
+
return parts.length ? ' ' + parts.join(' ') : '';
|
|
425
|
+
}
|
|
426
|
+
|
|
352
427
|
function serializeProps(value: any): any {
|
|
428
|
+
if (typeof value === 'function') return undefined; // must come before the object check
|
|
353
429
|
if (value == null || typeof value !== 'object') return value;
|
|
354
|
-
if (typeof value === 'function') return undefined;
|
|
355
430
|
if (Array.isArray(value)) return value.map(serializeProps).filter((v: any) => v !== undefined);
|
|
356
431
|
if ((value as any).$$typeof) {
|
|
357
432
|
const { type, props: p } = value as any;
|
|
@@ -409,14 +484,16 @@ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
|
|
|
409
484
|
const clientId = CLIENT_COMPONENTS[type.name];
|
|
410
485
|
if (clientId) {
|
|
411
486
|
hydrated.add(clientId);
|
|
412
|
-
const
|
|
487
|
+
const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
|
|
488
|
+
const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
|
|
489
|
+
const serializedProps = serializeProps(componentProps ?? {});
|
|
413
490
|
let ssrHtml: string;
|
|
414
491
|
try {
|
|
415
|
-
ssrHtml = __renderToString__(__createElement__(type as any,
|
|
492
|
+
ssrHtml = __renderToString__(__createElement__(type as any, componentProps || {}));
|
|
416
493
|
} catch {
|
|
417
494
|
ssrHtml = PRERENDERED_HTML[clientId] ?? '';
|
|
418
495
|
}
|
|
419
|
-
return \`<span data-hydrate-id="\${clientId}" data-hydrate-props="\${escapeHtml(JSON.stringify(serializedProps))}">\${ssrHtml}</span>\`;
|
|
496
|
+
return \`<span data-hydrate-id="\${clientId}"\${wrapperAttrStr} data-hydrate-props="\${escapeHtml(JSON.stringify(serializedProps))}">\${ssrHtml}</span>\`;
|
|
420
497
|
}
|
|
421
498
|
const instance = type.prototype?.isReactComponent ? new (type as any)(props) : null;
|
|
422
499
|
return renderNode(instance ? instance.render() : await (type as Function)(props || {}), hydrated);
|
|
@@ -439,19 +516,45 @@ function wrapWithLayouts(element: any): any {
|
|
|
439
516
|
export default async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
440
517
|
try {
|
|
441
518
|
const parsed = new URL(req.url || '/', 'http://localhost');
|
|
519
|
+
const url = req.url || '/';
|
|
520
|
+
const pathname = parsed.pathname;
|
|
521
|
+
|
|
522
|
+
// Route params are injected as query-string keys by the server entry.
|
|
523
|
+
// Build 'params' only from known route segments, and 'query' from the rest.
|
|
442
524
|
const params: Record<string, string | string[]> = {};
|
|
525
|
+
ROUTE_PARAM_NAMES.forEach(k => {
|
|
526
|
+
if (CATCH_ALL_NAMES.has(k)) {
|
|
527
|
+
params[k] = parsed.searchParams.getAll(k);
|
|
528
|
+
} else {
|
|
529
|
+
const v = parsed.searchParams.get(k);
|
|
530
|
+
if (v !== null) params[k] = v;
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const query: Record<string, string | string[]> = {};
|
|
443
535
|
parsed.searchParams.forEach((_, k) => {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
536
|
+
if (!ROUTE_PARAM_NAMES.has(k)) {
|
|
537
|
+
const all = parsed.searchParams.getAll(k);
|
|
538
|
+
query[k] = all.length > 1 ? all : all[0];
|
|
539
|
+
}
|
|
447
540
|
});
|
|
448
|
-
|
|
541
|
+
|
|
542
|
+
const rawHeaders = req.headers as Record<string, string | string[] | undefined>;
|
|
543
|
+
// Full headers (including credentials) for server components via the request store.
|
|
544
|
+
const normHeaders = normaliseHeaders(rawHeaders);
|
|
545
|
+
// Stripped headers safe for embedding in the HTML document.
|
|
546
|
+
const safeHeaders = sanitiseHeaders(rawHeaders);
|
|
449
547
|
|
|
450
548
|
const hydrated = new Set<string>();
|
|
451
|
-
|
|
549
|
+
// Merge query params into page props to match dev behaviour (ssr.ts mergedParams).
|
|
550
|
+
const merged = { ...query, ...params } as any;
|
|
551
|
+
const wrapped = wrapWithLayouts({ type: __page__.default, props: merged, key: null, ref: null });
|
|
452
552
|
|
|
453
553
|
let appHtml = '';
|
|
454
|
-
const store = await
|
|
554
|
+
const store = await runWithRequestStore(
|
|
555
|
+
{ url, pathname, params, query, headers: normHeaders },
|
|
556
|
+
() => runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); }),
|
|
557
|
+
);
|
|
455
558
|
|
|
456
559
|
const pageTitle = resolveTitle(store.titleOps, 'NukeJS');
|
|
457
560
|
const headScripts = store.script.filter((s: any) => (s.position ?? 'head') === 'head');
|
|
@@ -475,7 +578,8 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
475
578
|
const bodyScriptsHtml = bodyScriptLines.length ? '\\n' + bodyScriptLines.join('\\n') + '\\n' : '';
|
|
476
579
|
|
|
477
580
|
const runtimeData = JSON.stringify({
|
|
478
|
-
hydrateIds: [...hydrated], allIds: ALL_CLIENT_IDS, url, params,
|
|
581
|
+
hydrateIds: [...hydrated], allIds: ALL_CLIENT_IDS, url, params,
|
|
582
|
+
query, headers: safeHeaders, debug: 'silent',
|
|
479
583
|
}).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
|
|
480
584
|
|
|
481
585
|
const html = \`<!DOCTYPE html>
|
|
@@ -547,6 +651,7 @@ async function bundlePageHandler(opts) {
|
|
|
547
651
|
allClientIds,
|
|
548
652
|
layoutPaths,
|
|
549
653
|
prerenderedHtml,
|
|
654
|
+
routeParamNames,
|
|
550
655
|
catchAllNames
|
|
551
656
|
} = opts;
|
|
552
657
|
const adapterDir = path.dirname(absPath);
|
|
@@ -562,6 +667,7 @@ async function bundlePageHandler(opts) {
|
|
|
562
667
|
allClientIds,
|
|
563
668
|
layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
|
|
564
669
|
prerenderedHtml,
|
|
670
|
+
routeParamNames,
|
|
565
671
|
catchAllNames
|
|
566
672
|
}));
|
|
567
673
|
let text;
|