nukejs 0.0.21 → 0.0.23
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/dist/build-cloudflare.js +31 -6
- package/dist/build-node.js +53 -0
- package/dist/build-vercel.js +23 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/middleware-loader.js +6 -1
- package/dist/store.d.ts +42 -0
- package/dist/store.js +22 -0
- package/package.json +1 -1
package/dist/build-cloudflare.js
CHANGED
|
@@ -133,7 +133,13 @@ class __NodeResponse__ {
|
|
|
133
133
|
}
|
|
134
134
|
`
|
|
135
135
|
);
|
|
136
|
-
function makeApiDispatcherSource(routes) {
|
|
136
|
+
function makeApiDispatcherSource(routes, middlewarePath) {
|
|
137
|
+
const middlewareImport = middlewarePath ? `import __userMiddleware__ from ${JSON.stringify(middlewarePath)};` : "";
|
|
138
|
+
const middlewareRun = middlewarePath ? `
|
|
139
|
+
// Run user middleware before routing. If it ends the response, bail out.
|
|
140
|
+
await __userMiddleware__(req, res);
|
|
141
|
+
if ((res as any).writableEnded || (res as any).headersSent) return true;
|
|
142
|
+
` : "";
|
|
137
143
|
const imports = routes.map((r, i) => `import * as __api_${i}__ from ${JSON.stringify(r.absPath)};`).join("\n");
|
|
138
144
|
const routeEntries = routes.map(
|
|
139
145
|
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, mod: __api_${i}__ },`
|
|
@@ -142,6 +148,7 @@ function makeApiDispatcherSource(routes) {
|
|
|
142
148
|
/* ts */
|
|
143
149
|
`import type { IncomingMessage, ServerResponse } from 'http';
|
|
144
150
|
${imports}
|
|
151
|
+
${middlewareImport}
|
|
145
152
|
|
|
146
153
|
const __CF_API_ROUTES__ = [
|
|
147
154
|
${routeEntries}
|
|
@@ -154,7 +161,7 @@ ${routeEntries}
|
|
|
154
161
|
export async function __dispatchApi__(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
155
162
|
const url = new URL((req as any).url || '/', 'http://localhost');
|
|
156
163
|
const pathname = url.pathname;
|
|
157
|
-
|
|
164
|
+
${middlewareRun}
|
|
158
165
|
for (const route of __CF_API_ROUTES__) {
|
|
159
166
|
const m = pathname.match(new RegExp(route.regex));
|
|
160
167
|
if (!m) continue;
|
|
@@ -194,7 +201,13 @@ export async function __dispatchApi__(req: IncomingMessage, res: ServerResponse)
|
|
|
194
201
|
`
|
|
195
202
|
);
|
|
196
203
|
}
|
|
197
|
-
function makePagesDispatcherSource(routes, errorAdapters2 = {}) {
|
|
204
|
+
function makePagesDispatcherSource(routes, errorAdapters2 = {}, middlewarePath) {
|
|
205
|
+
const middlewareImport = middlewarePath ? `import __userMiddleware__ from ${JSON.stringify(middlewarePath)};` : "";
|
|
206
|
+
const middlewareRun = middlewarePath ? `
|
|
207
|
+
// Run user middleware before routing. If it ends the response, bail out.
|
|
208
|
+
await __userMiddleware__(req, res);
|
|
209
|
+
if ((res as any).writableEnded || (res as any).headersSent) return true;
|
|
210
|
+
` : "";
|
|
198
211
|
const imports = routes.map((r, i) => `import __page_${i}__ from ${JSON.stringify(r.adapterPath)};`).join("\n");
|
|
199
212
|
const routeEntries = routes.map(
|
|
200
213
|
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, catchAll: ${JSON.stringify(r.catchAllNames)}, handler: __page_${i}__ },`
|
|
@@ -235,6 +248,7 @@ function makePagesDispatcherSource(routes, errorAdapters2 = {}) {
|
|
|
235
248
|
${imports}
|
|
236
249
|
${error404Import}
|
|
237
250
|
${error500Import}
|
|
251
|
+
${middlewareImport}
|
|
238
252
|
|
|
239
253
|
const __CF_PAGE_ROUTES__: Array<{
|
|
240
254
|
regex: string;
|
|
@@ -252,7 +266,7 @@ ${routeEntries}
|
|
|
252
266
|
export async function __dispatchPages__(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
253
267
|
const url = new URL((req as any).url || '/', 'http://localhost');
|
|
254
268
|
const pathname = url.pathname;
|
|
255
|
-
|
|
269
|
+
${middlewareRun}
|
|
256
270
|
// Client-side error \u2014 forward to _500 page if available.
|
|
257
271
|
if (url.searchParams.has('__clientError')) {
|
|
258
272
|
${clientErrHandler}
|
|
@@ -289,9 +303,14 @@ ${notFoundFallback}
|
|
|
289
303
|
`
|
|
290
304
|
);
|
|
291
305
|
}
|
|
292
|
-
function makeWorkerEntrySource(hasApi2, hasPages2, inlineStaticMap2) {
|
|
306
|
+
function makeWorkerEntrySource(hasApi2, hasPages2, inlineStaticMap2, middlewarePath) {
|
|
293
307
|
const apiImport = hasApi2 ? `import { __dispatchApi__ } from "./cf-api-dispatcher.js";` : "";
|
|
294
308
|
const pagesImport = hasPages2 ? `import { __dispatchPages__ } from "./cf-pages-dispatcher.js";` : "";
|
|
309
|
+
const middlewareImport = middlewarePath ? `import __userMiddleware__ from ${JSON.stringify(middlewarePath)};` : "";
|
|
310
|
+
const middlewareRun = middlewarePath ? `
|
|
311
|
+
await __userMiddleware__(nodeReq as any, nodeRes as any);
|
|
312
|
+
if ((nodeRes as any).writableEnded || (nodeRes as any).headersSent) return nodeRes.toResponse();
|
|
313
|
+
` : "";
|
|
295
314
|
const apiDispatch = hasApi2 ? `if (await __dispatchApi__(nodeReq as any, nodeRes as any)) return nodeRes.toResponse();` : "";
|
|
296
315
|
const pagesDispatch = hasPages2 ? `if (await __dispatchPages__(nodeReq as any, nodeRes as any)) return nodeRes.toResponse();` : "";
|
|
297
316
|
return (
|
|
@@ -300,6 +319,7 @@ function makeWorkerEntrySource(hasApi2, hasPages2, inlineStaticMap2) {
|
|
|
300
319
|
|
|
301
320
|
${apiImport}
|
|
302
321
|
${pagesImport}
|
|
322
|
+
${middlewareImport}
|
|
303
323
|
|
|
304
324
|
/**
|
|
305
325
|
* Pre-buffer the request body into a Uint8Array so we can both:
|
|
@@ -392,6 +412,8 @@ export default {
|
|
|
392
412
|
const nodeRes = new ((__NodeResponse__ as any))();
|
|
393
413
|
|
|
394
414
|
try {
|
|
415
|
+
// \u2500\u2500 User middleware \u2014 runs once, before any routing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
416
|
+
${middlewareRun}
|
|
395
417
|
// \u2500\u2500 4. API routes \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
|
|
396
418
|
${apiDispatch}
|
|
397
419
|
|
|
@@ -505,6 +527,9 @@ function walkStaticDir(dir, base = dir) {
|
|
|
505
527
|
}
|
|
506
528
|
return results;
|
|
507
529
|
}
|
|
530
|
+
const cfUserMiddlewareSrc = path.resolve("middleware.ts");
|
|
531
|
+
const cfMiddlewarePath = fs.existsSync(cfUserMiddlewareSrc) ? cfUserMiddlewareSrc : void 0;
|
|
532
|
+
if (cfMiddlewarePath) console.log(" found middleware.ts (will be bundled into worker)");
|
|
508
533
|
let hasApi = false;
|
|
509
534
|
let hasPages = false;
|
|
510
535
|
if (apiRoutes.length > 0) {
|
|
@@ -625,7 +650,7 @@ const inlineStaticMap = staticEntries.map(({ rel, abs }) => {
|
|
|
625
650
|
return ` [${JSON.stringify(rel)}, { ct: ${JSON.stringify(contentType)}, body: ${JSON.stringify(b64)}, text: false }],`;
|
|
626
651
|
}
|
|
627
652
|
}).join("\n");
|
|
628
|
-
const workerSrc = makeWorkerEntrySource(hasApi, hasPages, inlineStaticMap);
|
|
653
|
+
const workerSrc = makeWorkerEntrySource(hasApi, hasPages, inlineStaticMap, cfMiddlewarePath);
|
|
629
654
|
const workerSrcPath = path.join(
|
|
630
655
|
OUTPUT_DIR,
|
|
631
656
|
`_cf_worker_entry_${randomBytes(4).toString("hex")}.ts`
|
package/dist/build-node.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { build } from "esbuild";
|
|
3
4
|
import { loadConfig } from "./config.js";
|
|
4
5
|
import {
|
|
5
6
|
analyzeFile,
|
|
@@ -27,6 +28,21 @@ const manifest = [];
|
|
|
27
28
|
function funcPathToFilename(funcPath, prefix) {
|
|
28
29
|
return funcPath.replace(new RegExp(`^\\/${prefix}\\/`), "") + ".mjs";
|
|
29
30
|
}
|
|
31
|
+
const userMiddlewareSrc = path.resolve("middleware.ts");
|
|
32
|
+
const hasUserMiddleware = fs.existsSync(userMiddlewareSrc);
|
|
33
|
+
if (hasUserMiddleware) {
|
|
34
|
+
const result = await build({
|
|
35
|
+
entryPoints: [userMiddlewareSrc],
|
|
36
|
+
bundle: true,
|
|
37
|
+
format: "esm",
|
|
38
|
+
platform: "node",
|
|
39
|
+
target: "node20",
|
|
40
|
+
packages: "external",
|
|
41
|
+
write: false
|
|
42
|
+
});
|
|
43
|
+
fs.writeFileSync(path.join(OUT_DIR, "middleware.mjs"), result.outputFiles[0].text);
|
|
44
|
+
console.log(" built middleware.ts \u2192 dist/middleware.mjs");
|
|
45
|
+
}
|
|
30
46
|
const apiFiles = walkFiles(SERVER_DIR);
|
|
31
47
|
if (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);
|
|
32
48
|
const apiRoutes = apiFiles.map((relPath) => ({ ...analyzeFile(relPath, "api"), absPath: path.join(SERVER_DIR, relPath) })).sort((a, b) => b.specificity - a.specificity);
|
|
@@ -100,7 +116,44 @@ const compiled = routes.map(r => ({ ...r, regex: new RegExp(r.srcRegex) }));
|
|
|
100
116
|
const STATIC_DIR = path.join(__dirname, 'static');
|
|
101
117
|
const MIME_MAP = { ${MIME_MAP_ENTRIES} };
|
|
102
118
|
|
|
119
|
+
// \u2500\u2500 Middleware \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
|
|
120
|
+
// Load user middleware (compiled from middleware.ts at build time) once at
|
|
121
|
+
// startup. The variable stays null if no middleware was built.
|
|
122
|
+
let __middleware__ = null;
|
|
123
|
+
{
|
|
124
|
+
const mwPath = path.join(__dirname, 'middleware.mjs');
|
|
125
|
+
if (fs.existsSync(mwPath)) {
|
|
126
|
+
try {
|
|
127
|
+
const mod = await import(pathToFileURL(mwPath).href);
|
|
128
|
+
if (typeof mod.default === 'function') {
|
|
129
|
+
__middleware__ = mod.default;
|
|
130
|
+
console.log('nukejs: middleware loaded');
|
|
131
|
+
} else {
|
|
132
|
+
console.warn('nukejs: middleware.mjs does not export a default function, skipping');
|
|
133
|
+
}
|
|
134
|
+
} catch(e) { console.error('[middleware load error]', e); }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
103
138
|
const server = http.createServer(async (req, res) => {
|
|
139
|
+
// 0. User middleware \u2014 runs before every request (static files included).
|
|
140
|
+
// May mutate req.url (e.g. locale rewrites) \u2014 read it AFTER this runs.
|
|
141
|
+
// If it ends the response, skip all framework handling.
|
|
142
|
+
if (__middleware__) {
|
|
143
|
+
try {
|
|
144
|
+
await __middleware__(req, res);
|
|
145
|
+
if (res.writableEnded || res.headersSent) return;
|
|
146
|
+
} catch(e) {
|
|
147
|
+
console.error('[middleware error]', e);
|
|
148
|
+
if (!res.headersSent) {
|
|
149
|
+
res.statusCode = 500;
|
|
150
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
151
|
+
res.end('Internal Server Error');
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
104
157
|
const url = req.url || '/';
|
|
105
158
|
const clean = url.split('?')[0];
|
|
106
159
|
|
package/dist/build-vercel.js
CHANGED
|
@@ -72,13 +72,20 @@ function emitVercelFunction(name, bundleText) {
|
|
|
72
72
|
JSON.stringify({ runtime: "nodejs20.x", handler: "index.mjs", launcherType: "Nodejs" }, null, 2)
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
|
-
function makeApiDispatcherSource(routes) {
|
|
75
|
+
function makeApiDispatcherSource(routes, middlewarePath) {
|
|
76
|
+
const middlewareImport = middlewarePath ? `import __userMiddleware__ from ${JSON.stringify(middlewarePath)};` : "";
|
|
77
|
+
const middlewareRun = middlewarePath ? `
|
|
78
|
+
// Run user middleware before routing. If it ends the response, bail out.
|
|
79
|
+
await __userMiddleware__(req, res);
|
|
80
|
+
if ((res as any).writableEnded || (res as any).headersSent) return;
|
|
81
|
+
` : "";
|
|
76
82
|
const imports = routes.map((r, i) => `import * as __api_${i}__ from ${JSON.stringify(r.absPath)};`).join("\n");
|
|
77
83
|
const routeEntries = routes.map(
|
|
78
84
|
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, mod: __api_${i}__ },`
|
|
79
85
|
).join("\n");
|
|
80
86
|
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
81
87
|
${imports}
|
|
88
|
+
${middlewareImport}
|
|
82
89
|
|
|
83
90
|
function enhance(res: ServerResponse) {
|
|
84
91
|
(res as any).json = function(data: any, status = 200) {
|
|
@@ -112,6 +119,7 @@ ${routeEntries}
|
|
|
112
119
|
];
|
|
113
120
|
|
|
114
121
|
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
122
|
+
${middlewareRun}
|
|
115
123
|
const url = new URL(req.url || '/', 'http://localhost');
|
|
116
124
|
const pathname = url.pathname;
|
|
117
125
|
|
|
@@ -143,7 +151,13 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
143
151
|
}
|
|
144
152
|
`;
|
|
145
153
|
}
|
|
146
|
-
function makePagesDispatcherSource(routes, errorAdapters = {}) {
|
|
154
|
+
function makePagesDispatcherSource(routes, errorAdapters = {}, middlewarePath) {
|
|
155
|
+
const middlewareImport = middlewarePath ? `import __userMiddleware__ from ${JSON.stringify(middlewarePath)};` : "";
|
|
156
|
+
const middlewareRun = middlewarePath ? `
|
|
157
|
+
// Run user middleware before routing. If it ends the response, bail out.
|
|
158
|
+
await __userMiddleware__(req, res);
|
|
159
|
+
if ((res as any).writableEnded || (res as any).headersSent) return;
|
|
160
|
+
` : "";
|
|
147
161
|
const imports = routes.map((r, i) => `import __page_${i}__ from ${JSON.stringify(r.adapterPath)};`).join("\n");
|
|
148
162
|
const routeEntries = routes.map(
|
|
149
163
|
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, catchAll: ${JSON.stringify(r.catchAllNames)}, handler: __page_${i}__ },`
|
|
@@ -180,6 +194,7 @@ function makePagesDispatcherSource(routes, errorAdapters = {}) {
|
|
|
180
194
|
${imports}
|
|
181
195
|
${error404Import}
|
|
182
196
|
${error500Import}
|
|
197
|
+
${middlewareImport}
|
|
183
198
|
|
|
184
199
|
const ROUTES: Array<{
|
|
185
200
|
regex: string;
|
|
@@ -191,6 +206,7 @@ ${routeEntries}
|
|
|
191
206
|
];
|
|
192
207
|
|
|
193
208
|
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
209
|
+
${middlewareRun}
|
|
194
210
|
const url = new URL(req.url || '/', 'http://localhost');
|
|
195
211
|
const pathname = url.pathname;
|
|
196
212
|
|
|
@@ -227,13 +243,16 @@ ${notFoundHandler}
|
|
|
227
243
|
}
|
|
228
244
|
`;
|
|
229
245
|
}
|
|
246
|
+
const userMiddlewareSrc = path.resolve("middleware.ts");
|
|
247
|
+
const vercelMiddlewarePath = fs.existsSync(userMiddlewareSrc) ? userMiddlewareSrc : void 0;
|
|
248
|
+
if (vercelMiddlewarePath) console.log(" found middleware.ts (will be bundled into functions)");
|
|
230
249
|
const vercelRoutes = [];
|
|
231
250
|
const apiFiles = walkFiles(SERVER_DIR);
|
|
232
251
|
if (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);
|
|
233
252
|
const apiRoutes = apiFiles.map((relPath) => ({ ...analyzeFile(relPath, "api"), absPath: path.join(SERVER_DIR, relPath) })).sort((a, b) => b.specificity - a.specificity);
|
|
234
253
|
if (apiRoutes.length > 0) {
|
|
235
254
|
const dispatcherPath = path.join(SERVER_DIR, `_api_dispatcher_${randomBytes(4).toString("hex")}.ts`);
|
|
236
|
-
fs.writeFileSync(dispatcherPath, makeApiDispatcherSource(apiRoutes));
|
|
255
|
+
fs.writeFileSync(dispatcherPath, makeApiDispatcherSource(apiRoutes, vercelMiddlewarePath));
|
|
237
256
|
try {
|
|
238
257
|
const result = await build({
|
|
239
258
|
entryPoints: [dispatcherPath],
|
|
@@ -320,7 +339,7 @@ if (serverPages.length > 0 || hasErrorPages) {
|
|
|
320
339
|
errorAdapterPaths.push(adapterPath);
|
|
321
340
|
}
|
|
322
341
|
const dispatcherPath = path.join(PAGES_DIR, `_pages_dispatcher_${randomBytes(4).toString("hex")}.ts`);
|
|
323
|
-
fs.writeFileSync(dispatcherPath, makePagesDispatcherSource(dispatcherRoutes, errorAdapters));
|
|
342
|
+
fs.writeFileSync(dispatcherPath, makePagesDispatcherSource(dispatcherRoutes, errorAdapters, vercelMiddlewarePath));
|
|
324
343
|
try {
|
|
325
344
|
const result = await build({
|
|
326
345
|
entryPoints: [dispatcherPath],
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createStore, useStore } from "./store.js";
|
|
1
|
+
import { createStore, createPersistedStore, useStore } from "./store.js";
|
|
2
2
|
import { useHtml } from "./use-html.js";
|
|
3
3
|
import { default as default2 } from "./use-router.js";
|
|
4
4
|
import { useRequest } from "./use-request.js";
|
|
@@ -11,6 +11,7 @@ export {
|
|
|
11
11
|
default3 as Link,
|
|
12
12
|
ansi,
|
|
13
13
|
c,
|
|
14
|
+
createPersistedStore,
|
|
14
15
|
createStore,
|
|
15
16
|
escapeHtml,
|
|
16
17
|
getDebugLevel,
|
|
@@ -26,7 +26,12 @@ async function loadMiddleware() {
|
|
|
26
26
|
appDir,
|
|
27
27
|
`middleware.${appDir.endsWith("dist") ? "js" : "ts"}`
|
|
28
28
|
);
|
|
29
|
-
const
|
|
29
|
+
const userCandidates = [
|
|
30
|
+
path.join(process.cwd(), "middleware.ts"),
|
|
31
|
+
path.join(process.cwd(), "middleware.js"),
|
|
32
|
+
path.join(process.cwd(), "middleware.mjs")
|
|
33
|
+
];
|
|
34
|
+
const userPath = userCandidates.find((p) => fs.existsSync(p)) ?? userCandidates[0];
|
|
30
35
|
const paths = [.../* @__PURE__ */ new Set([builtinPath, userPath])];
|
|
31
36
|
for (const middlewarePath of paths) {
|
|
32
37
|
await loadMiddlewareFromPath(middlewarePath);
|
package/dist/store.d.ts
CHANGED
|
@@ -65,6 +65,7 @@ interface StoreEntry<T> {
|
|
|
65
65
|
declare global {
|
|
66
66
|
interface Window {
|
|
67
67
|
__nukeStores?: Map<string, StoreEntry<any>>;
|
|
68
|
+
__nukePersisted?: Set<string>;
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
@@ -80,6 +81,47 @@ declare global {
|
|
|
80
81
|
* @param initialState Default state used when the store is first created.
|
|
81
82
|
*/
|
|
82
83
|
export declare function createStore<T extends object>(name: string, initialState: T): Store<T>;
|
|
84
|
+
/**
|
|
85
|
+
* Creates a `Store` that survives full page refreshes by mirroring its state
|
|
86
|
+
* into `localStorage` (or `sessionStorage`).
|
|
87
|
+
*
|
|
88
|
+
* A plain `createStore` only lives in `window.__nukeStores`, which is wiped
|
|
89
|
+
* on every hard reload — fine for SPA navigations, not for data you want to
|
|
90
|
+
* keep around. `createPersistedStore` wraps `createStore` and:
|
|
91
|
+
*
|
|
92
|
+
* 1. On first creation in the browser, reads any previously saved value
|
|
93
|
+
* from storage and applies it via `setState`.
|
|
94
|
+
* 2. Subscribes to the store and writes the new state to storage on every
|
|
95
|
+
* change.
|
|
96
|
+
*
|
|
97
|
+
* `store.initialState` (used by `useStore` as the SSR snapshot) is left
|
|
98
|
+
* untouched as the value you passed in — the persisted value is applied
|
|
99
|
+
* *after* creation via `setState`, not by changing `initialState`. This
|
|
100
|
+
* keeps the server-rendered HTML and the client's first hydration pass in
|
|
101
|
+
* sync (no hydration mismatch); components simply re-render with the
|
|
102
|
+
* persisted value immediately after mount, the same way `useSyncExternalStore`
|
|
103
|
+
* already reconciles store mutations.
|
|
104
|
+
*
|
|
105
|
+
* Because `createStore` itself is idempotent per `name` but storage I/O is
|
|
106
|
+
* not, a `window.__nukePersisted` set guards against re-running the
|
|
107
|
+
* read/subscribe wiring if multiple bundles import the same persisted store.
|
|
108
|
+
*
|
|
109
|
+
* @param name Unique store key — also used to derive the storage key.
|
|
110
|
+
* @param initialState Default state used when nothing is in storage yet.
|
|
111
|
+
* @param options.storage `'local'` (default) or `'session'`.
|
|
112
|
+
* @param options.key Override the storage key (defaults to `nuke-store:${name}`).
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* export const cartStore = createPersistedStore('cart', { items: [], total: 0 })
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* // Cleared when the tab closes, kept across refreshes within the session
|
|
119
|
+
* export const draftStore = createPersistedStore('draft', { text: '' }, { storage: 'session' })
|
|
120
|
+
*/
|
|
121
|
+
export declare function createPersistedStore<T extends object>(name: string, initialState: T, options?: {
|
|
122
|
+
storage?: 'local' | 'session';
|
|
123
|
+
key?: string;
|
|
124
|
+
}): Store<T>;
|
|
83
125
|
/**
|
|
84
126
|
* React hook that subscribes a component to a store.
|
|
85
127
|
*
|
package/dist/store.js
CHANGED
|
@@ -30,6 +30,27 @@ function createStore(name, initialState) {
|
|
|
30
30
|
};
|
|
31
31
|
return { name, initialState, getState, setState, subscribe };
|
|
32
32
|
}
|
|
33
|
+
function createPersistedStore(name, initialState, options) {
|
|
34
|
+
const store = createStore(name, initialState);
|
|
35
|
+
if (typeof window === "undefined") return store;
|
|
36
|
+
const storageKey = options?.key ?? `nuke-store:${name}`;
|
|
37
|
+
const backend = options?.storage === "session" ? window.sessionStorage : window.localStorage;
|
|
38
|
+
if (!window.__nukePersisted) window.__nukePersisted = /* @__PURE__ */ new Set();
|
|
39
|
+
if (window.__nukePersisted.has(storageKey)) return store;
|
|
40
|
+
window.__nukePersisted.add(storageKey);
|
|
41
|
+
try {
|
|
42
|
+
const raw = backend.getItem(storageKey);
|
|
43
|
+
if (raw !== null) store.setState(JSON.parse(raw));
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
store.subscribe(() => {
|
|
47
|
+
try {
|
|
48
|
+
backend.setItem(storageKey, JSON.stringify(store.getState()));
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return store;
|
|
53
|
+
}
|
|
33
54
|
function useStore(store, selector) {
|
|
34
55
|
const getSnapshot = selector ? () => selector(store.getState()) : () => store.getState();
|
|
35
56
|
const getServerSnapshot = selector ? () => selector(store.initialState) : () => store.initialState;
|
|
@@ -40,6 +61,7 @@ function useStore(store, selector) {
|
|
|
40
61
|
);
|
|
41
62
|
}
|
|
42
63
|
export {
|
|
64
|
+
createPersistedStore,
|
|
43
65
|
createStore,
|
|
44
66
|
useStore
|
|
45
67
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nukejs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
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",
|