nukejs 0.0.18 → 0.0.20

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.
@@ -225,7 +225,9 @@ function makeApiAdapterSource(handlerFilename) {
225
225
  return `import type { IncomingMessage, ServerResponse } from 'http';
226
226
  import * as mod from ${JSON.stringify("./" + handlerFilename)};
227
227
 
228
- function enhance(res: ServerResponse) {
228
+ const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
229
+
230
+ function enhanceRes(res: ServerResponse) {
229
231
  (res as any).json = function (data: any, status = 200) {
230
232
  this.statusCode = status;
231
233
  this.setHeader('Content-Type', 'application/json');
@@ -235,26 +237,46 @@ function enhance(res: ServerResponse) {
235
237
  return res;
236
238
  }
237
239
 
238
- async function parseBody(req: IncomingMessage): Promise<any> {
239
- return new Promise((resolve, reject) => {
240
- let body = '';
241
- req.on('data', chunk => { body += chunk.toString(); });
242
- req.on('end', () => {
243
- try {
244
- resolve(body && req.headers['content-type']?.includes('application/json')
245
- ? JSON.parse(body) : body);
246
- } catch (e) { reject(e); }
247
- });
248
- req.on('error', reject);
240
+ function enhanceReq(req: IncomingMessage) {
241
+ const apiReq = req as any;
242
+ let bufferPromise: Promise<Buffer> | null = null;
243
+
244
+ const getBuffer = (): Promise<Buffer> => {
245
+ if (!bufferPromise) {
246
+ bufferPromise = new Promise((resolve, reject) => {
247
+ const chunks: Buffer[] = [];
248
+ let bytes = 0;
249
+ req.on('data', (chunk: Buffer) => {
250
+ bytes += chunk.length;
251
+ if (bytes > MAX_BODY_BYTES) { req.destroy(); return reject(new Error('Request body too large')); }
252
+ chunks.push(chunk);
253
+ });
254
+ req.on('end', () => resolve(Buffer.concat(chunks)));
255
+ req.on('error', reject);
256
+ });
257
+ }
258
+ return bufferPromise;
259
+ };
260
+
261
+ apiReq.buffer = () => getBuffer();
262
+ apiReq.text = () => getBuffer().then(buf => buf.toString('utf8'));
263
+ apiReq.json = () => getBuffer().then(buf => {
264
+ const parsed = JSON.parse(buf.toString('utf8'));
265
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
266
+ delete parsed.__proto__;
267
+ delete parsed.constructor;
268
+ }
269
+ return parsed;
249
270
  });
271
+
272
+ return apiReq;
250
273
  }
251
274
 
252
275
  export default async function handler(req: IncomingMessage, res: ServerResponse) {
253
276
  const method = (req.method || 'GET').toUpperCase();
254
- const apiRes = enhance(res);
255
- const apiReq = req as any;
277
+ const apiRes = enhanceRes(res);
278
+ const apiReq = enhanceReq(req);
256
279
 
257
- apiReq.body = await parseBody(req);
258
280
  // In production, route dynamic segments are injected as query-string keys by
259
281
  // the server entry, so params and query share the same parsed URL values.
260
282
  const qs = Object.fromEntries(new URL(req.url || '/', 'http://localhost').searchParams);
@@ -721,23 +743,36 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
721
743
  if (globalRegistry.size === 0) return /* @__PURE__ */ new Map();
722
744
  const outDir = path.join(staticDir, "__client-component");
723
745
  fs.mkdirSync(outDir, { recursive: true });
724
- const prerendered = /* @__PURE__ */ new Map();
746
+ const entryPoints = {};
725
747
  for (const [id, filePath] of globalRegistry) {
748
+ entryPoints[id] = filePath;
726
749
  console.log(` bundling client ${id} (${path.relative(pagesDir, filePath)})`);
727
- const browserResult = await build({
728
- entryPoints: [filePath],
729
- bundle: true,
730
- format: "esm",
731
- platform: "browser",
732
- jsx: "automatic",
733
- minify: true,
734
- conditions: ["module", "browser", "import"],
735
- banner: { js: `const require=(m)=>{if(m==='react')return window.__nukejs_react__;if(m==='react/jsx-runtime')return window.__nukejs_jsx__;throw new Error('Dynamic require of "'+m+'" is not supported');};` },
736
- external: ["react", "react-dom/client", "react/jsx-runtime"],
737
- define: { "process.env.NODE_ENV": '"production"' },
738
- write: false
739
- });
740
- fs.writeFileSync(path.join(outDir, `${id}.js`), browserResult.outputFiles[0].text);
750
+ }
751
+ await build({
752
+ entryPoints,
753
+ bundle: true,
754
+ splitting: true,
755
+ // ← shared deps extracted into chunks
756
+ format: "esm",
757
+ // splitting requires ESM
758
+ platform: "browser",
759
+ jsx: "automatic",
760
+ minify: true,
761
+ write: true,
762
+ // splitting requires write:true + outdir
763
+ outdir: outDir,
764
+ conditions: ["module", "browser", "import"],
765
+ banner: { js: `const require=(m)=>{if(m==='react')return window.__nukejs_react__;if(m==='react/jsx-runtime')return window.__nukejs_jsx__;throw new Error('Dynamic require of "'+m+'" is not supported');};` },
766
+ external: ["react", "react-dom/client", "react/jsx-runtime"],
767
+ define: { "process.env.NODE_ENV": '"production"' },
768
+ entryNames: "[name]",
769
+ // cc_abc123.js (no hash on entries)
770
+ chunkNames: "__chunks/[hash]"
771
+ // __chunks/ABCDEF.js
772
+ });
773
+ console.log(` bundled ${globalRegistry.size} client component(s) \u2192 ${path.relative(process.cwd(), outDir)}/`);
774
+ const prerendered = /* @__PURE__ */ new Map();
775
+ for (const [id, filePath] of globalRegistry) {
741
776
  const ssrTmp = path.join(
742
777
  path.dirname(filePath),
743
778
  `_ssr_${id}_${randomBytes(4).toString("hex")}.mjs`
@@ -766,7 +801,6 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
766
801
  if (fs.existsSync(ssrTmp)) fs.unlinkSync(ssrTmp);
767
802
  }
768
803
  }
769
- console.log(` bundled ${globalRegistry.size} client component(s) \u2192 ${path.relative(process.cwd(), outDir)}/`);
770
804
  return prerendered;
771
805
  }
772
806
  async function buildErrorPages(pagesDir, outPagesDir, prerenderedHtml) {
package/dist/bundler.js CHANGED
@@ -1,40 +1,89 @@
1
1
  import path from "path";
2
+ import fs from "fs";
3
+ import os from "os";
2
4
  import { fileURLToPath } from "url";
3
5
  import { build } from "esbuild";
4
6
  import { log } from "./logger.js";
5
- import { getComponentById } from "./component-analyzer.js";
7
+ import { getComponentCache } from "./component-analyzer.js";
6
8
  let reactBundlePromise = null;
7
9
  let nukeBundlePromise = null;
8
- async function bundleClientComponent(filePath) {
9
- const result = await build({
10
- entryPoints: [filePath],
10
+ const SPLIT_OUT_DIR = path.join(os.tmpdir(), "nukejs-dev-components");
11
+ let splitBuildValid = false;
12
+ let splitBuildPromise = null;
13
+ async function buildAllComponentsSplit() {
14
+ const cache = getComponentCache();
15
+ const clientComponents = /* @__PURE__ */ new Map();
16
+ for (const [filePath, info] of cache) {
17
+ if (info.isClientComponent && info.clientComponentId) {
18
+ clientComponents.set(info.clientComponentId, filePath);
19
+ }
20
+ }
21
+ if (clientComponents.size === 0) {
22
+ splitBuildValid = true;
23
+ return;
24
+ }
25
+ const entryPoints = {};
26
+ for (const [id, filePath] of clientComponents) {
27
+ entryPoints[id] = filePath;
28
+ }
29
+ fs.mkdirSync(SPLIT_OUT_DIR, { recursive: true });
30
+ log.verbose(`[bundler] Split build: ${clientComponents.size} component(s) \u2192 ${SPLIT_OUT_DIR}`);
31
+ await build({
32
+ entryPoints,
11
33
  bundle: true,
34
+ splitting: true,
35
+ // ← shared deps extracted into chunks
12
36
  format: "esm",
37
+ // splitting requires ESM
13
38
  platform: "browser",
14
- write: false,
15
39
  jsx: "automatic",
16
- // Prefer ESM exports from dual-mode packages (e.g. radix-ui) to avoid
17
- // CJS require() calls that break in an ESM context at runtime.
40
+ minify: false,
41
+ // keep readable in dev
42
+ write: true,
43
+ // splitting requires write:true + outdir
44
+ outdir: SPLIT_OUT_DIR,
18
45
  conditions: ["module", "browser", "import"],
19
- // Shim require() for CJS-only packages that call require('react') at
20
- // runtime. Resolves to the already-loaded React instance on window.
46
+ // Shim require() for CJS packages that call require('react') at runtime.
21
47
  banner: { js: `const require=(m)=>{if(m==='react')return window.__nukejs_react__;if(m==='react/jsx-runtime')return window.__nukejs_jsx__;throw new Error('Dynamic require of "'+m+'" is not supported');};` },
22
- // Keep React external resolved by the importmap to /__react.js
23
- external: ["react", "react-dom/client", "react/jsx-runtime"]
48
+ external: ["react", "react-dom/client", "react/jsx-runtime"],
49
+ define: { "process.env.NODE_ENV": '"development"' },
50
+ entryNames: "[name]",
51
+ // cc_abc123.js (stable, no hash)
52
+ chunkNames: "__chunks/[hash]"
53
+ // __chunks/ABCDEF.js
24
54
  });
25
- return result.outputFiles[0].text;
55
+ splitBuildValid = true;
56
+ log.verbose("[bundler] Split build complete");
57
+ }
58
+ async function ensureSplitBuild() {
59
+ if (splitBuildValid) return;
60
+ if (!splitBuildPromise) {
61
+ splitBuildPromise = buildAllComponentsSplit().finally(() => {
62
+ splitBuildPromise = null;
63
+ });
64
+ }
65
+ await splitBuildPromise;
66
+ }
67
+ function invalidateSplitBundle() {
68
+ splitBuildValid = false;
69
+ log.verbose("[bundler] Split bundle invalidated");
26
70
  }
27
71
  async function serveClientComponentBundle(componentId, res) {
28
- const filePath = getComponentById(componentId);
29
- if (filePath) {
30
- log.verbose(`Bundling client component: ${componentId} (${path.basename(filePath)})`);
31
- res.setHeader("Content-Type", "application/javascript");
32
- res.end(await bundleClientComponent(filePath));
33
- return;
72
+ await ensureSplitBuild();
73
+ const outPath = path.join(SPLIT_OUT_DIR, `${componentId}.js`);
74
+ if (!fs.existsSync(outPath)) {
75
+ log.verbose(`[bundler] ${componentId} not in split build \u2014 rebuilding`);
76
+ invalidateSplitBundle();
77
+ await ensureSplitBuild();
78
+ if (!fs.existsSync(outPath)) {
79
+ log.error(`Client component not found: ${componentId}`);
80
+ res.statusCode = 404;
81
+ res.end("Client component not found");
82
+ return;
83
+ }
34
84
  }
35
- log.error(`Client component not found: ${componentId}`);
36
- res.statusCode = 404;
37
- res.end("Client component not found");
85
+ res.setHeader("Content-Type", "application/javascript");
86
+ res.end(fs.readFileSync(outPath));
38
87
  }
39
88
  async function serveReactBundle(res) {
40
89
  log.verbose("Bundling React runtime");
@@ -105,7 +154,7 @@ async function serveNukeBundle(res) {
105
154
  res.end(await nukeBundlePromise);
106
155
  }
107
156
  export {
108
- bundleClientComponent,
157
+ invalidateSplitBundle,
109
158
  serveClientComponentBundle,
110
159
  serveNukeBundle,
111
160
  serveReactBundle
package/dist/hmr.js CHANGED
@@ -2,6 +2,7 @@ import { existsSync, watch } from "fs";
2
2
  import path from "path";
3
3
  import { log } from "./logger.js";
4
4
  import { invalidateComponentCache } from "./component-analyzer.js";
5
+ import { invalidateSplitBundle } from "./bundler.js";
5
6
  const hmrClients = /* @__PURE__ */ new Set();
6
7
  function broadcastHmr(payload) {
7
8
  const data = `data: ${JSON.stringify(payload)}
@@ -51,6 +52,7 @@ function watchDir(dir, label) {
51
52
  const payload = buildPayload(filename);
52
53
  log.info(`[HMR] ${label} changed: ${filename}`, JSON.stringify(payload));
53
54
  if (dir) invalidateComponentCache(path.resolve(dir, filename));
55
+ invalidateSplitBundle();
54
56
  broadcastHmr(payload);
55
57
  pending.delete(filename);
56
58
  }, 100);
@@ -31,9 +31,9 @@ function discoverApiPrefixes(serverDir) {
31
31
  return prefixes;
32
32
  }
33
33
  const MAX_BODY_BYTES = 10 * 1024 * 1024;
34
- async function parseBody(req) {
34
+ function collectBuffer(req) {
35
35
  return new Promise((resolve, reject) => {
36
- let body = "";
36
+ const chunks = [];
37
37
  let bytes = 0;
38
38
  req.on("data", (chunk) => {
39
39
  bytes += chunk.length;
@@ -41,27 +41,31 @@ async function parseBody(req) {
41
41
  req.destroy();
42
42
  return reject(new Error("Request body too large"));
43
43
  }
44
- body += chunk.toString();
45
- });
46
- req.on("end", () => {
47
- try {
48
- if (body && req.headers["content-type"]?.includes("application/json")) {
49
- const parsed = JSON.parse(body);
50
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
51
- delete parsed.__proto__;
52
- delete parsed.constructor;
53
- }
54
- resolve(parsed);
55
- } else {
56
- resolve(body);
57
- }
58
- } catch (err) {
59
- reject(err);
60
- }
44
+ chunks.push(chunk);
61
45
  });
46
+ req.on("end", () => resolve(Buffer.concat(chunks)));
62
47
  req.on("error", reject);
63
48
  });
64
49
  }
50
+ function enhanceRequest(req) {
51
+ const apiReq = req;
52
+ let bufferPromise = null;
53
+ const getBuffer = () => {
54
+ if (!bufferPromise) bufferPromise = collectBuffer(req);
55
+ return bufferPromise;
56
+ };
57
+ apiReq.buffer = () => getBuffer();
58
+ apiReq.text = () => getBuffer().then((buf) => buf.toString("utf8"));
59
+ apiReq.json = () => getBuffer().then((buf) => {
60
+ const parsed = JSON.parse(buf.toString("utf8"));
61
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
62
+ delete parsed.__proto__;
63
+ delete parsed.constructor;
64
+ }
65
+ return parsed;
66
+ });
67
+ return apiReq;
68
+ }
65
69
  function parseQuery(url, port) {
66
70
  const query = {};
67
71
  new URL(url, `http://localhost:${port}`).searchParams.forEach((v, k) => {
@@ -145,8 +149,7 @@ function createApiHandler({ apiPrefixes, port, isDev }) {
145
149
  respondOptions(apiRes);
146
150
  return;
147
151
  }
148
- const apiReq = req;
149
- apiReq.body = await parseBody(req);
152
+ const apiReq = enhanceRequest(req);
150
153
  apiReq.params = params;
151
154
  apiReq.query = parseQuery(url, port);
152
155
  const apiModule = isDev ? await importFreshInDev(filePath) : await import(pathToFileURL(filePath).href);
@@ -165,8 +168,8 @@ function createApiHandler({ apiPrefixes, port, isDev }) {
165
168
  export {
166
169
  createApiHandler,
167
170
  discoverApiPrefixes,
171
+ enhanceRequest,
168
172
  enhanceResponse,
169
173
  matchApiPrefix,
170
- parseBody,
171
174
  parseQuery
172
175
  };
@@ -2,6 +2,8 @@ type Router = {
2
2
  path: string;
3
3
  push: (url: string) => void;
4
4
  replace: (url: string) => void;
5
+ back: () => void;
6
+ refresh: () => void;
5
7
  };
6
8
  export default function useRouter(): Router;
7
9
  export {};
@@ -1,26 +1,34 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
+ const SSR_ROUTER = { push: () => {
3
+ }, replace: () => {
4
+ }, back: () => {
5
+ }, refresh: () => {
6
+ }, path: "" };
2
7
  function useRouter() {
3
- try {
4
- const [path, setPath] = useState(() => window.location.pathname);
5
- useEffect(() => {
6
- const handleLocationChange = () => setPath(window.location.pathname);
7
- window.addEventListener("locationchange", handleLocationChange);
8
- return () => window.removeEventListener("locationchange", handleLocationChange);
9
- }, []);
10
- const push = useCallback((url) => {
11
- window.history.pushState({}, "", url);
12
- setPath(url);
13
- }, []);
14
- const replace = useCallback((url) => {
15
- window.history.replaceState({}, "", url);
16
- setPath(url);
17
- }, []);
18
- return { path, push, replace };
19
- } catch {
20
- return { push: () => {
21
- }, replace: () => {
22
- }, path: "" };
8
+ if (typeof window === "undefined") {
9
+ return SSR_ROUTER;
23
10
  }
11
+ const [path, setPath] = useState(() => window.location.pathname);
12
+ useEffect(() => {
13
+ const handleLocationChange = () => setPath(window.location.pathname);
14
+ window.addEventListener("locationchange", handleLocationChange);
15
+ return () => window.removeEventListener("locationchange", handleLocationChange);
16
+ }, []);
17
+ const push = useCallback((url) => {
18
+ window.history.pushState({}, "", url);
19
+ setPath(url);
20
+ }, []);
21
+ const replace = useCallback((url) => {
22
+ window.history.replaceState({}, "", url);
23
+ setPath(url);
24
+ }, []);
25
+ const back = useCallback(() => {
26
+ window.history.back();
27
+ }, []);
28
+ const refresh = useCallback(() => {
29
+ window.dispatchEvent(new Event("locationchange"));
30
+ }, []);
31
+ return { path, push, replace, back, refresh };
24
32
  }
25
33
  export {
26
34
  useRouter as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nukejs",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
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",