nukejs 0.0.23 → 0.0.24

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 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
@@ -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
- return h
428
- .replace(/<!--(?!(n-head|\\/n-head|n-body-scripts|\\/n-body-scripts))[\\s\\S]*?-->/g, '')
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, '')
429
434
  .replace(/>\\s+</g, '> <')
430
435
  .replace(/\\s*\\n\\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.23",
3
+ "version": "0.0.24",
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",