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.
- package/dist/build-common.js +65 -31
- package/dist/bundler.js +71 -22
- package/dist/hmr.js +2 -0
- package/dist/http-server.js +25 -22
- package/dist/use-router.d.ts +2 -0
- package/dist/use-router.js +28 -20
- package/package.json +1 -1
package/dist/build-common.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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 =
|
|
255
|
-
const apiReq = req
|
|
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
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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 {
|
|
7
|
+
import { getComponentCache } from "./component-analyzer.js";
|
|
6
8
|
let reactBundlePromise = null;
|
|
7
9
|
let nukeBundlePromise = null;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
17
|
-
//
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
res.
|
|
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
|
-
|
|
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);
|
package/dist/http-server.js
CHANGED
|
@@ -31,9 +31,9 @@ function discoverApiPrefixes(serverDir) {
|
|
|
31
31
|
return prefixes;
|
|
32
32
|
}
|
|
33
33
|
const MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
34
|
-
|
|
34
|
+
function collectBuffer(req) {
|
|
35
35
|
return new Promise((resolve, reject) => {
|
|
36
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/use-router.d.ts
CHANGED
package/dist/use-router.js
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
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.
|
|
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",
|