nukejs 0.0.6 → 0.0.8
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 +89 -6
- package/dist/{as-is/Link.js → Link.js} +3 -1
- package/dist/Link.js.map +7 -0
- package/dist/app.d.ts +3 -2
- package/dist/app.js +3 -13
- package/dist/app.js.map +2 -2
- package/dist/build-common.d.ts +6 -0
- package/dist/build-common.js +20 -6
- package/dist/build-common.js.map +2 -2
- package/dist/build-node.d.ts +1 -1
- package/dist/build-node.js +6 -17
- package/dist/build-node.js.map +2 -2
- package/dist/build-vercel.js +1 -1
- package/dist/build-vercel.js.map +2 -2
- package/dist/builder.d.ts +4 -10
- package/dist/builder.js +7 -38
- package/dist/builder.js.map +2 -2
- package/dist/bundle.js +60 -4
- package/dist/bundle.js.map +2 -2
- package/dist/component-analyzer.d.ts +6 -0
- package/dist/component-analyzer.js +12 -1
- package/dist/component-analyzer.js.map +2 -2
- package/dist/hmr-bundle.js +17 -4
- package/dist/hmr-bundle.js.map +2 -2
- package/dist/html-store.d.ts +7 -0
- package/dist/html-store.js.map +2 -2
- package/dist/http-server.d.ts +2 -9
- package/dist/http-server.js +16 -2
- package/dist/http-server.js.map +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/renderer.js +2 -7
- package/dist/renderer.js.map +2 -2
- package/dist/router.d.ts +20 -19
- package/dist/router.js +14 -6
- package/dist/router.js.map +2 -2
- package/dist/ssr.js +21 -4
- package/dist/ssr.js.map +2 -2
- package/dist/use-html.js +5 -1
- package/dist/use-html.js.map +2 -2
- package/dist/{as-is/useRouter.js → use-router.js} +1 -1
- package/dist/{as-is/useRouter.js.map → use-router.js.map} +2 -2
- package/package.json +1 -1
- package/dist/as-is/Link.js.map +0 -7
- package/dist/as-is/Link.tsx +0 -20
- package/dist/as-is/useRouter.ts +0 -33
- /package/dist/{as-is/Link.d.ts → Link.d.ts} +0 -0
- /package/dist/{as-is/useRouter.d.ts → use-router.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
[](https://nukejs.com)
|
|
2
2
|
|
|
3
|
-
# NukeJS
|
|
3
|
+
# NukeJS  [](https://nukejs.com) [<img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" height="20">](https://stackblitz.com/edit/nuke?file=app/pages/index.tsx)
|
|
4
|
+
|
|
4
5
|
|
|
5
6
|
A **minimal**, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.
|
|
6
7
|
|
|
@@ -21,6 +22,7 @@ npm create nuke@latest
|
|
|
21
22
|
- [Static Files](#static-files)
|
|
22
23
|
- [useHtml() — Head Management](#usehtml--head-management)
|
|
23
24
|
- [Configuration](#configuration)
|
|
25
|
+
- [Link Component & Navigation](#link-component--navigation)
|
|
24
26
|
- [Building & Deploying](#building--deploying)
|
|
25
27
|
|
|
26
28
|
## Overview
|
|
@@ -154,6 +156,28 @@ export default async function BlogPost({ slug }: { slug: string }) {
|
|
|
154
156
|
|
|
155
157
|
Route params are passed as props to the component.
|
|
156
158
|
|
|
159
|
+
### Query string params
|
|
160
|
+
|
|
161
|
+
Query string parameters are automatically merged into the page component's props alongside route params. If a query param shares a name with a route param, the route param takes precedence.
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
// app/pages/search.tsx
|
|
165
|
+
// URL: /search?q=nuke&page=2
|
|
166
|
+
export default function Search({ q, page }: { q: string; page: string }) {
|
|
167
|
+
return <h1>Results for "{q}" — page {page}</h1>;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// app/pages/blog/[slug].tsx
|
|
173
|
+
// URL: /blog/hello-world?preview=true
|
|
174
|
+
export default function BlogPost({ slug, preview }: { slug: string; preview?: string }) {
|
|
175
|
+
return <article data-preview={preview}>{slug}</article>;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A query param that appears multiple times (e.g. `?tag=a&tag=b`) is passed as a `string[]`.
|
|
180
|
+
|
|
157
181
|
### Catch-all routes
|
|
158
182
|
|
|
159
183
|
```tsx
|
|
@@ -389,13 +413,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|
|
389
413
|
| `nuke build` (Node) | Copied to `dist/static/` and served by the production HTTP server |
|
|
390
414
|
| `nuke build` (Vercel) | Copied to `.vercel/output/static/` — served by Vercel's CDN, no function invocation |
|
|
391
415
|
|
|
392
|
-
On Vercel, public files receive the same zero-latency CDN treatment as `
|
|
416
|
+
On Vercel, public files receive the same zero-latency CDN treatment as `__n.js`.
|
|
393
417
|
|
|
394
418
|
---
|
|
395
419
|
|
|
396
420
|
## useHtml() — Head Management
|
|
397
421
|
|
|
398
|
-
The `useHtml()` hook works in both server components and client components to control the document head
|
|
422
|
+
The `useHtml()` hook works in both server components and client components to control the document `<head>`, `<html>` attributes, `<body>` attributes, and scripts injected at the end of `<body>`.
|
|
399
423
|
|
|
400
424
|
```tsx
|
|
401
425
|
import { useHtml } from 'nukejs';
|
|
@@ -434,6 +458,65 @@ Result: "Home | Site"
|
|
|
434
458
|
|
|
435
459
|
The page title always serves as the base value; layout functions wrap it outward.
|
|
436
460
|
|
|
461
|
+
### Script injection & position
|
|
462
|
+
|
|
463
|
+
The `script` option accepts an array of script tags. Each entry supports the standard attributes (`src`, `type`, `async`, `defer`, `content` for inline scripts, etc.) plus a `position` field:
|
|
464
|
+
|
|
465
|
+
| `position` | Where it's injected |
|
|
466
|
+
|---|---|
|
|
467
|
+
| `'head'` (default) | Inside `<head>`, in the managed `<!--n-head-->` block |
|
|
468
|
+
| `'body'` | End of `<body>`, just before `</body>`, in the `<!--n-body-scripts-->` block |
|
|
469
|
+
|
|
470
|
+
**Use `position: 'body'`** for third-party analytics and tracking scripts (Google Analytics, Hotjar, Intercom, etc.) that should load after page content is in the DOM and must not block rendering.
|
|
471
|
+
|
|
472
|
+
```tsx
|
|
473
|
+
// app/pages/layout.tsx — Google Analytics on every page
|
|
474
|
+
import { useHtml } from 'nukejs';
|
|
475
|
+
|
|
476
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
477
|
+
useHtml({
|
|
478
|
+
script: [
|
|
479
|
+
// Load the gtag library — async so it doesn't block rendering
|
|
480
|
+
{
|
|
481
|
+
src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX',
|
|
482
|
+
async: true,
|
|
483
|
+
position: 'body',
|
|
484
|
+
},
|
|
485
|
+
// Inline initialisation — must follow the loader above
|
|
486
|
+
{
|
|
487
|
+
content: `
|
|
488
|
+
window.dataLayer = window.dataLayer || [];
|
|
489
|
+
function gtag(){dataLayer.push(arguments);}
|
|
490
|
+
gtag('js', new Date());
|
|
491
|
+
gtag('config', 'G-XXXXXXXXXX');
|
|
492
|
+
`,
|
|
493
|
+
position: 'body',
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
return <>{children}</>;
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Use `position: 'head'` (the default)** for scripts that must run before first paint, such as theme detection to avoid flash-of-unstyled-content:
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
useHtml({
|
|
506
|
+
script: [
|
|
507
|
+
{
|
|
508
|
+
content: `
|
|
509
|
+
const theme = localStorage.getItem('theme') ?? 'light';
|
|
510
|
+
document.documentElement.classList.add(theme);
|
|
511
|
+
`,
|
|
512
|
+
// position defaults to 'head' — runs before the page renders
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
});
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Both head and body scripts are re-executed on every HMR update and SPA navigation so they always reflect the current page state.
|
|
519
|
+
|
|
437
520
|
---
|
|
438
521
|
|
|
439
522
|
## Configuration
|
|
@@ -512,8 +595,7 @@ dist/
|
|
|
512
595
|
├── api/ # Bundled API route handlers (.mjs)
|
|
513
596
|
├── pages/ # Bundled page handlers (.mjs)
|
|
514
597
|
├── static/
|
|
515
|
-
│ ├──
|
|
516
|
-
│ ├── __n.js # NukeJS client runtime
|
|
598
|
+
│ ├── __n.js # NukeJS client runtime (React + NukeJS bundled together)
|
|
517
599
|
│ ├── __client-component/ # Bundled "use client" component files
|
|
518
600
|
│ └── <app/public files> # Copied from app/public/ at build time
|
|
519
601
|
├── manifest.json # Route dispatch table
|
|
@@ -521,6 +603,7 @@ dist/
|
|
|
521
603
|
```
|
|
522
604
|
|
|
523
605
|
### Vercel
|
|
606
|
+
|
|
524
607
|
Just import the code from GitHub.
|
|
525
608
|
|
|
526
609
|
### Environment variables
|
|
@@ -532,4 +615,4 @@ Just import the code from GitHub.
|
|
|
532
615
|
|
|
533
616
|
## License
|
|
534
617
|
|
|
535
|
-
MIT
|
|
618
|
+
MIT
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import useRouter from "./use-router.js";
|
|
3
4
|
const Link = ({ href, children, className }) => {
|
|
5
|
+
const r = useRouter();
|
|
4
6
|
const handleClick = (e) => {
|
|
5
7
|
e.preventDefault();
|
|
6
|
-
|
|
8
|
+
r.push(href);
|
|
7
9
|
};
|
|
8
10
|
return /* @__PURE__ */ jsx("a", { href, onClick: handleClick, className, children });
|
|
9
11
|
};
|
package/dist/Link.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/Link.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\r\nimport useRouter from \"./use-router\"\r\n\r\nconst Link = ({ href, children, className }: {\r\n href: string;\r\n children: React.ReactNode;\r\n className?: string;\r\n}) => {\r\n const r = useRouter()\r\n const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\r\n e.preventDefault();\r\n r.push(href);\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;\r\n"],
|
|
5
|
+
"mappings": ";AAcQ;AAbR,OAAO,eAAe;AAEtB,MAAM,OAAO,CAAC,EAAE,MAAM,UAAU,UAAU,MAIpC;AACF,QAAM,IAAI,UAAU;AACpB,QAAM,cAAc,CAAC,MAA2C;AAC5D,MAAE,eAAe;AACjB,MAAE,KAAK,IAAI;AAAA,EACf;AACA,SACI,oBAAC,OAAE,MAAY,SAAS,aAAa,WAChC,UACL;AAER;AAEA,IAAO,eAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/app.d.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* 1. Loads your nuke.config.ts (or uses sensible defaults)
|
|
6
6
|
* 2. Discovers API route prefixes from your server directory
|
|
7
7
|
* 3. Starts an HTTP server that handles:
|
|
8
|
+
* app/public/** — static files (highest priority, via middleware)
|
|
8
9
|
* /__hmr_ping — heartbeat for HMR reconnect polling
|
|
9
10
|
* /__react.js — bundled React + ReactDOM (resolved via importmap)
|
|
10
11
|
* /__n.js — NukeJS client runtime bundle
|
|
11
12
|
* /__client-component/* — on-demand "use client" component bundles
|
|
12
|
-
*
|
|
13
|
-
* /** — SSR pages from app/pages
|
|
13
|
+
* server/** — API route handlers from serverDir
|
|
14
|
+
* /** — SSR pages from app/pages (lowest priority)
|
|
14
15
|
* 4. Watches for file changes and broadcasts HMR events to connected browsers
|
|
15
16
|
*
|
|
16
17
|
* In production (ENVIRONMENT=production), HMR and all file watching are skipped.
|
package/dist/app.js
CHANGED
|
@@ -26,17 +26,7 @@ log.info(` - Debug level: ${String(getDebugLevel())}`);
|
|
|
26
26
|
log.info(` - Dev mode: ${String(isDev)}`);
|
|
27
27
|
if (isDev) watchDir(path.resolve("./app"), "App");
|
|
28
28
|
const apiPrefixes = discoverApiPrefixes(SERVER_DIR);
|
|
29
|
-
const handleApiRoute = createApiHandler({ apiPrefixes, port: PORT });
|
|
30
|
-
if (isDev && existsSync(SERVER_DIR)) {
|
|
31
|
-
watch(SERVER_DIR, { recursive: true }, (_event, filename) => {
|
|
32
|
-
if (!filename) return;
|
|
33
|
-
const ext = path.extname(filename);
|
|
34
|
-
if (ext !== ".ts" && ext !== ".tsx") return;
|
|
35
|
-
const fresh = discoverApiPrefixes(SERVER_DIR);
|
|
36
|
-
apiPrefixes.splice(0, apiPrefixes.length, ...fresh);
|
|
37
|
-
log.info("[Server] Routes updated (" + fresh.length + " prefix" + (fresh.length === 1 ? "" : "es") + ")");
|
|
38
|
-
});
|
|
39
|
-
}
|
|
29
|
+
const handleApiRoute = createApiHandler({ apiPrefixes, port: PORT, isDev });
|
|
40
30
|
log.info(`API prefixes discovered: ${apiPrefixes.length === 0 ? "none" : ""}`);
|
|
41
31
|
apiPrefixes.forEach((p) => {
|
|
42
32
|
log.info(` - ${p.prefix || "/"} -> ${path.relative(process.cwd(), p.directory)}`);
|
|
@@ -62,8 +52,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
62
52
|
const middlewareHandled = await runMiddleware(req, res);
|
|
63
53
|
if (middlewareHandled) return;
|
|
64
54
|
const url = req.url || "/";
|
|
65
|
-
if (matchApiPrefix(url, apiPrefixes))
|
|
66
|
-
return await handleApiRoute(url, req, res);
|
|
67
55
|
if (url === "/__hmr_ping") {
|
|
68
56
|
res.setHeader("Content-Type", "text/plain");
|
|
69
57
|
res.end("ok");
|
|
@@ -78,6 +66,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
78
66
|
url.slice(20).split("?")[0].replace(".js", ""),
|
|
79
67
|
res
|
|
80
68
|
);
|
|
69
|
+
if (matchApiPrefix(url, apiPrefixes))
|
|
70
|
+
return await handleApiRoute(url, req, res);
|
|
81
71
|
return await serverSideRender(url, res, PAGES_DIR, isDev);
|
|
82
72
|
} catch (error) {
|
|
83
73
|
log.error("Server error:", error);
|
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 * /__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 * /api/** \u2014 API route handlers from serverDir\r\n * /** \u2014 SSR pages from app/pages\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 });\r\n\r\nif (isDev && existsSync(SERVER_DIR)) {\r\n watch(SERVER_DIR, { recursive: true }, (_event, filename) => {\r\n if (!filename) return;\r\n\r\n // Only react to TypeScript source changes, not compiled output or assets.\r\n const ext = path.extname(filename);\r\n if (ext !== '.ts' && ext !== '.tsx') return;\r\n\r\n const fresh = discoverApiPrefixes(SERVER_DIR);\r\n apiPrefixes.splice(0, apiPrefixes.length, ...fresh);\r\n log.info('[Server] Routes updated (' + fresh.length + ' prefix' + (fresh.length === 1 ? '' : 'es') + ')');\r\n });\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 // API routes \u2014 prefixes discovered from serverDir take priority over pages.\r\n if (matchApiPrefix(url, apiPrefixes))\r\n return await handleApiRoute(url, req, res);\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\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 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 // No API prefix 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": "
|
|
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": "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",
|
|
6
6
|
"names": ["isDev"]
|
|
7
7
|
}
|
package/dist/build-common.d.ts
CHANGED
|
@@ -65,6 +65,12 @@ export declare function findPageLayouts(routeFilePath: string, pagesDir: string)
|
|
|
65
65
|
/**
|
|
66
66
|
* Extracts the identifier used as the default export from a component file.
|
|
67
67
|
* Returns null when no default export is found.
|
|
68
|
+
*
|
|
69
|
+
* Handles three formats so that components compiled by esbuild are recognised
|
|
70
|
+
* alongside hand-written source files:
|
|
71
|
+
* 1. Source: `export default function Foo` / `export default Foo`
|
|
72
|
+
* 2. esbuild: `var Foo_default = Foo` (compiled arrow-function component)
|
|
73
|
+
* 3. Re-export: `export { Foo as default }`
|
|
68
74
|
*/
|
|
69
75
|
export declare function extractDefaultExportName(filePath: string): string | null;
|
|
70
76
|
/**
|
package/dist/build-common.js
CHANGED
|
@@ -136,7 +136,13 @@ function findPageLayouts(routeFilePath, pagesDir) {
|
|
|
136
136
|
}
|
|
137
137
|
function extractDefaultExportName(filePath) {
|
|
138
138
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
139
|
-
|
|
139
|
+
let m = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
|
|
140
|
+
if (m?.[1]) return m[1];
|
|
141
|
+
m = content.match(/var\s+\w+_default\s*=\s*(\w+)/);
|
|
142
|
+
if (m?.[1]) return m[1];
|
|
143
|
+
m = content.match(/export\s*\{[^}]*\b(\w+)\s+as\s+default\b[^}]*\}/);
|
|
144
|
+
if (m?.[1] && !m[1].endsWith("_default")) return m[1];
|
|
145
|
+
return null;
|
|
140
146
|
}
|
|
141
147
|
function collectServerPages(pagesDir) {
|
|
142
148
|
if (!fs.existsSync(pagesDir)) return [];
|
|
@@ -257,6 +263,8 @@ function makePageAdapterSource(opts) {
|
|
|
257
263
|
catchAllNames
|
|
258
264
|
} = opts;
|
|
259
265
|
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
266
|
+
import { createElement as __createElement__ } from 'react';
|
|
267
|
+
import { renderToString as __renderToString__ } from 'react-dom/server';
|
|
260
268
|
import * as __page__ from ${pageImport};
|
|
261
269
|
${layoutImports}
|
|
262
270
|
|
|
@@ -404,7 +412,7 @@ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
|
|
|
404
412
|
const serializedProps = serializeProps(props ?? {});
|
|
405
413
|
let ssrHtml: string;
|
|
406
414
|
try {
|
|
407
|
-
ssrHtml =
|
|
415
|
+
ssrHtml = __renderToString__(__createElement__(type as any, props || {}));
|
|
408
416
|
} catch {
|
|
409
417
|
ssrHtml = PRERENDERED_HTML[clientId] ?? '';
|
|
410
418
|
}
|
|
@@ -445,20 +453,26 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
445
453
|
let appHtml = '';
|
|
446
454
|
const store = await runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); });
|
|
447
455
|
|
|
448
|
-
const pageTitle = resolveTitle(store.titleOps, '
|
|
456
|
+
const pageTitle = resolveTitle(store.titleOps, 'NukeJS');
|
|
457
|
+
const headScripts = store.script.filter((s: any) => (s.position ?? 'head') === 'head');
|
|
458
|
+
const bodyScripts = store.script.filter((s: any) => s.position === 'body');
|
|
449
459
|
const headLines = [
|
|
450
460
|
' <meta charset="utf-8" />',
|
|
451
461
|
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
452
462
|
\` <title>\${escapeHtml(pageTitle)}</title>\`,
|
|
453
|
-
...(store.meta.length || store.link.length || store.style.length ||
|
|
463
|
+
...(store.meta.length || store.link.length || store.style.length || headScripts.length ? [
|
|
454
464
|
' <!--n-head-->',
|
|
455
465
|
...store.meta.map(renderMetaTag),
|
|
456
466
|
...store.link.map(renderLinkTag),
|
|
457
467
|
...store.style.map(renderStyleTag),
|
|
458
|
-
...
|
|
468
|
+
...headScripts.map(renderScriptTag),
|
|
459
469
|
' <!--/n-head-->',
|
|
460
470
|
] : []),
|
|
461
471
|
];
|
|
472
|
+
const bodyScriptLines = bodyScripts.length
|
|
473
|
+
? [' <!--n-body-scripts-->', ...bodyScripts.map(renderScriptTag), ' <!--/n-body-scripts-->']
|
|
474
|
+
: [];
|
|
475
|
+
const bodyScriptsHtml = bodyScriptLines.length ? '\\n' + bodyScriptLines.join('\\n') + '\\n' : '';
|
|
462
476
|
|
|
463
477
|
const runtimeData = JSON.stringify({
|
|
464
478
|
hydrateIds: [...hydrated], allIds: ALL_CLIENT_IDS, url, params, debug: 'silent',
|
|
@@ -490,7 +504,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
490
504
|
const data = JSON.parse(document.getElementById('__n_data').textContent);
|
|
491
505
|
await initRuntime(data);
|
|
492
506
|
</script>
|
|
493
|
-
</body>
|
|
507
|
+
\${bodyScriptsHtml}</body>
|
|
494
508
|
</html>\`;
|
|
495
509
|
|
|
496
510
|
res.statusCode = 200;
|