nukejs 0.0.23 → 0.0.25
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 +244 -0
- package/dist/build-common.js +12 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -428,7 +428,61 @@ cartStore.setState(s => ({ ...s, total: s.total + 5 }));
|
|
|
428
428
|
| `store.setState(updater)` | Updates state and notifies all subscribers |
|
|
429
429
|
| `store.subscribe(listener)` | Registers a listener; returns an unsubscribe function |
|
|
430
430
|
|
|
431
|
+
### createPersistedStore
|
|
431
432
|
|
|
433
|
+
`createPersistedStore` is a drop-in replacement for `createStore` that survives full page refreshes by mirroring state into `localStorage` (or `sessionStorage`). A plain `createStore` lives only in `window.__nukeStores` and is wiped on every hard reload — fine for SPA navigations, not for data you want to keep around.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
import { createPersistedStore } from 'nukejs';
|
|
437
|
+
|
|
438
|
+
// Persists to localStorage under the key "nuke-store:cart"
|
|
439
|
+
export const cartStore = createPersistedStore('cart', {
|
|
440
|
+
items: [] as CartItem[],
|
|
441
|
+
total: 0,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Cleared when the tab closes, kept across refreshes within the session
|
|
445
|
+
export const draftStore = createPersistedStore('draft', { text: '' }, {
|
|
446
|
+
storage: 'session',
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Custom storage key
|
|
450
|
+
export const settingsStore = createPersistedStore('settings', { theme: 'light' }, {
|
|
451
|
+
key: 'my-app:settings',
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
| Option | Type | Default | Description |
|
|
456
|
+
|---|---|---|---|
|
|
457
|
+
| `storage` | `'local' \| 'session'` | `'local'` | Storage backend — `localStorage` or `sessionStorage` |
|
|
458
|
+
| `key` | `string` | `nuke-store:{name}` | Override the key used in storage |
|
|
459
|
+
|
|
460
|
+
On first mount, the persisted value is read from storage and applied via `setState` immediately after the store is created. On every subsequent state change the new value is written back. The `initialState` you pass in is still used as the SSR snapshot, so there are no hydration mismatches — components simply re-render with the persisted value after mount.
|
|
461
|
+
|
|
462
|
+
```tsx
|
|
463
|
+
// app/components/ThemeToggle.tsx
|
|
464
|
+
"use client";
|
|
465
|
+
import { useStore } from 'nukejs';
|
|
466
|
+
import { settingsStore } from '../stores/settings';
|
|
467
|
+
|
|
468
|
+
export default function ThemeToggle() {
|
|
469
|
+
const { theme } = useStore(settingsStore);
|
|
470
|
+
|
|
471
|
+
return (
|
|
472
|
+
<button onClick={() =>
|
|
473
|
+
settingsStore.setState(s => ({ ...s, theme: s.theme === 'light' ? 'dark' : 'light' }))
|
|
474
|
+
}>
|
|
475
|
+
Switch to {theme === 'light' ? 'dark' : 'light'} mode
|
|
476
|
+
</button>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
The selected theme persists across hard reloads without any extra wiring.
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## API Routes
|
|
432
486
|
|
|
433
487
|
Export named HTTP method handlers from `.ts` files in your `server/` directory.
|
|
434
488
|
|
|
@@ -522,6 +576,66 @@ export default async function middleware(
|
|
|
522
576
|
|
|
523
577
|
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.
|
|
524
578
|
|
|
579
|
+
### URL rewriting
|
|
580
|
+
|
|
581
|
+
Middleware can transparently rewrite the incoming URL by mutating `req.url`. The rewritten URL is used for routing — the browser's address bar is unchanged:
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
// middleware.ts
|
|
585
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
586
|
+
|
|
587
|
+
export default async function middleware(req: IncomingMessage, res: ServerResponse) {
|
|
588
|
+
// Redirect legacy paths to their new equivalents without a browser redirect
|
|
589
|
+
if (req.url === '/old-page') {
|
|
590
|
+
req.url = '/new-page'; // routed to app/pages/new-page.tsx, URL stays /old-page
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Normalize trailing slashes
|
|
594
|
+
if (req.url && req.url !== '/' && req.url.endsWith('/')) {
|
|
595
|
+
req.url = req.url.slice(0, -1);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### CORS & OPTIONS handling
|
|
601
|
+
|
|
602
|
+
A common pattern for API routes is to set CORS headers and short-circuit `OPTIONS` preflight requests in middleware:
|
|
603
|
+
|
|
604
|
+
```ts
|
|
605
|
+
// middleware.ts
|
|
606
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
607
|
+
|
|
608
|
+
export default async function middleware(req: IncomingMessage, res: ServerResponse) {
|
|
609
|
+
if (req.url?.startsWith('/api/')) {
|
|
610
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
611
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
612
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
613
|
+
|
|
614
|
+
if (req.method === 'OPTIONS') {
|
|
615
|
+
res.statusCode = 204;
|
|
616
|
+
res.end();
|
|
617
|
+
return; // short-circuits routing
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### Maintenance mode
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
// middleware.ts
|
|
627
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
628
|
+
|
|
629
|
+
export default async function middleware(req: IncomingMessage, res: ServerResponse) {
|
|
630
|
+
// Bypass maintenance for internal NukeJS routes (/__hmr, /__client-component/*)
|
|
631
|
+
if (process.env.MAINTENANCE_MODE === 'true' && !req.url?.startsWith('/__')) {
|
|
632
|
+
res.statusCode = 503;
|
|
633
|
+
res.setHeader('Content-Type', 'text/html');
|
|
634
|
+
res.end('<h1>503 — Down for maintenance</h1><p>Back shortly.</p>');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
525
639
|
---
|
|
526
640
|
|
|
527
641
|
## Static Files
|
|
@@ -664,6 +778,33 @@ useHtml({
|
|
|
664
778
|
|
|
665
779
|
Both head and body scripts are re-executed on every HMR update and SPA navigation so they always reflect the current page state.
|
|
666
780
|
|
|
781
|
+
### Inline style injection
|
|
782
|
+
|
|
783
|
+
The `style` option injects `<style>` blocks into `<head>`. This is useful for critical CSS that must be present before first paint, or for component-scoped styles that vary per page:
|
|
784
|
+
|
|
785
|
+
```tsx
|
|
786
|
+
// app/pages/dashboard.tsx
|
|
787
|
+
import { useHtml } from 'nukejs';
|
|
788
|
+
|
|
789
|
+
export default function Dashboard() {
|
|
790
|
+
useHtml({
|
|
791
|
+
style: [
|
|
792
|
+
{ content: `.chart { height: 400px; }` },
|
|
793
|
+
{ content: `.sidebar { width: 260px; }`, media: '(min-width: 768px)' },
|
|
794
|
+
],
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
return <main>...</main>;
|
|
798
|
+
}
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
Each `StyleTag` entry supports:
|
|
802
|
+
|
|
803
|
+
| Field | Type | Description |
|
|
804
|
+
|---|---|---|
|
|
805
|
+
| `content` | `string` | Raw CSS injected inside a `<style>` block |
|
|
806
|
+
| `media` | `string` | Optional `media` attribute (e.g. `'print'`, `'(prefers-color-scheme: dark)'`) |
|
|
807
|
+
|
|
667
808
|
---
|
|
668
809
|
|
|
669
810
|
## Configuration
|
|
@@ -708,8 +849,24 @@ export default function Nav() {
|
|
|
708
849
|
}
|
|
709
850
|
```
|
|
710
851
|
|
|
852
|
+
`<Link>` accepts a `className` prop for styling:
|
|
853
|
+
|
|
854
|
+
```tsx
|
|
855
|
+
<Link href="/pricing" className="nav-link nav-link--active">
|
|
856
|
+
Pricing
|
|
857
|
+
</Link>
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
| Prop | Type | Description |
|
|
861
|
+
|---|---|---|
|
|
862
|
+
| `href` | `string` | Destination URL |
|
|
863
|
+
| `children` | `React.ReactNode` | Link content |
|
|
864
|
+
| `className` | `string` (optional) | CSS class(es) applied to the underlying `<a>` element |
|
|
865
|
+
|
|
711
866
|
### useRouter
|
|
712
867
|
|
|
868
|
+
`useRouter()` gives client components programmatic control over navigation. It can only be used inside `"use client"` components.
|
|
869
|
+
|
|
713
870
|
```tsx
|
|
714
871
|
"use client";
|
|
715
872
|
import { useRouter } from 'nukejs';
|
|
@@ -724,6 +881,93 @@ export default function SearchForm() {
|
|
|
724
881
|
}
|
|
725
882
|
```
|
|
726
883
|
|
|
884
|
+
The hook returns an object with the following properties and methods:
|
|
885
|
+
|
|
886
|
+
| | Type | Description |
|
|
887
|
+
|---|---|---|
|
|
888
|
+
| `router.path` | `string` | The current pathname — reactive, updates on every SPA navigation |
|
|
889
|
+
| `router.push(url)` | `(url: string) => void` | Navigate to a new URL, adding an entry to the browser history |
|
|
890
|
+
| `router.replace(url)` | `(url: string) => void` | Navigate without adding a history entry (replaces the current one) |
|
|
891
|
+
| `router.back()` | `() => void` | Go back one entry in the browser history (same as `window.history.back()`) |
|
|
892
|
+
| `router.refresh()` | `() => void` | Re-trigger the current route without a URL change — useful after a mutation |
|
|
893
|
+
|
|
894
|
+
#### `router.replace` — navigation without a history entry
|
|
895
|
+
|
|
896
|
+
Use `replace` when you want to update the URL (e.g. after a form submit or login redirect) without letting the user go "back" to the previous state:
|
|
897
|
+
|
|
898
|
+
```tsx
|
|
899
|
+
"use client";
|
|
900
|
+
import { useRouter } from 'nukejs';
|
|
901
|
+
|
|
902
|
+
export default function LoginForm() {
|
|
903
|
+
const router = useRouter();
|
|
904
|
+
|
|
905
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
906
|
+
e.preventDefault();
|
|
907
|
+
await login();
|
|
908
|
+
router.replace('/dashboard'); // can't go back to /login
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
return <form onSubmit={handleSubmit}>...</form>;
|
|
912
|
+
}
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
#### `router.back` — back button
|
|
916
|
+
|
|
917
|
+
```tsx
|
|
918
|
+
"use client";
|
|
919
|
+
import { useRouter } from 'nukejs';
|
|
920
|
+
|
|
921
|
+
export default function BackButton() {
|
|
922
|
+
const { back } = useRouter();
|
|
923
|
+
return <button onClick={back}>← Go back</button>;
|
|
924
|
+
}
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
#### `router.refresh` — re-render after a mutation
|
|
928
|
+
|
|
929
|
+
`refresh` replays the current route without changing the URL. Use it after a server mutation to re-fetch and display the updated data:
|
|
930
|
+
|
|
931
|
+
```tsx
|
|
932
|
+
"use client";
|
|
933
|
+
import { useRouter } from 'nukejs';
|
|
934
|
+
|
|
935
|
+
export default function DeleteButton({ postId }: { postId: string }) {
|
|
936
|
+
const { refresh } = useRouter();
|
|
937
|
+
|
|
938
|
+
async function handleDelete() {
|
|
939
|
+
await fetch(`/api/posts/${postId}`, { method: 'DELETE' });
|
|
940
|
+
refresh(); // re-renders the page so the deleted post disappears
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return <button onClick={handleDelete}>Delete post</button>;
|
|
944
|
+
}
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
#### `router.path` — reactive current path
|
|
948
|
+
|
|
949
|
+
`router.path` stays in sync with the current pathname across SPA navigations — no `window.location` polling required:
|
|
950
|
+
|
|
951
|
+
```tsx
|
|
952
|
+
"use client";
|
|
953
|
+
import { useRouter } from 'nukejs';
|
|
954
|
+
|
|
955
|
+
export default function NavLink({ href, label }: { href: string; label: string }) {
|
|
956
|
+
const { path, push } = useRouter();
|
|
957
|
+
const isActive = path === href;
|
|
958
|
+
|
|
959
|
+
return (
|
|
960
|
+
<a
|
|
961
|
+
href={href}
|
|
962
|
+
onClick={(e) => { e.preventDefault(); push(href); }}
|
|
963
|
+
style={{ fontWeight: isActive ? 'bold' : 'normal' }}
|
|
964
|
+
>
|
|
965
|
+
{label}
|
|
966
|
+
</a>
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
727
971
|
---
|
|
728
972
|
|
|
729
973
|
## useRequest() — URL Params, Query & Headers
|
package/dist/build-common.js
CHANGED
|
@@ -424,11 +424,19 @@ function renderStyleTag(tag: any): string {
|
|
|
424
424
|
// <!--/n-body-scripts-->) are preserved \u2014 the client runtime needs them for
|
|
425
425
|
// head diffing during soft navigation.
|
|
426
426
|
function minifyHtml(h: string): string {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
.
|
|
430
|
-
|
|
427
|
+
const pres: string[] = [];
|
|
428
|
+
const withoutPres = h.replace(/<pre[\\s\\S]*?<\\/pre>/g, (m) => {
|
|
429
|
+
pres.push(m);
|
|
430
|
+
return '<!--n-pre-' + (pres.length - 1) + '-->';
|
|
431
|
+
});
|
|
432
|
+
const minified = withoutPres
|
|
433
|
+
.replace(/<!--(?!(n-head|\\/n-head|n-body-scripts|\\/n-body-scripts|n-pre-))[\\s\\S]*?-->/g, '')
|
|
434
|
+
.replace(/\\s*\\n\\s*/g, ' ')
|
|
435
|
+
.replace(/>\\s+</g, '><')
|
|
431
436
|
.trim();
|
|
437
|
+
return pres.length === 0
|
|
438
|
+
? minified
|
|
439
|
+
: minified.replace(/<!--n-pre-(\\d+)-->/g, (_, i) => pres[+i]);
|
|
432
440
|
}
|
|
433
441
|
|
|
434
442
|
// \u2500\u2500\u2500 Renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nukejs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "A minimal, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|