nukejs 0.0.14 → 0.0.16
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 +137 -1
- package/dist/app.js +20 -21
- package/dist/build-common.js +6 -3
- package/dist/builder.js +1 -0
- package/dist/bundler.js +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +1 -0
- package/dist/middleware-loader.js +8 -4
- package/dist/store.d.ts +104 -0
- package/dist/store.js +45 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ npm create nuke@latest
|
|
|
17
17
|
- [Pages & Routing](#pages--routing)
|
|
18
18
|
- [Layouts](#layouts)
|
|
19
19
|
- [Client Components](#client-components)
|
|
20
|
+
- [State Management](#state-management)
|
|
20
21
|
- [API Routes](#api-routes)
|
|
21
22
|
- [Middleware](#middleware)
|
|
22
23
|
- [Static Files](#static-files)
|
|
@@ -292,7 +293,142 @@ Children and other React elements can be passed as props — NukeJS serializes t
|
|
|
292
293
|
|
|
293
294
|
---
|
|
294
295
|
|
|
295
|
-
##
|
|
296
|
+
## State Management
|
|
297
|
+
|
|
298
|
+
NukeJS ships a lightweight built-in store for sharing state across client components. Because each `"use client"` component is hydrated into its own independent React root, React Context cannot cross component boundaries — the store solves this.
|
|
299
|
+
|
|
300
|
+
All store state lives in `window.__nukeStores`, so it is shared across every bundle on the page regardless of how many times a store module is evaluated.
|
|
301
|
+
|
|
302
|
+
### createStore
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import { createStore } from 'nukejs';
|
|
306
|
+
|
|
307
|
+
const counterStore = createStore('counter', { count: 0 });
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The first argument is a unique name used to key the store in the global registry. The second is the initial state. If two bundles call `createStore` with the same name, the first one wins and subsequent calls reuse the existing entry.
|
|
311
|
+
|
|
312
|
+
### useStore
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
"use client"
|
|
316
|
+
import { useStore } from 'nukejs';
|
|
317
|
+
import { counterStore } from '../stores/counter';
|
|
318
|
+
|
|
319
|
+
export default function Counter() {
|
|
320
|
+
const { count } = useStore(counterStore);
|
|
321
|
+
return (
|
|
322
|
+
<div>
|
|
323
|
+
<p>{count}</p>
|
|
324
|
+
<button onClick={() => counterStore.setState(s => ({ count: s.count + 1 }))}>
|
|
325
|
+
Increment
|
|
326
|
+
</button>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Pass an optional selector to avoid re-renders when unrelated parts of state change:
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
// Only re-renders when `count` changes, not on any other state update
|
|
336
|
+
const count = useStore(counterStore, s => s.count);
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Sharing state across components
|
|
340
|
+
|
|
341
|
+
Stores shine when two completely separate client components need to stay in sync. Define the store in its own file (no `"use client"` needed) and import it from both:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
// app/stores/cart.ts
|
|
345
|
+
import { createStore } from 'nukejs';
|
|
346
|
+
|
|
347
|
+
export type CartItem = { id: string; name: string; price: number };
|
|
348
|
+
|
|
349
|
+
export const cartStore = createStore('cart', {
|
|
350
|
+
items: [] as CartItem[],
|
|
351
|
+
total: 0,
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
// app/components/AddToCartButton.tsx
|
|
357
|
+
"use client"
|
|
358
|
+
import { cartStore, type CartItem } from '../stores/cart';
|
|
359
|
+
|
|
360
|
+
export default function AddToCartButton({ item }: { item: CartItem }) {
|
|
361
|
+
return (
|
|
362
|
+
<button onClick={() =>
|
|
363
|
+
cartStore.setState(s => ({
|
|
364
|
+
items: [...s.items, item],
|
|
365
|
+
total: s.total + item.price,
|
|
366
|
+
}))
|
|
367
|
+
}>
|
|
368
|
+
Add to cart
|
|
369
|
+
</button>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
// app/components/CartIcon.tsx
|
|
376
|
+
"use client"
|
|
377
|
+
import { useStore } from 'nukejs';
|
|
378
|
+
import { cartStore } from '../stores/cart';
|
|
379
|
+
|
|
380
|
+
export default function CartIcon() {
|
|
381
|
+
const { items, total } = useStore(cartStore);
|
|
382
|
+
return (
|
|
383
|
+
<span>
|
|
384
|
+
🛒 {items.length} items — ${total.toFixed(2)}
|
|
385
|
+
</span>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
```tsx
|
|
391
|
+
// app/pages/shop.tsx — server component, no JS cost
|
|
392
|
+
import AddToCartButton from '../components/AddToCartButton';
|
|
393
|
+
import CartIcon from '../components/CartIcon';
|
|
394
|
+
|
|
395
|
+
export default function ShopPage() {
|
|
396
|
+
const item = { id: '1', name: 'Widget', price: 9.99 };
|
|
397
|
+
return (
|
|
398
|
+
<div>
|
|
399
|
+
<CartIcon />
|
|
400
|
+
<AddToCartButton item={item} />
|
|
401
|
+
</div>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`CartIcon` updates instantly when the button is clicked — they are separate React roots with separate bundles, but they write and read through the same `'cart'` entry in `window.__nukeStores`.
|
|
407
|
+
|
|
408
|
+
### setState
|
|
409
|
+
|
|
410
|
+
Accepts a full replacement value or an updater function:
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
// Replace
|
|
414
|
+
cartStore.setState({ items: [], total: 0 });
|
|
415
|
+
|
|
416
|
+
// Updater — receives current state, returns next state
|
|
417
|
+
cartStore.setState(s => ({ ...s, total: s.total + 5 }));
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### API reference
|
|
421
|
+
|
|
422
|
+
| | Description |
|
|
423
|
+
|---|---|
|
|
424
|
+
| `createStore(name, initialState)` | Creates or retrieves a named store |
|
|
425
|
+
| `useStore(store)` | Subscribes to the full state |
|
|
426
|
+
| `useStore(store, selector)` | Subscribes to a derived slice (re-renders only when slice changes) |
|
|
427
|
+
| `store.getState()` | Returns the current state snapshot |
|
|
428
|
+
| `store.setState(updater)` | Updates state and notifies all subscribers |
|
|
429
|
+
| `store.subscribe(listener)` | Registers a listener; returns an unsubscribe function |
|
|
430
|
+
|
|
431
|
+
|
|
296
432
|
|
|
297
433
|
Export named HTTP method handlers from `.ts` files in your `server/` directory.
|
|
298
434
|
|
package/dist/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from "http";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { existsSync, watch } from "fs";
|
|
4
|
-
import {
|
|
4
|
+
import { c, log, setDebugLevel, getDebugLevel } from "./logger.js";
|
|
5
5
|
import { loadConfig } from "./config.js";
|
|
6
6
|
import { discoverApiPrefixes, matchApiPrefix, createApiHandler } from "./http-server.js";
|
|
7
7
|
import { loadMiddleware, runMiddleware } from "./middleware-loader.js";
|
|
@@ -9,6 +9,7 @@ import { serveReactBundle, serveNukeBundle, serveClientComponentBundle } from ".
|
|
|
9
9
|
import { serverSideRender } from "./ssr.js";
|
|
10
10
|
import { watchDir, broadcastRestart } from "./hmr.js";
|
|
11
11
|
const isDev = process.env.ENVIRONMENT !== "production";
|
|
12
|
+
const _startMs = Date.now();
|
|
12
13
|
if (isDev) {
|
|
13
14
|
const React = await import("react");
|
|
14
15
|
global.React = React;
|
|
@@ -84,30 +85,28 @@ function tryListen(port) {
|
|
|
84
85
|
server.listen(port, () => resolve(port));
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
function printStartupBanner(port,
|
|
88
|
+
function printStartupBanner(port, configuredPort, startMs) {
|
|
88
89
|
const url = `http://localhost:${port}`;
|
|
89
90
|
const level = getDebugLevel();
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
91
|
+
const elapsed = Date.now() - startMs;
|
|
92
|
+
const keyW = 5;
|
|
93
|
+
const visible = (s) => s.replace(/\x1b\[[\d;]*m/g, "").length;
|
|
94
|
+
const row = (key, val) => {
|
|
95
|
+
const pad = " ".repeat(Math.max(0, keyW - visible(key)));
|
|
96
|
+
console.log(` ${c("gray", key)}${pad} ${val}`);
|
|
96
97
|
};
|
|
97
|
-
const row = (content, w = 2) => `${ansi.gray}\u2502${ansi.reset} ${pad(content, innerWidth - w)} ${ansi.gray}\u2502${ansi.reset}`;
|
|
98
|
-
const label = (key, val) => row(`${c("gray", key)} ${val}`);
|
|
99
98
|
console.log("");
|
|
100
|
-
console.log(
|
|
101
|
-
console.log(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
console.log(` ${c("orange", "NukeJS", true)} ${c("gray", `ready in ${elapsed}ms`)}`);
|
|
100
|
+
console.log("");
|
|
101
|
+
row(
|
|
102
|
+
"Local",
|
|
103
|
+
port !== configuredPort ? `${c("cyan", url, true)} ${c("yellow", `(${configuredPort} was in use)`)}` : c("cyan", url, true)
|
|
104
|
+
);
|
|
105
|
+
row(
|
|
106
|
+
"Debug",
|
|
107
|
+
level === false ? c("gray", "off") : level === true ? c("green", "verbose") : c("yellow", String(level))
|
|
108
|
+
);
|
|
110
109
|
console.log("");
|
|
111
110
|
}
|
|
112
111
|
const actualPort = await tryListen(PORT);
|
|
113
|
-
printStartupBanner(actualPort,
|
|
112
|
+
printStartupBanner(actualPort, PORT, _startMs);
|
package/dist/build-common.js
CHANGED
|
@@ -618,7 +618,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
618
618
|
|
|
619
619
|
<script id="__n_data" type="application/json">\${runtimeData}</script>
|
|
620
620
|
|
|
621
|
-
|
|
621
|
+
\${hydrated.size > 0 ? \`<script type="importmap">
|
|
622
622
|
{
|
|
623
623
|
"imports": {
|
|
624
624
|
"react": "/__n.js",
|
|
@@ -633,7 +633,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
633
633
|
const { initRuntime } = await import('nukejs');
|
|
634
634
|
const data = JSON.parse(document.getElementById('__n_data').textContent);
|
|
635
635
|
await initRuntime(data);
|
|
636
|
-
</script
|
|
636
|
+
</script>\` : ''}
|
|
637
637
|
\${bodyScriptsHtml}</body>
|
|
638
638
|
</html>\`;
|
|
639
639
|
|
|
@@ -800,16 +800,19 @@ async function buildCombinedBundle(staticDir) {
|
|
|
800
800
|
stdin: {
|
|
801
801
|
contents: `
|
|
802
802
|
import React, {
|
|
803
|
+
createElement, cloneElement, createRef, isValidElement, Children,
|
|
803
804
|
useState, useEffect, useContext, useReducer, useCallback, useMemo,
|
|
804
805
|
useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
|
|
805
806
|
useDeferredValue, useTransition, useId, useSyncExternalStore,
|
|
806
807
|
useInsertionEffect, createContext, forwardRef, memo, lazy,
|
|
807
|
-
Suspense, Fragment, StrictMode, Component, PureComponent
|
|
808
|
+
Suspense, Fragment, StrictMode, Component, PureComponent,
|
|
809
|
+
createPortal
|
|
808
810
|
} from 'react';
|
|
809
811
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
810
812
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
811
813
|
export { initRuntime, setupLocationChangeMonitor } from "./${bundleFile}.js";
|
|
812
814
|
export {
|
|
815
|
+
createElement, cloneElement, createRef, isValidElement, Children,
|
|
813
816
|
useState, useEffect, useContext, useReducer, useCallback, useMemo,
|
|
814
817
|
useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
|
|
815
818
|
useDeferredValue, useTransition, useId, useSyncExternalStore,
|
package/dist/builder.js
CHANGED
package/dist/bundler.js
CHANGED
|
@@ -37,16 +37,19 @@ async function serveReactBundle(res) {
|
|
|
37
37
|
stdin: {
|
|
38
38
|
contents: `
|
|
39
39
|
import React, {
|
|
40
|
+
createElement, cloneElement, createRef, isValidElement, Children,
|
|
40
41
|
useState, useEffect, useContext, useReducer, useCallback, useMemo,
|
|
41
42
|
useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
|
|
42
43
|
useDeferredValue, useTransition, useId, useSyncExternalStore,
|
|
43
44
|
useInsertionEffect, createContext, forwardRef, memo, lazy,
|
|
44
|
-
Suspense, Fragment, StrictMode, Component, PureComponent
|
|
45
|
+
Suspense, Fragment, StrictMode, Component, PureComponent,
|
|
46
|
+
createPortal
|
|
45
47
|
} from 'react';
|
|
46
48
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
47
49
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
48
50
|
|
|
49
51
|
export {
|
|
52
|
+
createElement, cloneElement, createRef, isValidElement, Children,
|
|
50
53
|
useState, useEffect, useContext, useReducer, useCallback, useMemo,
|
|
51
54
|
useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
|
|
52
55
|
useDeferredValue, useTransition, useId, useSyncExternalStore,
|
|
@@ -75,7 +78,7 @@ async function serveReactBundle(res) {
|
|
|
75
78
|
res.end(await reactBundlePromise);
|
|
76
79
|
}
|
|
77
80
|
async function serveNukeBundle(res) {
|
|
78
|
-
log.verbose("Bundling
|
|
81
|
+
log.verbose("Bundling NukeJS runtime");
|
|
79
82
|
if (!nukeBundlePromise) {
|
|
80
83
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
81
84
|
const entry = path.join(dir, `bundle.${dir.endsWith("dist") ? "js" : "ts"}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { createStore, useStore } from './store';
|
|
2
|
+
export type { Store } from './store';
|
|
1
3
|
export { useHtml } from './use-html';
|
|
2
4
|
export type { HtmlOptions } from './use-html';
|
|
3
5
|
export type { TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag, } from './html-store';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createStore, useStore } from "./store.js";
|
|
1
2
|
import { useHtml } from "./use-html.js";
|
|
2
3
|
import { default as default2 } from "./use-router.js";
|
|
3
4
|
import { useRequest } from "./use-request.js";
|
|
@@ -10,6 +11,7 @@ export {
|
|
|
10
11
|
default3 as Link,
|
|
11
12
|
ansi,
|
|
12
13
|
c,
|
|
14
|
+
createStore,
|
|
13
15
|
escapeHtml,
|
|
14
16
|
getDebugLevel,
|
|
15
17
|
getRequestStore,
|
|
@@ -21,5 +23,6 @@ export {
|
|
|
21
23
|
setupLocationChangeMonitor,
|
|
22
24
|
useHtml,
|
|
23
25
|
useRequest,
|
|
24
|
-
default2 as useRouter
|
|
26
|
+
default2 as useRouter,
|
|
27
|
+
useStore
|
|
25
28
|
};
|
package/dist/logger.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare const ansi: {
|
|
|
26
26
|
readonly cyan: "\u001B[36m";
|
|
27
27
|
readonly white: "\u001B[37m";
|
|
28
28
|
readonly gray: "\u001B[90m";
|
|
29
|
+
readonly orange: "\u001B[38;5;208m";
|
|
29
30
|
readonly bgBlue: "\u001B[44m";
|
|
30
31
|
readonly bgGreen: "\u001B[42m";
|
|
31
32
|
readonly bgMagenta: "\u001B[45m";
|
package/dist/logger.js
CHANGED
|
@@ -11,7 +11,7 @@ async function loadMiddlewareFromPath(middlewarePath) {
|
|
|
11
11
|
try {
|
|
12
12
|
const mod = await import(pathToFileURL(middlewarePath).href);
|
|
13
13
|
if (typeof mod.default === "function") {
|
|
14
|
-
middlewares.push(mod.default);
|
|
14
|
+
middlewares.push({ fn: mod.default, src: middlewarePath });
|
|
15
15
|
log.info(`Middleware loaded from ${middlewarePath}`);
|
|
16
16
|
} else {
|
|
17
17
|
log.warn(`${middlewarePath} does not export a default function`);
|
|
@@ -34,10 +34,14 @@ async function loadMiddleware() {
|
|
|
34
34
|
}
|
|
35
35
|
async function runMiddleware(req, res) {
|
|
36
36
|
if (middlewares.length === 0) return false;
|
|
37
|
-
for (const
|
|
38
|
-
await
|
|
37
|
+
for (const { fn, src } of middlewares) {
|
|
38
|
+
await fn(req, res);
|
|
39
39
|
if (res.writableEnded || res.headersSent) {
|
|
40
|
-
|
|
40
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
41
|
+
const url = req.url ?? "/";
|
|
42
|
+
const status = res.statusCode;
|
|
43
|
+
const srcName = path.basename(src);
|
|
44
|
+
log.verbose(`middleware ${method} ${url} ${status} (${srcName})`);
|
|
41
45
|
return true;
|
|
42
46
|
}
|
|
43
47
|
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* store.ts — NukeJS Client State Management
|
|
3
|
+
*
|
|
4
|
+
* Provides a lightweight, cross-boundary state system for "use client"
|
|
5
|
+
* components. The core problem it solves: NukeJS hydrates every client
|
|
6
|
+
* component into its own independent React root (via hydrateRoot / createRoot),
|
|
7
|
+
* so React Context cannot carry state across component boundaries. Each
|
|
8
|
+
* component's esbuild bundle is also a separate module instance, so a plain
|
|
9
|
+
* module-level variable would not be shared between bundles.
|
|
10
|
+
*
|
|
11
|
+
* Solution: all store state lives in `window.__nukeStores`, a Map that persists
|
|
12
|
+
* for the lifetime of the page regardless of how many times the store module
|
|
13
|
+
* is evaluated. Every bundle that imports `createStore('counter', …)` gets
|
|
14
|
+
* a thin proxy object that reads from and writes to the same backing entry —
|
|
15
|
+
* state is automatically shared across roots and bundles.
|
|
16
|
+
*
|
|
17
|
+
* API:
|
|
18
|
+
*
|
|
19
|
+
* const cartStore = createStore('cart', { items: [], total: 0 });
|
|
20
|
+
*
|
|
21
|
+
* // Inside any "use client" component on the same page:
|
|
22
|
+
* const items = useStore(cartStore, s => s.items);
|
|
23
|
+
* cartStore.setState(s => ({ ...s, items: [...s.items, newItem] }));
|
|
24
|
+
*
|
|
25
|
+
* The store is safe to import in server components — it detects the absence of
|
|
26
|
+
* `window` and returns a lightweight no-op stub so SSR never throws.
|
|
27
|
+
*/
|
|
28
|
+
type Listener = () => void;
|
|
29
|
+
type Unsubscribe = () => void;
|
|
30
|
+
type Updater<T> = T | ((prev: T) => T);
|
|
31
|
+
/**
|
|
32
|
+
* A NukeJS store handle. Create it once at module scope; pass it into
|
|
33
|
+
* `useStore()` inside any client component to subscribe.
|
|
34
|
+
*/
|
|
35
|
+
export interface Store<T extends object> {
|
|
36
|
+
/** Returns the current state snapshot. */
|
|
37
|
+
getState(): T;
|
|
38
|
+
/**
|
|
39
|
+
* Updates the state and notifies every subscriber.
|
|
40
|
+
*
|
|
41
|
+
* Accepts a full replacement value or an updater function:
|
|
42
|
+
* store.setState({ count: 0 })
|
|
43
|
+
* store.setState(s => ({ ...s, count: s.count + 1 }))
|
|
44
|
+
*/
|
|
45
|
+
setState(updater: Updater<T>): void;
|
|
46
|
+
/**
|
|
47
|
+
* Registers a change listener. Returns an unsubscribe function.
|
|
48
|
+
* Compatible with `useSyncExternalStore`.
|
|
49
|
+
*/
|
|
50
|
+
subscribe(listener: Listener): Unsubscribe;
|
|
51
|
+
/** The name this store is registered under in the global registry. */
|
|
52
|
+
readonly name: string;
|
|
53
|
+
/**
|
|
54
|
+
* The value passed to `createStore` as its second argument.
|
|
55
|
+
* Used internally as the server snapshot so `useSyncExternalStore` always
|
|
56
|
+
* reconciles against a value that matches the server-rendered HTML —
|
|
57
|
+
* the server never has mutations, so initial state is always what it renders.
|
|
58
|
+
*/
|
|
59
|
+
readonly initialState: T;
|
|
60
|
+
}
|
|
61
|
+
interface StoreEntry<T> {
|
|
62
|
+
state: T;
|
|
63
|
+
listeners: Set<Listener>;
|
|
64
|
+
}
|
|
65
|
+
declare global {
|
|
66
|
+
interface Window {
|
|
67
|
+
__nukeStores?: Map<string, StoreEntry<any>>;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Creates (or retrieves) a named store backed by the page-global registry.
|
|
72
|
+
*
|
|
73
|
+
* If a store with the given `name` already exists in the registry (e.g.
|
|
74
|
+
* because another bundle called `createStore` first), the existing entry is
|
|
75
|
+
* reused and `initialState` is ignored. This means the first bundle to run
|
|
76
|
+
* wins the initial value — define your stores in a single shared file, or
|
|
77
|
+
* treat `initialState` as a consistent default across all bundles.
|
|
78
|
+
*
|
|
79
|
+
* @param name A unique string key for this store.
|
|
80
|
+
* @param initialState Default state used when the store is first created.
|
|
81
|
+
*/
|
|
82
|
+
export declare function createStore<T extends object>(name: string, initialState: T): Store<T>;
|
|
83
|
+
/**
|
|
84
|
+
* React hook that subscribes a component to a store.
|
|
85
|
+
*
|
|
86
|
+
* An optional `selector` lets you derive a slice of state. The component
|
|
87
|
+
* only re-renders when the selected value changes (by reference equality),
|
|
88
|
+
* not on every store mutation.
|
|
89
|
+
*
|
|
90
|
+
* Works across independent React roots — any component on the page that calls
|
|
91
|
+
* `useStore` with the same store will re-render when that store changes,
|
|
92
|
+
* regardless of which component boundary each lives in.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* // Full state
|
|
96
|
+
* const state = useStore(cartStore);
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* // Selected slice — re-renders only when `items` changes
|
|
100
|
+
* const items = useStore(cartStore, s => s.items);
|
|
101
|
+
*/
|
|
102
|
+
export declare function useStore<T extends object>(store: Store<T>): T;
|
|
103
|
+
export declare function useStore<T extends object, U>(store: Store<T>, selector: (state: T) => U): U;
|
|
104
|
+
export {};
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
function getRegistry() {
|
|
3
|
+
if (typeof window === "undefined") {
|
|
4
|
+
return /* @__PURE__ */ new Map();
|
|
5
|
+
}
|
|
6
|
+
if (!window.__nukeStores) {
|
|
7
|
+
window.__nukeStores = /* @__PURE__ */ new Map();
|
|
8
|
+
}
|
|
9
|
+
return window.__nukeStores;
|
|
10
|
+
}
|
|
11
|
+
function createStore(name, initialState) {
|
|
12
|
+
const registry = getRegistry();
|
|
13
|
+
if (!registry.has(name)) {
|
|
14
|
+
registry.set(name, {
|
|
15
|
+
state: initialState,
|
|
16
|
+
listeners: /* @__PURE__ */ new Set()
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const entry = registry.get(name);
|
|
20
|
+
const subscribe = (listener) => {
|
|
21
|
+
entry.listeners.add(listener);
|
|
22
|
+
return () => {
|
|
23
|
+
entry.listeners.delete(listener);
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const getState = () => entry.state;
|
|
27
|
+
const setState = (updater) => {
|
|
28
|
+
entry.state = typeof updater === "function" ? updater(entry.state) : updater;
|
|
29
|
+
for (const l of Array.from(entry.listeners)) l();
|
|
30
|
+
};
|
|
31
|
+
return { name, initialState, getState, setState, subscribe };
|
|
32
|
+
}
|
|
33
|
+
function useStore(store, selector) {
|
|
34
|
+
const getSnapshot = selector ? () => selector(store.getState()) : () => store.getState();
|
|
35
|
+
const getServerSnapshot = selector ? () => selector(store.initialState) : () => store.initialState;
|
|
36
|
+
return useSyncExternalStore(
|
|
37
|
+
store.subscribe,
|
|
38
|
+
getSnapshot,
|
|
39
|
+
getServerSnapshot
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
createStore,
|
|
44
|
+
useStore
|
|
45
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nukejs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
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",
|