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.
@@ -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`
@@ -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
 
@@ -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
@@ -1,4 +1,4 @@
1
- export { createStore, useStore } from './store';
1
+ export { createStore, createPersistedStore, useStore } from './store';
2
2
  export type { Store } from './store';
3
3
  export { useHtml } from './use-html';
4
4
  export type { HtmlOptions } from './use-html';
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 userPath = path.join(process.cwd(), "middleware.ts");
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.21",
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",