nukejs 0.0.5 → 0.0.7

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.
Files changed (42) hide show
  1. package/README.md +96 -8
  2. package/dist/Link.js +16 -0
  3. package/dist/Link.js.map +7 -0
  4. package/dist/build-common.d.ts +57 -80
  5. package/dist/build-common.js +156 -168
  6. package/dist/build-common.js.map +2 -2
  7. package/dist/build-node.d.ts +14 -0
  8. package/dist/build-node.js +50 -51
  9. package/dist/build-node.js.map +2 -2
  10. package/dist/build-vercel.d.ts +18 -0
  11. package/dist/build-vercel.js +76 -63
  12. package/dist/build-vercel.js.map +2 -2
  13. package/dist/builder.d.ts +10 -0
  14. package/dist/builder.js +31 -62
  15. package/dist/builder.js.map +3 -3
  16. package/dist/bundle.js +69 -6
  17. package/dist/bundle.js.map +2 -2
  18. package/dist/component-analyzer.d.ts +13 -10
  19. package/dist/component-analyzer.js +26 -17
  20. package/dist/component-analyzer.js.map +2 -2
  21. package/dist/hmr-bundle.js +17 -4
  22. package/dist/hmr-bundle.js.map +2 -2
  23. package/dist/html-store.d.ts +7 -0
  24. package/dist/html-store.js.map +2 -2
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +2 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/renderer.js +2 -7
  29. package/dist/renderer.js.map +2 -2
  30. package/dist/router.js +16 -4
  31. package/dist/router.js.map +2 -2
  32. package/dist/ssr.js +21 -4
  33. package/dist/ssr.js.map +2 -2
  34. package/dist/use-html.js +5 -1
  35. package/dist/use-html.js.map +2 -2
  36. package/dist/use-router.js +28 -0
  37. package/dist/use-router.js.map +7 -0
  38. package/package.json +1 -1
  39. package/dist/as-is/Link.tsx +0 -20
  40. package/dist/as-is/useRouter.ts +0 -33
  41. /package/dist/{as-is/Link.d.ts → Link.d.ts} +0 -0
  42. /package/dist/{as-is/useRouter.d.ts → use-router.d.ts} +0 -0
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
- # ☢️ NukeJS
1
+ [![NukeJS Banner](.github/banner.png)](https://nukejs.com)
2
+
3
+ # NukeJS
2
4
 
3
5
  A **minimal**, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.
4
6
 
5
7
  ```
6
- npm create nuke
8
+ npm create nuke@latest
7
9
  ```
8
10
 
9
11
  ## Table of Contents
@@ -19,6 +21,7 @@ npm create nuke
19
21
  - [Static Files](#static-files)
20
22
  - [useHtml() — Head Management](#usehtml--head-management)
21
23
  - [Configuration](#configuration)
24
+ - [Link Component & Navigation](#link-component--navigation)
22
25
  - [Building & Deploying](#building--deploying)
23
26
 
24
27
  ## Overview
@@ -129,8 +132,9 @@ Each `.tsx` file in `app/pages/` maps to a URL route:
129
132
  | `about.tsx` | `/about` |
130
133
  | `blog/index.tsx` | `/blog` |
131
134
  | `blog/[slug].tsx` | `/blog/:slug` |
132
- | `docs/[...path].tsx` | `/docs/*` (catch-all) |
133
- | `files/[[...path]].tsx` | `/files` or `/files/*` (optional) |
135
+ | `docs/[...path].tsx` | `/docs/*` (catch-all, required) |
136
+ | `users/[[id]].tsx` | `/users` or `/users/42` (optional single segment) |
137
+ | `files/[[...path]].tsx` | `/files` or `/files/*` (optional catch-all) |
134
138
 
135
139
  ### Page component
136
140
 
@@ -151,6 +155,28 @@ export default async function BlogPost({ slug }: { slug: string }) {
151
155
 
152
156
  Route params are passed as props to the component.
153
157
 
158
+ ### Query string params
159
+
160
+ 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.
161
+
162
+ ```tsx
163
+ // app/pages/search.tsx
164
+ // URL: /search?q=nuke&page=2
165
+ export default function Search({ q, page }: { q: string; page: string }) {
166
+ return <h1>Results for "{q}" — page {page}</h1>;
167
+ }
168
+ ```
169
+
170
+ ```tsx
171
+ // app/pages/blog/[slug].tsx
172
+ // URL: /blog/hello-world?preview=true
173
+ export default function BlogPost({ slug, preview }: { slug: string; preview?: string }) {
174
+ return <article data-preview={preview}>{slug}</article>;
175
+ }
176
+ ```
177
+
178
+ A query param that appears multiple times (e.g. `?tag=a&tag=b`) is passed as a `string[]`.
179
+
154
180
  ### Catch-all routes
155
181
 
156
182
  ```tsx
@@ -168,9 +194,12 @@ When multiple routes could match a URL, the most specific one wins:
168
194
  ```
169
195
  /users/profile → users/profile.tsx (static, wins)
170
196
  /users/42 → users/[id].tsx (dynamic)
197
+ /users → users/[[id]].tsx (optional single, matches with no id)
171
198
  /users/a/b/c → users/[...rest].tsx (catch-all)
172
199
  ```
173
200
 
201
+ Specificity order, highest to lowest: static → `[param]` → `[[param]]` → `[...catchAll]` → `[[...optionalCatchAll]]`.
202
+
174
203
  ---
175
204
 
176
205
  ## Layouts
@@ -383,13 +412,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
383
412
  | `nuke build` (Node) | Copied to `dist/static/` and served by the production HTTP server |
384
413
  | `nuke build` (Vercel) | Copied to `.vercel/output/static/` — served by Vercel's CDN, no function invocation |
385
414
 
386
- On Vercel, public files receive the same zero-latency CDN treatment as `__react.js` and `__n.js`.
415
+ On Vercel, public files receive the same zero-latency CDN treatment as `__n.js`.
387
416
 
388
417
  ---
389
418
 
390
419
  ## useHtml() — Head Management
391
420
 
392
- The `useHtml()` hook works in both server components and client components to control the document head.
421
+ 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>`.
393
422
 
394
423
  ```tsx
395
424
  import { useHtml } from 'nukejs';
@@ -428,6 +457,65 @@ Result: "Home | Site"
428
457
 
429
458
  The page title always serves as the base value; layout functions wrap it outward.
430
459
 
460
+ ### Script injection & position
461
+
462
+ 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:
463
+
464
+ | `position` | Where it's injected |
465
+ |---|---|
466
+ | `'head'` (default) | Inside `<head>`, in the managed `<!--n-head-->` block |
467
+ | `'body'` | End of `<body>`, just before `</body>`, in the `<!--n-body-scripts-->` block |
468
+
469
+ **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.
470
+
471
+ ```tsx
472
+ // app/pages/layout.tsx — Google Analytics on every page
473
+ import { useHtml } from 'nukejs';
474
+
475
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
476
+ useHtml({
477
+ script: [
478
+ // Load the gtag library — async so it doesn't block rendering
479
+ {
480
+ src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX',
481
+ async: true,
482
+ position: 'body',
483
+ },
484
+ // Inline initialisation — must follow the loader above
485
+ {
486
+ content: `
487
+ window.dataLayer = window.dataLayer || [];
488
+ function gtag(){dataLayer.push(arguments);}
489
+ gtag('js', new Date());
490
+ gtag('config', 'G-XXXXXXXXXX');
491
+ `,
492
+ position: 'body',
493
+ },
494
+ ],
495
+ });
496
+
497
+ return <>{children}</>;
498
+ }
499
+ ```
500
+
501
+ **Use `position: 'head'` (the default)** for scripts that must run before first paint, such as theme detection to avoid flash-of-unstyled-content:
502
+
503
+ ```tsx
504
+ useHtml({
505
+ script: [
506
+ {
507
+ content: `
508
+ const theme = localStorage.getItem('theme') ?? 'light';
509
+ document.documentElement.classList.add(theme);
510
+ `,
511
+ // position defaults to 'head' — runs before the page renders
512
+ },
513
+ ],
514
+ });
515
+ ```
516
+
517
+ Both head and body scripts are re-executed on every HMR update and SPA navigation so they always reflect the current page state.
518
+
431
519
  ---
432
520
 
433
521
  ## Configuration
@@ -506,8 +594,7 @@ dist/
506
594
  ├── api/ # Bundled API route handlers (.mjs)
507
595
  ├── pages/ # Bundled page handlers (.mjs)
508
596
  ├── static/
509
- │ ├── __react.js # Bundled React runtime
510
- │ ├── __n.js # NukeJS client runtime
597
+ │ ├── __n.js # NukeJS client runtime (React + NukeJS bundled together)
511
598
  │ ├── __client-component/ # Bundled "use client" component files
512
599
  │ └── <app/public files> # Copied from app/public/ at build time
513
600
  ├── manifest.json # Route dispatch table
@@ -515,6 +602,7 @@ dist/
515
602
  ```
516
603
 
517
604
  ### Vercel
605
+
518
606
  Just import the code from GitHub.
519
607
 
520
608
  ### Environment variables
package/dist/Link.js ADDED
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import useRouter from "./use-router.js";
4
+ const Link = ({ href, children, className }) => {
5
+ const r = useRouter();
6
+ const handleClick = (e) => {
7
+ e.preventDefault();
8
+ r.push(href);
9
+ };
10
+ return /* @__PURE__ */ jsx("a", { href, onClick: handleClick, className, children });
11
+ };
12
+ var Link_default = Link;
13
+ export {
14
+ Link_default as default
15
+ };
16
+ //# sourceMappingURL=Link.js.map
@@ -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
+ }
@@ -1,41 +1,59 @@
1
1
  /**
2
- * build-common.ts
2
+ * build-common.ts — Shared Build Logic
3
3
  *
4
- * Shared build logic used by both build-vercel.ts and build-node.ts.
4
+ * Used by both build-node.ts and build-vercel.ts.
5
5
  *
6
6
  * Exports:
7
- * — utility helpers : walkFiles, analyzeFile, isServerComponent,
8
- * findPageLayouts, extractDefaultExportName
9
- * — collection : collectServerPages, collectGlobalClientRegistry
10
- * — template codegen : makeApiAdapterSource, makePageAdapterSource
11
- * — bundle operations : bundleApiHandler, bundlePageHandler,
12
- * bundleClientComponents, buildCombinedBundle
7
+ * — types : AnalyzedRoute, ServerPage, BuiltPage,
8
+ * PageAdapterOptions, PageBundleOptions
9
+ * — utility helpers : walkFiles, analyzeFile, isServerComponent,
10
+ * findPageLayouts, extractDefaultExportName
11
+ * — collection : collectServerPages, collectGlobalClientRegistry,
12
+ * buildPerPageRegistry
13
+ * — template codegen : makeApiAdapterSource, makePageAdapterSource
14
+ * — bundle ops : bundleApiHandler, bundlePageHandler,
15
+ * bundleClientComponents, buildPages,
16
+ * buildCombinedBundle, copyPublicFiles
13
17
  */
14
18
  export interface AnalyzedRoute {
19
+ /** Regex string matching the URL path, e.g. '^/users/([^/]+)$' */
15
20
  srcRegex: string;
21
+ /** Names of captured groups in srcRegex order */
16
22
  paramNames: string[];
17
- /** Path used as function namespace, e.g. '/api/users' or '/page/about'. */
23
+ /**
24
+ * Subset of paramNames that are catch-all ([...slug] or [[...path]]).
25
+ * Their runtime values are string[] not string.
26
+ */
27
+ catchAllNames: string[];
28
+ /** Function namespace path, e.g. '/api/users' or '/page/about' */
18
29
  funcPath: string;
19
30
  specificity: number;
20
31
  }
21
32
  export interface ServerPage extends AnalyzedRoute {
22
33
  absPath: string;
23
34
  }
24
- /** A server page together with its fully bundled ESM text, ready to emit. */
25
35
  export interface BuiltPage extends ServerPage {
26
36
  bundleText: string;
27
37
  }
28
38
  export declare function walkFiles(dir: string, base?: string): string[];
29
39
  /**
30
40
  * Parses dynamic-route segments from a relative file path and returns a regex,
31
- * captured param names, a function path, and a specificity score.
41
+ * captured param names, catch-all param names, a function path, and a
42
+ * specificity score.
43
+ *
44
+ * Supported patterns per segment:
45
+ * [[...name]] optional catch-all → regex (.*) → string[]
46
+ * [...name] required catch-all → regex (.+) → string[]
47
+ * [[name]] optional single → regex ([^/]*)? → string
48
+ * [name] required single → regex ([^/]+) → string
49
+ * literal static → escaped literal
32
50
  *
33
51
  * @param relPath Relative path from the dir root (e.g. 'users/[id].tsx').
34
52
  * @param prefix Namespace for funcPath ('api' | 'page').
35
53
  */
36
54
  export declare function analyzeFile(relPath: string, prefix?: string): AnalyzedRoute;
37
55
  /**
38
- * Returns true when a file does NOT begin with a "use client" directive
56
+ * Returns true when a file does NOT begin with a "use client" directive,
39
57
  * i.e. it is a server component.
40
58
  */
41
59
  export declare function isServerComponent(filePath: string): boolean;
@@ -47,26 +65,29 @@ export declare function findPageLayouts(routeFilePath: string, pagesDir: string)
47
65
  /**
48
66
  * Extracts the identifier used as the default export from a component file.
49
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 }`
50
74
  */
51
75
  export declare function extractDefaultExportName(filePath: string): string | null;
52
76
  /**
53
- * Returns all server-component pages inside `pagesDir`, sorted by specificity
54
- * (most-specific first so more-precise routes shadow catch-alls in routers).
77
+ * Returns all server-component pages inside `pagesDir`, sorted most-specific
78
+ * first so precise routes shadow catch-alls in routers.
55
79
  * layout.tsx files and "use client" files are excluded.
56
80
  */
57
81
  export declare function collectServerPages(pagesDir: string): ServerPage[];
58
82
  /**
59
83
  * Walks every server page and its layout chain to collect all client component
60
- * IDs reachable anywhere in the app. Deduplication is automatic because the
61
- * Map key is the stable content-hash ID produced by component-analyzer.ts.
84
+ * IDs reachable anywhere in the app.
62
85
  */
63
86
  export declare function collectGlobalClientRegistry(serverPages: ServerPage[], pagesDir: string): Map<string, string>;
64
87
  /**
65
- * Builds the per-page client component registry (page + its layout chain) and
66
- * returns both the id→path map and the name→id map needed by bundlePageHandler.
67
- *
68
- * Extracted here to eliminate the identical loop duplicated across
69
- * build-node.ts and build-vercel.ts.
88
+ * Builds the per-page client component registry (page + its layout chain)
89
+ * and returns both the id→path map and the name→id map needed by
90
+ * bundlePageHandler.
70
91
  */
71
92
  export declare function buildPerPageRegistry(absPath: string, layoutPaths: string[], pagesDir: string): {
72
93
  registry: Map<string, string>;
@@ -79,19 +100,11 @@ export declare function buildPerPageRegistry(absPath: string, layoutPaths: strin
79
100
  * and collects pre-rendered HTML for each.
80
101
  * Pass 2 — bundles every server-component page into a self-contained ESM
81
102
  * handler and returns the results as `BuiltPage[]`.
82
- *
83
- * Callers (build-node, build-vercel) only need to write the bundled text to
84
- * their respective output destinations — the format-specific logic stays local.
85
- *
86
- * Returns an empty array when there are no server pages.
87
103
  */
88
104
  export declare function buildPages(pagesDir: string, staticDir: string): Promise<BuiltPage[]>;
89
105
  /**
90
106
  * Returns the TypeScript source for a thin HTTP adapter that wraps an API
91
107
  * route module and exposes a single `handler(req, res)` default export.
92
- *
93
- * @param handlerFilename Basename of the handler file relative to the adapter
94
- * (e.g. 'users.ts'). Must be in the same directory.
95
108
  */
96
109
  export declare function makeApiAdapterSource(handlerFilename: string): string;
97
110
  export interface PageAdapterOptions {
@@ -107,27 +120,24 @@ export interface PageAdapterOptions {
107
120
  layoutArrayItems: string;
108
121
  /** Pre-rendered HTML per client component ID, computed at build time */
109
122
  prerenderedHtml: Record<string, string>;
123
+ /** Catch-all param names whose runtime values are string[] not string */
124
+ catchAllNames: string[];
110
125
  }
111
126
  /**
112
127
  * Returns the TypeScript source for a fully self-contained page handler.
113
128
  *
114
129
  * The adapter:
115
130
  * • Inlines the html-store so useHtml() works without external deps.
116
- * • Contains an async recursive renderer that handles server + client
117
- * components without react-dom/server.
131
+ * • Contains an async recursive renderer for server + client components.
118
132
  * • Client components are identified via the pre-computed CLIENT_COMPONENTS
119
- * map (no fs.readFileSync at runtime).
120
- * • Emits the same full HTML document structure as ssr.ts including the
121
- * __n_data blob, importmap, and initRuntime bootstrap.
133
+ * map no fs.readFileSync at runtime.
134
+ * • Emits the full HTML document including the __n_data blob and bootstrap.
122
135
  */
123
136
  export declare function makePageAdapterSource(opts: PageAdapterOptions): string;
124
137
  /**
125
138
  * Bundles an API route handler into a single self-contained ESM string.
126
- *
127
- * Writes a temporary adapter next to `absPath`, bundles them together with
128
- * esbuild (node_modules kept external), then removes the temp file.
129
- *
130
- * @returns The bundled ESM text ready to write to disk.
139
+ * node_modules are kept external — they exist at runtime on both Node and
140
+ * Vercel (Vercel bundles them separately via the pages dispatcher).
131
141
  */
132
142
  export declare function bundleApiHandler(absPath: string): Promise<string>;
133
143
  export interface PageBundleOptions {
@@ -137,59 +147,26 @@ export interface PageBundleOptions {
137
147
  allClientIds: string[];
138
148
  layoutPaths: string[];
139
149
  prerenderedHtml: Record<string, string>;
150
+ catchAllNames: string[];
140
151
  }
141
152
  /**
142
153
  * Bundles a server-component page into a single self-contained ESM string.
143
- *
144
- * Writes a temporary adapter next to `absPath` (so relative imports inside
145
- * the component resolve from the correct base directory), bundles it with
146
- * esbuild (React and all npm deps inlined, only Node built-ins stay external),
147
- * then removes the temp file.
148
- *
149
- * @returns The bundled ESM text ready to write to disk.
154
+ * All npm packages are kept external — the Node production server has
155
+ * node_modules available at runtime.
150
156
  */
151
157
  export declare function bundlePageHandler(opts: PageBundleOptions): Promise<string>;
152
158
  /**
153
159
  * Bundles every client component in `globalRegistry` to
154
- * `<staticDir>/__client-component/<id>.js`.
155
- *
156
- * Mirrors bundleClientComponent() in bundler.ts:
157
- * • browser ESM, JSX automatic
158
- * • react / react-dom/client / react/jsx-runtime kept external so the
159
- * importmap can resolve them to the already-loaded /__react.js bundle.
160
+ * `<staticDir>/__client-component/<id>.js` and pre-renders each to HTML.
160
161
  */
161
162
  export declare function bundleClientComponents(globalRegistry: Map<string, string>, pagesDir: string, staticDir: string): Promise<Map<string, string>>;
162
163
  /**
163
- * Builds a single combined browser bundle to `<staticDir>/__n.js`.
164
- *
165
- * Inlines the full React + ReactDOM runtime together with the NukeJS client
166
- * runtime (bundle.ts) so the browser only needs one file instead of two.
167
- * The importmap in every page points 'react', 'react-dom/client',
168
- * 'react/jsx-runtime', and 'nukejs' all to /__n.js, so dynamic imports
169
- * inside the runtime (e.g. `await import('react')`) hit the module cache
170
- * and return the same singleton that was already loaded.
171
- *
172
- * Dev mode (bundler.ts) keeps separate /__react.js and /__n.js files for
173
- * easier debugging — this function is production-only.
164
+ * Builds the combined browser bundle (__n.js) that contains the full React
165
+ * runtime + NukeJS client runtime in a single file.
174
166
  */
175
167
  export declare function buildCombinedBundle(staticDir: string): Promise<void>;
176
168
  /**
177
- * Recursively copies every file from `app/public/` into `destDir`,
178
- * preserving the directory structure.
179
- *
180
- * Called by both build-vercel.ts (dest = .vercel/output/static/) and
181
- * build-node.ts (dest = dist/static/) so that:
182
- *
183
- * app/public/favicon.ico → <destDir>/favicon.ico
184
- * app/public/images/logo.png → <destDir>/images/logo.png
185
- *
186
- * On Vercel, the Build Output API v3 serves everything in .vercel/output/static/
187
- * directly — no route entry needed, same as __react.js and __n.js.
188
- *
189
- * On Node, the serverEntry template serves files from dist/static/ with the
190
- * same MIME-type logic as the dev middleware.
191
- *
192
- * Skips silently when the public directory does not exist so projects without
193
- * one don't need any special configuration.
169
+ * Recursively copies every file from `publicDir` into `destDir`, preserving
170
+ * the directory structure. Skips silently when `publicDir` does not exist.
194
171
  */
195
172
  export declare function copyPublicFiles(publicDir: string, destDir: string): void;