nukejs 0.0.1 → 0.0.2
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/LICENSE +21 -0
- package/README.md +529 -0
- package/bin/index.mjs +126 -0
- package/dist/app.d.ts +18 -0
- package/dist/app.js +124 -0
- package/dist/app.js.map +7 -0
- package/dist/as-is/Link.d.ts +6 -0
- package/dist/as-is/Link.tsx +20 -0
- package/dist/as-is/useRouter.d.ts +7 -0
- package/dist/as-is/useRouter.ts +33 -0
- package/dist/build-common.d.ts +192 -0
- package/dist/build-common.js +737 -0
- package/dist/build-common.js.map +7 -0
- package/dist/build-node.d.ts +1 -0
- package/dist/build-node.js +170 -0
- package/dist/build-node.js.map +7 -0
- package/dist/build-vercel.d.ts +1 -0
- package/dist/build-vercel.js +65 -0
- package/dist/build-vercel.js.map +7 -0
- package/dist/builder.d.ts +1 -0
- package/dist/builder.js +97 -0
- package/dist/builder.js.map +7 -0
- package/dist/bundle.d.ts +68 -0
- package/dist/bundle.js +166 -0
- package/dist/bundle.js.map +7 -0
- package/dist/bundler.d.ts +58 -0
- package/dist/bundler.js +98 -0
- package/dist/bundler.js.map +7 -0
- package/dist/component-analyzer.d.ts +72 -0
- package/dist/component-analyzer.js +102 -0
- package/dist/component-analyzer.js.map +7 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.js +30 -0
- package/dist/config.js.map +7 -0
- package/dist/hmr-bundle.d.ts +25 -0
- package/dist/hmr-bundle.js +76 -0
- package/dist/hmr-bundle.js.map +7 -0
- package/dist/hmr.d.ts +55 -0
- package/dist/hmr.js +62 -0
- package/dist/hmr.js.map +7 -0
- package/dist/html-store.d.ts +121 -0
- package/dist/html-store.js +42 -0
- package/dist/html-store.js.map +7 -0
- package/dist/http-server.d.ts +99 -0
- package/dist/http-server.js +166 -0
- package/dist/http-server.js.map +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +7 -0
- package/dist/logger.d.ts +58 -0
- package/dist/logger.js +53 -0
- package/dist/logger.js.map +7 -0
- package/dist/metadata.d.ts +50 -0
- package/dist/metadata.js +43 -0
- package/dist/metadata.js.map +7 -0
- package/dist/middleware-loader.d.ts +50 -0
- package/dist/middleware-loader.js +50 -0
- package/dist/middleware-loader.js.map +7 -0
- package/dist/middleware.d.ts +22 -0
- package/dist/middleware.example.d.ts +8 -0
- package/dist/middleware.example.js +58 -0
- package/dist/middleware.example.js.map +7 -0
- package/dist/middleware.js +59 -0
- package/dist/middleware.js.map +7 -0
- package/dist/renderer.d.ts +44 -0
- package/dist/renderer.js +130 -0
- package/dist/renderer.js.map +7 -0
- package/dist/router.d.ts +84 -0
- package/dist/router.js +104 -0
- package/dist/router.js.map +7 -0
- package/dist/ssr.d.ts +39 -0
- package/dist/ssr.js +168 -0
- package/dist/ssr.js.map +7 -0
- package/dist/use-html.d.ts +64 -0
- package/dist/use-html.js +125 -0
- package/dist/use-html.js.map +7 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.js +62 -0
- package/dist/utils.js.map +7 -0
- package/package.json +64 -12
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NukeJS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
# ☢️ NukeJS
|
|
2
|
+
|
|
3
|
+
A **minimal**, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm create nuke
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Overview](#overview)
|
|
12
|
+
- [Getting Started](#getting-started)
|
|
13
|
+
- [Project Structure](#project-structure)
|
|
14
|
+
- [Pages & Routing](#pages--routing)
|
|
15
|
+
- [Layouts](#layouts)
|
|
16
|
+
- [Client Components](#client-components)
|
|
17
|
+
- [API Routes](#api-routes)
|
|
18
|
+
- [Middleware](#middleware)
|
|
19
|
+
- [Static Files](#static-files)
|
|
20
|
+
- [useHtml() — Head Management](#usehtml--head-management)
|
|
21
|
+
- [Configuration](#configuration)
|
|
22
|
+
- [Building & Deploying](#building--deploying)
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
NukeJS gives you:
|
|
27
|
+
|
|
28
|
+
| Feature | Description |
|
|
29
|
+
|---|---|
|
|
30
|
+
| **File-based routing** | Pages in `app/pages/`, API in `server/` |
|
|
31
|
+
| **Server-side rendering** | All pages rendered to HTML on the server |
|
|
32
|
+
| **Partial hydration** | Only `"use client"` components download JS |
|
|
33
|
+
| **SPA navigation** | Client-side page transitions after first load |
|
|
34
|
+
| **Hot module replacement** | Instant page updates during development |
|
|
35
|
+
| **Zero config** | Works out of the box; `nuke.config.ts` for overrides |
|
|
36
|
+
| **Deploy anywhere** | Node.js or Vercel serverless |
|
|
37
|
+
|
|
38
|
+
### The core idea
|
|
39
|
+
|
|
40
|
+
Most pages don't need JavaScript. NukeJS renders your entire React tree to HTML on the server, and only ships JavaScript for components explicitly marked `"use client"`. Everything else stays server-only — no hydration cost, no JS bundle for static content.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// app/pages/index.tsx — Server component (zero JS sent to browser)
|
|
44
|
+
export default async function Home() {
|
|
45
|
+
const posts = await db.getPosts(); // runs on server only
|
|
46
|
+
return (
|
|
47
|
+
<main>
|
|
48
|
+
<h1>Blog</h1>
|
|
49
|
+
{posts.map(p => <PostCard key={p.id} post={p} />)}
|
|
50
|
+
<LikeButton postId={posts[0].id} /> {/* ← this one is interactive */}
|
|
51
|
+
</main>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// app/components/LikeButton.tsx — Client component (JS downloaded)
|
|
58
|
+
"use client";
|
|
59
|
+
import { useState } from 'react';
|
|
60
|
+
|
|
61
|
+
export default function LikeButton({ postId }: { postId: string }) {
|
|
62
|
+
const [liked, setLiked] = useState(false);
|
|
63
|
+
return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Getting Started
|
|
70
|
+
|
|
71
|
+
### Prerequisites
|
|
72
|
+
|
|
73
|
+
- Node.js 20+
|
|
74
|
+
- React 19+
|
|
75
|
+
- esbuild (peer dependency)
|
|
76
|
+
|
|
77
|
+
### Installation
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm create nuke
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Running the dev server
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm run dev
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The server starts on port 3000 by default (auto-increments if in use).
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Project Structure
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
my-app/
|
|
97
|
+
├── app/
|
|
98
|
+
│ ├── pages/ # Page components (file-based routing)
|
|
99
|
+
│ │ ├── layout.tsx # Root layout (wraps every page)
|
|
100
|
+
│ │ ├── index.tsx # → /
|
|
101
|
+
│ │ ├── about.tsx # → /about
|
|
102
|
+
│ │ └── blog/
|
|
103
|
+
│ │ ├── layout.tsx # Blog section layout
|
|
104
|
+
│ │ ├── index.tsx # → /blog
|
|
105
|
+
│ │ └── [slug].tsx # → /blog/:slug
|
|
106
|
+
│ ├── components/ # Shared components (not routed)
|
|
107
|
+
│ └── public/ # Static files served at root (e.g. /favicon.ico)
|
|
108
|
+
├── server/ # API route handlers
|
|
109
|
+
│ ├── users/
|
|
110
|
+
│ │ ├── index.ts # → GET/POST /users
|
|
111
|
+
│ │ └── [id].ts # → GET/PUT/DELETE /users/:id
|
|
112
|
+
│ └── auth.ts # → /auth
|
|
113
|
+
├── middleware.ts # (optional) global request middleware
|
|
114
|
+
├── nuke.config.ts # (optional) configuration
|
|
115
|
+
└── package.json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Pages & Routing
|
|
121
|
+
|
|
122
|
+
### Basic pages
|
|
123
|
+
|
|
124
|
+
Each `.tsx` file in `app/pages/` maps to a URL route:
|
|
125
|
+
|
|
126
|
+
| File | URL |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `index.tsx` | `/` |
|
|
129
|
+
| `about.tsx` | `/about` |
|
|
130
|
+
| `blog/index.tsx` | `/blog` |
|
|
131
|
+
| `blog/[slug].tsx` | `/blog/:slug` |
|
|
132
|
+
| `docs/[...path].tsx` | `/docs/*` (catch-all) |
|
|
133
|
+
| `files/[[...path]].tsx` | `/files` or `/files/*` (optional) |
|
|
134
|
+
|
|
135
|
+
### Page component
|
|
136
|
+
|
|
137
|
+
A page exports a default React component. It may be async (runs on the server).
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// app/pages/blog/[slug].tsx
|
|
141
|
+
export default async function BlogPost({ slug }: { slug: string }) {
|
|
142
|
+
const post = await fetchPost(slug);
|
|
143
|
+
return (
|
|
144
|
+
<article>
|
|
145
|
+
<h1>{post.title}</h1>
|
|
146
|
+
<p>{post.content}</p>
|
|
147
|
+
</article>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Route params are passed as props to the component.
|
|
153
|
+
|
|
154
|
+
### Catch-all routes
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
// app/pages/docs/[...path].tsx
|
|
158
|
+
export default function Docs({ path }: { path: string[] }) {
|
|
159
|
+
// path = ['getting-started', 'installation'] for /docs/getting-started/installation
|
|
160
|
+
return <DocViewer segments={path} />;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Route specificity
|
|
165
|
+
|
|
166
|
+
When multiple routes could match a URL, the most specific one wins:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
/users/profile → users/profile.tsx (static, wins)
|
|
170
|
+
/users/42 → users/[id].tsx (dynamic)
|
|
171
|
+
/users/a/b/c → users/[...rest].tsx (catch-all)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Layouts
|
|
177
|
+
|
|
178
|
+
Place a `layout.tsx` alongside your pages to wrap a group of routes.
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
// app/pages/layout.tsx — Wraps every page
|
|
182
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
183
|
+
return (
|
|
184
|
+
<div>
|
|
185
|
+
<Nav />
|
|
186
|
+
<main>{children}</main>
|
|
187
|
+
<Footer />
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Layouts nest automatically. A page at `blog/[slug].tsx` gets wrapped by both `layout.tsx` (root) and `blog/layout.tsx` (blog section).
|
|
194
|
+
|
|
195
|
+
### Title templates in layouts
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
// app/pages/layout.tsx
|
|
199
|
+
import { useHtml } from 'nukejs';
|
|
200
|
+
|
|
201
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
202
|
+
useHtml({ title: (prev) => `${prev} | Acme Corp` });
|
|
203
|
+
return <>{children}</>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// app/pages/about.tsx
|
|
207
|
+
export default function About() {
|
|
208
|
+
useHtml({ title: 'About Us' });
|
|
209
|
+
// Final title: "About Us | Acme Corp"
|
|
210
|
+
return <h1>About</h1>;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Client Components
|
|
217
|
+
|
|
218
|
+
Add `"use client"` as the very first line of any component file to make it a **client component**. NukeJS will:
|
|
219
|
+
|
|
220
|
+
1. Bundle that file separately and serve it as `/__client-component/<id>.js`
|
|
221
|
+
2. Render a `<span data-hydrate-id="…">` placeholder in the server HTML
|
|
222
|
+
3. Hydrate the placeholder with React in the browser
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
"use client";
|
|
226
|
+
import { useState, useEffect } from 'react';
|
|
227
|
+
|
|
228
|
+
export default function Counter({ initial = 0 }: { initial?: number }) {
|
|
229
|
+
const [count, setCount] = useState(initial);
|
|
230
|
+
return (
|
|
231
|
+
<div>
|
|
232
|
+
<p>Count: {count}</p>
|
|
233
|
+
<button onClick={() => setCount(c => c + 1)}>+</button>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Rules for client components
|
|
240
|
+
|
|
241
|
+
- The `"use client"` directive must be the **first non-comment line**
|
|
242
|
+
- The component must have a **named default export** (NukeJS uses the function name to match props during hydration)
|
|
243
|
+
- Props must be **JSON-serializable** (no functions, no class instances)
|
|
244
|
+
- React elements passed as props are supported (serialized and reconstructed)
|
|
245
|
+
|
|
246
|
+
### Passing children to client components
|
|
247
|
+
|
|
248
|
+
Children and other React elements can be passed as props — NukeJS serializes them at render time:
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
// Server component
|
|
252
|
+
<Modal>
|
|
253
|
+
<p>This content is from the server</p>
|
|
254
|
+
</Modal>
|
|
255
|
+
|
|
256
|
+
// Modal is a "use client" component; its children are serialized as
|
|
257
|
+
// { __re: 'html', tag: 'p', props: { children: 'This content...' } }
|
|
258
|
+
// and reconstructed in the browser before mounting.
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## API Routes
|
|
264
|
+
|
|
265
|
+
Export named HTTP method handlers from `.ts` files in your `server/` directory.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// server/users/index.ts
|
|
269
|
+
import type { ApiRequest, ApiResponse } from 'nukejs';
|
|
270
|
+
|
|
271
|
+
export async function GET(req: ApiRequest, res: ApiResponse) {
|
|
272
|
+
const users = await db.getUsers();
|
|
273
|
+
res.json(users);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function POST(req: ApiRequest, res: ApiResponse) {
|
|
277
|
+
const user = await db.createUser(req.body);
|
|
278
|
+
res.json(user, 201);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
// server/users/[id].ts
|
|
284
|
+
export async function GET(req: ApiRequest, res: ApiResponse) {
|
|
285
|
+
const { id } = req.params as { id: string };
|
|
286
|
+
const user = await db.getUser(id);
|
|
287
|
+
if (!user) { res.json({ error: 'Not found' }, 404); return; }
|
|
288
|
+
res.json(user);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function DELETE(req: ApiRequest, res: ApiResponse) {
|
|
292
|
+
await db.deleteUser(req.params!.id as string);
|
|
293
|
+
res.status(204).end();
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Request object
|
|
298
|
+
|
|
299
|
+
| Property | Type | Description |
|
|
300
|
+
|---|---|---|
|
|
301
|
+
| `req.body` | `any` | Parsed JSON body (or raw string), up to 10 MB |
|
|
302
|
+
| `req.params` | `Record<string, string \| string[]>` | Dynamic route segments |
|
|
303
|
+
| `req.query` | `Record<string, string>` | URL search params |
|
|
304
|
+
| `req.method` | `string` | HTTP method |
|
|
305
|
+
| `req.headers` | `IncomingHttpHeaders` | Request headers |
|
|
306
|
+
|
|
307
|
+
### Response object
|
|
308
|
+
|
|
309
|
+
| Method | Description |
|
|
310
|
+
|---|---|
|
|
311
|
+
| `res.json(data, status?)` | Send a JSON response (default status 200) |
|
|
312
|
+
| `res.status(code)` | Set status code and return `res` for chaining |
|
|
313
|
+
| `res.setHeader(name, value)` | Set a response header |
|
|
314
|
+
| `res.end(body?)` | Send raw response |
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Middleware
|
|
319
|
+
|
|
320
|
+
Create `middleware.ts` in your project root to intercept every request before routing:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
// middleware.ts
|
|
324
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
325
|
+
|
|
326
|
+
export default async function middleware(
|
|
327
|
+
req: IncomingMessage,
|
|
328
|
+
res: ServerResponse,
|
|
329
|
+
): Promise<void> {
|
|
330
|
+
// Logging
|
|
331
|
+
console.log(`${req.method} ${req.url}`);
|
|
332
|
+
|
|
333
|
+
// Auth guard
|
|
334
|
+
if (req.url?.startsWith('/admin') && !isAuthenticated(req)) {
|
|
335
|
+
res.statusCode = 401;
|
|
336
|
+
res.end('Unauthorized');
|
|
337
|
+
return; // End response to halt further processing
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Header injection (let request continue without ending it)
|
|
341
|
+
res.setHeader('X-Powered-By', 'nukejs');
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
If `res.end()` (or `res.json()`) is called, NukeJS stops processing and does not handle the request through routing. If middleware returns without ending the response, the request continues to API routes or SSR.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Static Files
|
|
350
|
+
|
|
351
|
+
Place any file in `app/public/` and it will be served directly at its path relative to that directory — no route file needed.
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
app/public/
|
|
355
|
+
├── favicon.ico → GET /favicon.ico
|
|
356
|
+
├── robots.txt → GET /robots.txt
|
|
357
|
+
├── logo.png → GET /logo.png
|
|
358
|
+
└── fonts/
|
|
359
|
+
└── inter.woff2 → GET /fonts/inter.woff2
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Every file type is served with the correct `Content-Type` automatically (images, fonts, CSS, video, audio, JSON, WASM, etc.).
|
|
363
|
+
|
|
364
|
+
Reference public files directly in your components:
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
368
|
+
return (
|
|
369
|
+
<>
|
|
370
|
+
<link rel="icon" href="/favicon.ico" />
|
|
371
|
+
<img src="/logo.png" alt="Logo" />
|
|
372
|
+
{children}
|
|
373
|
+
</>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Deployment behaviour
|
|
379
|
+
|
|
380
|
+
| Environment | How public files are served |
|
|
381
|
+
|---|---|
|
|
382
|
+
| `nuke dev` | Served by the built-in middleware before any API or SSR routing |
|
|
383
|
+
| `nuke build` (Node) | Copied to `dist/static/` and served by the production HTTP server |
|
|
384
|
+
| `nuke build` (Vercel) | Copied to `.vercel/output/static/` — served by Vercel's CDN, no function invocation |
|
|
385
|
+
|
|
386
|
+
On Vercel, public files receive the same zero-latency CDN treatment as `__react.js` and `__n.js`.
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## useHtml() — Head Management
|
|
391
|
+
|
|
392
|
+
The `useHtml()` hook works in both server components and client components to control the document head.
|
|
393
|
+
|
|
394
|
+
```tsx
|
|
395
|
+
import { useHtml } from 'nukejs';
|
|
396
|
+
|
|
397
|
+
export default function Page() {
|
|
398
|
+
useHtml({
|
|
399
|
+
title: 'My Page',
|
|
400
|
+
|
|
401
|
+
meta: [
|
|
402
|
+
{ name: 'description', content: 'Page description' },
|
|
403
|
+
{ property: 'og:title', content: 'My Page' },
|
|
404
|
+
],
|
|
405
|
+
|
|
406
|
+
link: [
|
|
407
|
+
{ rel: 'canonical', href: 'https://example.com/page' },
|
|
408
|
+
{ rel: 'stylesheet', href: '/styles.css' },
|
|
409
|
+
],
|
|
410
|
+
|
|
411
|
+
htmlAttrs: { lang: 'en', class: 'dark' },
|
|
412
|
+
bodyAttrs: { class: 'page-home' },
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
return <main>...</main>;
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Title resolution order
|
|
420
|
+
|
|
421
|
+
When both a layout and a page call `useHtml({ title })`, they are resolved in this order:
|
|
422
|
+
|
|
423
|
+
```
|
|
424
|
+
Layout: useHtml({ title: (prev) => `${prev} | Site` })
|
|
425
|
+
Page: useHtml({ title: 'Home' })
|
|
426
|
+
Result: "Home | Site"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
The page title always serves as the base value; layout functions wrap it outward.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Configuration
|
|
434
|
+
|
|
435
|
+
Create `nuke.config.ts` in your project root:
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
// nuke.config.ts
|
|
439
|
+
export default {
|
|
440
|
+
// Directory containing API route files (default: './server')
|
|
441
|
+
serverDir: './server',
|
|
442
|
+
|
|
443
|
+
// Port for the dev server (default: 3000, auto-increments if in use)
|
|
444
|
+
port: 3000,
|
|
445
|
+
|
|
446
|
+
// Logging verbosity
|
|
447
|
+
// false — silent (default)
|
|
448
|
+
// 'error' — errors only
|
|
449
|
+
// 'info' — startup messages + errors
|
|
450
|
+
// true — verbose (all debug output)
|
|
451
|
+
debug: false,
|
|
452
|
+
};
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Link Component & Navigation
|
|
458
|
+
|
|
459
|
+
Use the built-in `<Link>` component for client-side navigation (no full page reload):
|
|
460
|
+
|
|
461
|
+
```tsx
|
|
462
|
+
import { Link } from 'nukejs';
|
|
463
|
+
|
|
464
|
+
export default function Nav() {
|
|
465
|
+
return (
|
|
466
|
+
<nav>
|
|
467
|
+
<Link href="/">Home</Link>
|
|
468
|
+
<Link href="/about">About</Link>
|
|
469
|
+
<Link href="/blog">Blog</Link>
|
|
470
|
+
</nav>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### useRouter
|
|
476
|
+
|
|
477
|
+
```tsx
|
|
478
|
+
"use client";
|
|
479
|
+
import { useRouter } from 'nukejs';
|
|
480
|
+
|
|
481
|
+
export default function SearchForm() {
|
|
482
|
+
const router = useRouter();
|
|
483
|
+
return (
|
|
484
|
+
<button onClick={() => router.push('/results?q=nuke')}>
|
|
485
|
+
Search
|
|
486
|
+
</button>
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Building & Deploying
|
|
494
|
+
|
|
495
|
+
### Node.js server
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
npm run build # builds to dist/
|
|
499
|
+
node dist/index.mjs # starts the production server
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
The build output:
|
|
503
|
+
|
|
504
|
+
```
|
|
505
|
+
dist/
|
|
506
|
+
├── api/ # Bundled API route handlers (.mjs)
|
|
507
|
+
├── pages/ # Bundled page handlers (.mjs)
|
|
508
|
+
├── static/
|
|
509
|
+
│ ├── __react.js # Bundled React runtime
|
|
510
|
+
│ ├── __n.js # NukeJS client runtime
|
|
511
|
+
│ ├── __client-component/ # Bundled "use client" component files
|
|
512
|
+
│ └── <app/public files> # Copied from app/public/ at build time
|
|
513
|
+
├── manifest.json # Route dispatch table
|
|
514
|
+
└── index.mjs # HTTP server entry point
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Vercel
|
|
518
|
+
Just import the code from GitHub.
|
|
519
|
+
|
|
520
|
+
### Environment variables
|
|
521
|
+
|
|
522
|
+
| Variable | Description |
|
|
523
|
+
|---|---|
|
|
524
|
+
| `ENVIRONMENT=production` | Disables HMR and file watching |
|
|
525
|
+
| `PORT` | Port for the production server |
|
|
526
|
+
|
|
527
|
+
## License
|
|
528
|
+
|
|
529
|
+
MIT
|
package/bin/index.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const distDir = path.join(__dirname, '../dist');
|
|
10
|
+
const srcDir = path.join(__dirname, '../src');
|
|
11
|
+
|
|
12
|
+
const arg = process.argv[2];
|
|
13
|
+
|
|
14
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function spawnWith(bin, args, extraEnv = {}) {
|
|
17
|
+
const child = spawn(bin, args, {
|
|
18
|
+
stdio: 'inherit',
|
|
19
|
+
cwd: process.cwd(),
|
|
20
|
+
env: { ...process.env, ...extraEnv },
|
|
21
|
+
});
|
|
22
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isWindows = process.platform === 'win32';
|
|
26
|
+
|
|
27
|
+
function resolveBin(name) {
|
|
28
|
+
// On Windows, .bin/ entries are .cmd wrappers — must include the extension
|
|
29
|
+
const candidates = isWindows ? [name + '.cmd', name + '.ps1', name] : [name];
|
|
30
|
+
|
|
31
|
+
const searchDirs = [
|
|
32
|
+
path.join(process.cwd(), 'node_modules', '.bin'), // user's project
|
|
33
|
+
path.join(__dirname, '..', 'node_modules', '.bin'), // nukejs's own deps
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const dir of searchDirs) {
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
const full = path.join(dir, candidate);
|
|
39
|
+
if (fs.existsSync(full)) return full;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// last resort: rely on PATH (works if tsx is installed globally)
|
|
44
|
+
return isWindows ? name + '.cmd' : name;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runWithNode(scriptPath, extraEnv = {}) {
|
|
48
|
+
if (!fs.existsSync(scriptPath)) {
|
|
49
|
+
console.error(`\n ✖ Cannot find ${path.relative(process.cwd(), scriptPath)}`);
|
|
50
|
+
console.error(` Run "nuke build" first.\n`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
spawnWith(process.execPath, [scriptPath], extraEnv);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const RESTART_CODE = 75;
|
|
57
|
+
|
|
58
|
+
function runWithTsx(scriptPath, extraEnv = {}) {
|
|
59
|
+
if (!fs.existsSync(scriptPath)) {
|
|
60
|
+
console.error(`\n ✖ Cannot find ${path.relative(process.cwd(), scriptPath)}`);
|
|
61
|
+
console.error(` Is the nukejs package intact?\n`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tsx = resolveBin('tsx');
|
|
66
|
+
|
|
67
|
+
function launch() {
|
|
68
|
+
// On Windows, .bin/ entries are .cmd wrappers which cannot be spawned
|
|
69
|
+
// directly — they require the shell to interpret them. Rather than
|
|
70
|
+
// setting shell:true (which triggers DEP0190 and passes args through an
|
|
71
|
+
// unescaped shell string), we invoke cmd.exe explicitly with /c so the
|
|
72
|
+
// arguments remain as a proper array and are never concatenated by Node.
|
|
73
|
+
const [bin, args] = isWindows && tsx.endsWith('.cmd')
|
|
74
|
+
? ['cmd.exe', ['/c', tsx, scriptPath]]
|
|
75
|
+
: [tsx, [scriptPath]];
|
|
76
|
+
|
|
77
|
+
const child = spawn(bin, args, {
|
|
78
|
+
stdio: 'inherit',
|
|
79
|
+
cwd: process.cwd(),
|
|
80
|
+
env: { ...process.env, ...extraEnv },
|
|
81
|
+
// shell is always false — cmd.exe /c handles .cmd dispatch on Windows,
|
|
82
|
+
// and Unix never needed it.
|
|
83
|
+
shell: false,
|
|
84
|
+
});
|
|
85
|
+
child.on('exit', (code) => {
|
|
86
|
+
if (code === RESTART_CODE) {
|
|
87
|
+
console.log('\n ↺ Restarting server...\n');
|
|
88
|
+
launch();
|
|
89
|
+
} else {
|
|
90
|
+
process.exit(code ?? 0);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
launch();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── commands ──────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
if (!arg || arg === 'dev') {
|
|
101
|
+
// nuke | nuke dev → prefer src/app.ts (monorepo / local dev),
|
|
102
|
+
// fall back to dist/app.js (installed package)
|
|
103
|
+
const srcEntry = path.join(srcDir, 'app.ts');
|
|
104
|
+
const distEntry = path.join(distDir, 'app.js');
|
|
105
|
+
const devScript = fs.existsSync(srcEntry) ? srcEntry : distEntry;
|
|
106
|
+
runWithTsx(devScript, { ENVIRONMENT: 'development' });
|
|
107
|
+
|
|
108
|
+
} else if (arg === 'build') {
|
|
109
|
+
// nuke build → run compiled dist via plain node
|
|
110
|
+
const isVercel = !!(
|
|
111
|
+
process.env.VERCEL ||
|
|
112
|
+
process.env.VERCEL_ENV ||
|
|
113
|
+
process.env.NOW_BUILDER
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (isVercel) {
|
|
117
|
+
runWithNode(path.join(distDir, 'build-vercel.js'));
|
|
118
|
+
} else {
|
|
119
|
+
runWithNode(path.join(distDir, 'build-node.js'));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
} else {
|
|
123
|
+
console.error(`\n ✖ Unknown command: "${arg}"`);
|
|
124
|
+
console.error(` Usage: nuke [dev|build]\n`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app.ts — NukeJS Dev Server Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This is the runtime that powers `nuke dev`. It:
|
|
5
|
+
* 1. Loads your nuke.config.ts (or uses sensible defaults)
|
|
6
|
+
* 2. Discovers API route prefixes from your server directory
|
|
7
|
+
* 3. Starts an HTTP server that handles:
|
|
8
|
+
* /__hmr_ping — heartbeat for HMR reconnect polling
|
|
9
|
+
* /__react.js — bundled React + ReactDOM (resolved via importmap)
|
|
10
|
+
* /__n.js — NukeJS client runtime bundle
|
|
11
|
+
* /__client-component/* — on-demand "use client" component bundles
|
|
12
|
+
* /api/** — API route handlers from serverDir
|
|
13
|
+
* /** — SSR pages from app/pages
|
|
14
|
+
* 4. Watches for file changes and broadcasts HMR events to connected browsers
|
|
15
|
+
*
|
|
16
|
+
* In production (ENVIRONMENT=production), HMR and all file watching are skipped.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|