nukejs 0.0.18 → 0.0.19

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.
@@ -721,23 +721,36 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
721
721
  if (globalRegistry.size === 0) return /* @__PURE__ */ new Map();
722
722
  const outDir = path.join(staticDir, "__client-component");
723
723
  fs.mkdirSync(outDir, { recursive: true });
724
- const prerendered = /* @__PURE__ */ new Map();
724
+ const entryPoints = {};
725
725
  for (const [id, filePath] of globalRegistry) {
726
+ entryPoints[id] = filePath;
726
727
  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);
728
+ }
729
+ await build({
730
+ entryPoints,
731
+ bundle: true,
732
+ splitting: true,
733
+ // ← shared deps extracted into chunks
734
+ format: "esm",
735
+ // splitting requires ESM
736
+ platform: "browser",
737
+ jsx: "automatic",
738
+ minify: true,
739
+ write: true,
740
+ // splitting requires write:true + outdir
741
+ outdir: outDir,
742
+ conditions: ["module", "browser", "import"],
743
+ 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');};` },
744
+ external: ["react", "react-dom/client", "react/jsx-runtime"],
745
+ define: { "process.env.NODE_ENV": '"production"' },
746
+ entryNames: "[name]",
747
+ // cc_abc123.js (no hash on entries)
748
+ chunkNames: "__chunks/[hash]"
749
+ // __chunks/ABCDEF.js
750
+ });
751
+ console.log(` bundled ${globalRegistry.size} client component(s) \u2192 ${path.relative(process.cwd(), outDir)}/`);
752
+ const prerendered = /* @__PURE__ */ new Map();
753
+ for (const [id, filePath] of globalRegistry) {
741
754
  const ssrTmp = path.join(
742
755
  path.dirname(filePath),
743
756
  `_ssr_${id}_${randomBytes(4).toString("hex")}.mjs`
@@ -766,7 +779,6 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
766
779
  if (fs.existsSync(ssrTmp)) fs.unlinkSync(ssrTmp);
767
780
  }
768
781
  }
769
- console.log(` bundled ${globalRegistry.size} client component(s) \u2192 ${path.relative(process.cwd(), outDir)}/`);
770
782
  return prerendered;
771
783
  }
772
784
  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);
@@ -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.19",
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",