nukejs 0.0.2 → 0.0.3
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/bundler.js +35 -33
- package/dist/bundler.js.map +2 -2
- package/dist/middleware.js +26 -13
- package/dist/middleware.js.map +2 -2
- package/package.json +1 -2
package/dist/bundler.js
CHANGED
|
@@ -3,6 +3,8 @@ import { fileURLToPath } from "url";
|
|
|
3
3
|
import { build } from "esbuild";
|
|
4
4
|
import { log } from "./logger.js";
|
|
5
5
|
import { getComponentById } from "./component-analyzer.js";
|
|
6
|
+
let reactBundlePromise = null;
|
|
7
|
+
let nukeBundlePromise = null;
|
|
6
8
|
async function bundleClientComponent(filePath) {
|
|
7
9
|
const result = await build({
|
|
8
10
|
entryPoints: [filePath],
|
|
@@ -30,11 +32,10 @@ async function serveClientComponentBundle(componentId, res) {
|
|
|
30
32
|
}
|
|
31
33
|
async function serveReactBundle(res) {
|
|
32
34
|
log.verbose("Bundling React runtime");
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
contents: `
|
|
35
|
+
if (!reactBundlePromise) {
|
|
36
|
+
reactBundlePromise = build({
|
|
37
|
+
stdin: {
|
|
38
|
+
contents: `
|
|
38
39
|
import React, {
|
|
39
40
|
useState, useEffect, useContext, useReducer, useCallback, useMemo,
|
|
40
41
|
useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
|
|
@@ -55,39 +56,40 @@ async function serveReactBundle(res) {
|
|
|
55
56
|
};
|
|
56
57
|
export default React;
|
|
57
58
|
`,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
});
|
|
59
|
+
loader: "ts"
|
|
60
|
+
},
|
|
61
|
+
bundle: true,
|
|
62
|
+
write: false,
|
|
63
|
+
treeShaking: true,
|
|
64
|
+
minify: false,
|
|
65
|
+
format: "esm",
|
|
66
|
+
jsx: "automatic",
|
|
67
|
+
alias: {
|
|
68
|
+
react: path.dirname(fileURLToPath(import.meta.resolve("react/package.json"))),
|
|
69
|
+
"react-dom": path.dirname(fileURLToPath(import.meta.resolve("react-dom/package.json")))
|
|
70
|
+
},
|
|
71
|
+
define: { "process.env.NODE_ENV": '"development"' }
|
|
72
|
+
}).then((r) => r.outputFiles[0].text);
|
|
73
|
+
}
|
|
74
74
|
res.setHeader("Content-Type", "application/javascript");
|
|
75
|
-
res.end(
|
|
75
|
+
res.end(await reactBundlePromise);
|
|
76
76
|
}
|
|
77
77
|
async function serveNukeBundle(res) {
|
|
78
78
|
log.verbose("Bundling nuke runtime");
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
if (!nukeBundlePromise) {
|
|
80
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
81
|
+
const entry = path.join(dir, `bundle.${dir.endsWith("dist") ? "js" : "ts"}`);
|
|
82
|
+
nukeBundlePromise = build({
|
|
83
|
+
entryPoints: [entry],
|
|
84
|
+
write: false,
|
|
85
|
+
format: "esm",
|
|
86
|
+
minify: true,
|
|
87
|
+
bundle: true,
|
|
88
|
+
external: ["react", "react-dom/client"]
|
|
89
|
+
}).then((r) => r.outputFiles[0].text);
|
|
90
|
+
}
|
|
89
91
|
res.setHeader("Content-Type", "application/javascript");
|
|
90
|
-
res.end(
|
|
92
|
+
res.end(await nukeBundlePromise);
|
|
91
93
|
}
|
|
92
94
|
export {
|
|
93
95
|
bundleClientComponent,
|
package/dist/bundler.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/bundler.ts"],
|
|
4
|
-
"sourcesContent": ["/**\
|
|
5
|
-
"mappings": "AAoBA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,aAAa;AAEtB,SAAS,WAAW;AACpB,SAAS,wBAAwB;
|
|
4
|
+
"sourcesContent": ["/**\n * bundler.ts \u2014 Dev-Mode On-Demand Bundler\n *\n * Handles the three internal JS routes served only in `nuke dev`:\n *\n * /__react.js \u2014 Full React + ReactDOM browser bundle.\n * All hooks, jsx-runtime, hydrateRoot, createRoot.\n * Served once and cached by the browser.\n *\n * /__n.js \u2014 NukeJS client runtime (bundle.ts compiled to ESM).\n * Provides initRuntime and SPA navigation.\n *\n * /__client-component/<id> \u2014 Individual \"use client\" component bundles.\n * Built on-demand the first time they're requested.\n * Re-built on every request in dev (no disk cache).\n *\n * In production (`nuke build`), equivalent bundles are written to dist/static/\n * by build-common.ts instead of being served dynamically.\n */\n\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { build } from 'esbuild';\nimport type { ServerResponse } from 'http';\nimport { log } from './logger';\nimport { getComponentById } from './component-analyzer';\n\n// \u2500\u2500\u2500 Bundle caches \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\n\n// Cache Promises (not just results) so concurrent requests share one build\n// instead of each spawning their own esbuild process.\nlet reactBundlePromise: Promise<string> | null = null;\nlet nukeBundlePromise: Promise<string> | null = null;\n\n// \u2500\u2500\u2500 Client component bundle \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\n\n/**\n * Bundles a single \"use client\" file for the browser.\n *\n * React and react-dom/client are kept external so the importmap can resolve\n * them to the already-loaded /__react.js bundle (avoids shipping React twice).\n *\n * @param filePath Absolute path to the source file.\n * @returns ESM string ready to serve as application/javascript.\n */\nexport async function bundleClientComponent(filePath: string): Promise<string> {\n const result = await build({\n entryPoints: [filePath],\n bundle: true,\n format: 'esm',\n platform: 'browser',\n write: false,\n jsx: 'automatic',\n // Keep React external \u2014 resolved by the importmap to /__react.js\n external: ['react', 'react-dom/client', 'react/jsx-runtime'],\n });\n return result.outputFiles[0].text;\n}\n\n/**\n * Looks up a client component by its content-hash ID (e.g. `cc_a1b2c3d4`),\n * bundles it on-demand, and writes the result to the HTTP response.\n *\n * The ID\u2192path mapping comes from the component analyzer cache, which is\n * populated during SSR as pages and their layouts are rendered.\n */\nexport async function serveClientComponentBundle(\n componentId: string,\n res: ServerResponse,\n): Promise<void> {\n const filePath = getComponentById(componentId);\n if (filePath) {\n log.verbose(`Bundling client component: ${componentId} (${path.basename(filePath)})`);\n res.setHeader('Content-Type', 'application/javascript');\n res.end(await bundleClientComponent(filePath));\n return;\n }\n\n // ID not found \u2014 either the page hasn't been visited yet (cache is empty)\n // or the ID is stale.\n log.error(`Client component not found: ${componentId}`);\n res.statusCode = 404;\n res.end('Client component not found');\n}\n\n// \u2500\u2500\u2500 React bundle \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\n\n/**\n * Builds and serves the unified React browser bundle to /__react.js.\n *\n * Exports every public React API so client components can import from 'react'\n * or 'react-dom/client' and have them resolve via the importmap to this single\n * pre-loaded bundle \u2014 no duplicate copies of React in the browser.\n *\n * esbuild aliases point at the project's installed React version so the bundle\n * always matches what the server is actually running.\n */\nexport async function serveReactBundle(res: ServerResponse): Promise<void> {\n log.verbose('Bundling React runtime');\n\n if (!reactBundlePromise) {\n reactBundlePromise = build({\n stdin: {\n contents: `\n import React, {\n useState, useEffect, useContext, useReducer, useCallback, useMemo,\n useRef, useImperativeHandle, useLayoutEffect, useDebugValue,\n useDeferredValue, useTransition, useId, useSyncExternalStore,\n useInsertionEffect, createContext, forwardRef, memo, lazy,\n Suspense, Fragment, StrictMode, Component, PureComponent\n } from 'react';\n import { jsx, jsxs } from 'react/jsx-runtime';\n import { hydrateRoot, createRoot } from 'react-dom/client';\n\n export {\n useState, useEffect, useContext, useReducer, useCallback, useMemo,\n useRef, useImperativeHandle, useLayoutEffect, useDebugValue,\n useDeferredValue, useTransition, useId, useSyncExternalStore,\n useInsertionEffect, createContext, forwardRef, memo, lazy,\n Suspense, Fragment, StrictMode, Component, PureComponent,\n hydrateRoot, createRoot, jsx, jsxs\n };\n export default React;\n `,\n loader: 'ts',\n },\n bundle: true,\n write: false,\n treeShaking: true,\n minify: false,\n format: 'esm',\n jsx: 'automatic',\n alias: {\n react: path.dirname(fileURLToPath(import.meta.resolve('react/package.json'))),\n 'react-dom': path.dirname(fileURLToPath(import.meta.resolve('react-dom/package.json'))),\n },\n define: { 'process.env.NODE_ENV': '\"development\"' },\n }).then(r => r.outputFiles[0].text);\n }\n\n res.setHeader('Content-Type', 'application/javascript');\n res.end(await reactBundlePromise);\n}\n\n// \u2500\u2500\u2500 NukeJS runtime bundle \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\n\n/**\n * Bundles and serves the NukeJS client runtime to /__n.js.\n *\n * The entry point is bundle.ts (or bundle.js in production dist/).\n * React is kept external so it resolves via the importmap to /__react.js.\n *\n * Minified because this script is loaded on every page request.\n */\nexport async function serveNukeBundle(res: ServerResponse): Promise<void> {\n log.verbose('Bundling nuke runtime');\n\n if (!nukeBundlePromise) {\n const dir = path.dirname(fileURLToPath(import.meta.url));\n const entry = path.join(dir, `bundle.${dir.endsWith('dist') ? 'js' : 'ts'}`);\n nukeBundlePromise = build({\n entryPoints: [entry],\n write: false,\n format: 'esm',\n minify: true,\n bundle: true,\n external: ['react', 'react-dom/client'],\n }).then(r => r.outputFiles[0].text);\n }\n\n res.setHeader('Content-Type', 'application/javascript');\n res.end(await nukeBundlePromise);\n}"],
|
|
5
|
+
"mappings": "AAoBA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,aAAa;AAEtB,SAAS,WAAW;AACpB,SAAS,wBAAwB;AAMjC,IAAI,qBAA6C;AACjD,IAAI,oBAA4C;AAahD,eAAsB,sBAAsB,UAAmC;AAC7E,QAAM,SAAS,MAAM,MAAM;AAAA,IACzB,aAAa,CAAC,QAAQ;AAAA,IACtB,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,OAAO;AAAA,IACP,KAAK;AAAA;AAAA,IAEL,UAAU,CAAC,SAAS,oBAAoB,mBAAmB;AAAA,EAC7D,CAAC;AACD,SAAO,OAAO,YAAY,CAAC,EAAE;AAC/B;AASA,eAAsB,2BACpB,aACA,KACe;AACf,QAAM,WAAW,iBAAiB,WAAW;AAC7C,MAAI,UAAU;AACZ,QAAI,QAAQ,8BAA8B,WAAW,KAAK,KAAK,SAAS,QAAQ,CAAC,GAAG;AACpF,QAAI,UAAU,gBAAgB,wBAAwB;AACtD,QAAI,IAAI,MAAM,sBAAsB,QAAQ,CAAC;AAC7C;AAAA,EACF;AAIA,MAAI,MAAM,+BAA+B,WAAW,EAAE;AACtD,MAAI,aAAa;AACjB,MAAI,IAAI,4BAA4B;AACtC;AAcA,eAAsB,iBAAiB,KAAoC;AACzE,MAAI,QAAQ,wBAAwB;AAEpC,MAAI,CAAC,oBAAoB;AACvB,yBAAqB,MAAM;AAAA,MACzB,OAAO;AAAA,QACL,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAqBV,QAAQ;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,KAAK;AAAA,MACL,OAAO;AAAA,QACL,OAAO,KAAK,QAAQ,cAAc,YAAY,QAAQ,oBAAoB,CAAC,CAAC;AAAA,QAC5E,aAAa,KAAK,QAAQ,cAAc,YAAY,QAAQ,wBAAwB,CAAC,CAAC;AAAA,MACxF;AAAA,MACA,QAAQ,EAAE,wBAAwB,gBAAgB;AAAA,IACpD,CAAC,EAAE,KAAK,OAAK,EAAE,YAAY,CAAC,EAAE,IAAI;AAAA,EACpC;AAEA,MAAI,UAAU,gBAAgB,wBAAwB;AACtD,MAAI,IAAI,MAAM,kBAAkB;AAClC;AAYA,eAAsB,gBAAgB,KAAoC;AACxE,MAAI,QAAQ,uBAAuB;AAEnC,MAAI,CAAC,mBAAmB;AACtB,UAAM,MAAM,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AACvD,UAAM,QAAQ,KAAK,KAAK,KAAK,UAAU,IAAI,SAAS,MAAM,IAAI,OAAO,IAAI,EAAE;AAC3E,wBAAoB,MAAM;AAAA,MACxB,aAAa,CAAC,KAAK;AAAA,MACnB,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,UAAU,CAAC,SAAS,kBAAkB;AAAA,IACxC,CAAC,EAAE,KAAK,OAAK,EAAE,YAAY,CAAC,EAAE,IAAI;AAAA,EACpC;AAEA,MAAI,UAAU,gBAAgB,wBAAwB;AACtD,MAAI,IAAI,MAAM,iBAAiB;AACjC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/middleware.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "fs";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { hmrClients } from "./hmr.js";
|
|
6
6
|
import { getMimeType } from "./utils.js";
|
|
7
|
+
let hmrBundlePromise = null;
|
|
7
8
|
const PUBLIC_DIR = path.resolve("./app/public");
|
|
8
9
|
async function middleware(req, res) {
|
|
9
10
|
const rawUrl = req.url ?? "/";
|
|
@@ -25,23 +26,35 @@ async function middleware(req, res) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
if (rawUrl === "/__hmr.js") {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
29
|
+
if (!hmrBundlePromise) {
|
|
30
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const entry = path.join(dir, `hmr-bundle.${dir.endsWith("dist") ? "js" : "ts"}`);
|
|
32
|
+
hmrBundlePromise = build({
|
|
33
|
+
entryPoints: [entry],
|
|
34
|
+
write: false,
|
|
35
|
+
format: "esm",
|
|
36
|
+
minify: true,
|
|
37
|
+
bundle: true,
|
|
38
|
+
external: ["react", "react-dom/client", "react/jsx-runtime"]
|
|
39
|
+
}).then((r) => r.outputFiles[0].text);
|
|
40
|
+
}
|
|
40
41
|
res.setHeader("Content-Type", "application/javascript");
|
|
41
|
-
res.end(
|
|
42
|
+
res.end(await hmrBundlePromise);
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
if (rawUrl === "/__hmr") {
|
|
46
|
+
const MAX_SSE_PER_IP = 5;
|
|
47
|
+
const remoteAddr = req.socket?.remoteAddress;
|
|
48
|
+
if (remoteAddr) {
|
|
49
|
+
const fromSameIp = [...hmrClients].filter(
|
|
50
|
+
(c) => c.socket?.remoteAddress === remoteAddr
|
|
51
|
+
);
|
|
52
|
+
if (fromSameIp.length >= MAX_SSE_PER_IP) {
|
|
53
|
+
const oldest = fromSameIp[0];
|
|
54
|
+
oldest.destroy();
|
|
55
|
+
hmrClients.delete(oldest);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
45
58
|
res.setHeader("Content-Type", "text/event-stream");
|
|
46
59
|
res.setHeader("Cache-Control", "no-cache");
|
|
47
60
|
res.setHeader("Connection", "keep-alive");
|
package/dist/middleware.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/middleware.ts"],
|
|
4
|
-
"sourcesContent": ["/**\
|
|
5
|
-
"mappings": "AAqBA,SAAS,aAAa;AAEtB,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;
|
|
4
|
+
"sourcesContent": ["/**\n * middleware.ts \u2014 Built-In NukeJS Middleware\n *\n * This is the internal middleware loaded before any user-defined middleware.\n * It handles three responsibilities:\n *\n * 1. Static public files (app/public/**)\n * Any file placed in app/public/ is served at its path relative to\n * that directory. E.g. app/public/favicon.ico \u2192 GET /favicon.ico.\n * The correct Content-Type is set automatically. Path traversal attempts\n * are rejected with 400.\n *\n * 2. HMR client script (/__hmr.js)\n * Builds and serves hmr-bundle.ts on demand. Injected into every dev\n * page as <script type=\"module\" src=\"/__hmr.js\">.\n *\n * 3. HMR SSE stream (/__hmr)\n * Long-lived Server-Sent Events connection used by the browser to receive\n * reload/replace/restart events when source files change.\n */\n\nimport { build } from 'esbuild';\nimport type { IncomingMessage, ServerResponse } from 'http';\nimport path from 'path';\nimport fs from 'fs';\nimport { fileURLToPath } from 'url';\nimport { hmrClients } from './hmr';\nimport { getMimeType } from './utils';\n\n// Cache the compiled HMR bundle Promise so esbuild only runs once per server lifetime.\n// Caching the Promise (not just the result) prevents a race condition where multiple\n// concurrent requests all see null and each kick off their own build.\nlet hmrBundlePromise: Promise<string> | null = null;\n\n// Absolute path to the static public directory.\n// Files here are served at their path relative to this directory.\nconst PUBLIC_DIR = path.resolve('./app/public');\n\nexport default async function middleware(\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n\n // \u2500\u2500 Static public files \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\n // Checked first so /favicon.ico, /main.css, etc. are never accidentally\n // routed to the SSR or API layers.\n const rawUrl = req.url ?? '/';\n const pathname = rawUrl.split('?')[0]; // strip query string\n\n if (fs.existsSync(PUBLIC_DIR)) {\n // path.join handles the leading '/' in pathname naturally and normalises\n // any '..' segments, making it safe to use directly with a startsWith guard.\n // Using path.join (not path.resolve) ensures an absolute second argument\n // cannot silently escape PUBLIC_DIR the way path.resolve would allow.\n const candidate = path.join(PUBLIC_DIR, pathname);\n\n // Path traversal guard: the resolved path must be inside PUBLIC_DIR.\n // We normalise PUBLIC_DIR with a trailing separator so that a directory\n // whose name is a prefix of another cannot pass (e.g. /public2 vs /public).\n const publicBase = PUBLIC_DIR.endsWith(path.sep) ? PUBLIC_DIR : PUBLIC_DIR + path.sep;\n const safe = candidate.startsWith(publicBase) || candidate === PUBLIC_DIR;\n\n if (!safe) {\n res.statusCode = 400;\n res.end('Bad request');\n return;\n }\n\n // Serve the file if it exists at any depth inside PUBLIC_DIR.\n // Directories are intentionally skipped (no directory listings).\n if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {\n const ext = path.extname(candidate);\n res.setHeader('Content-Type', getMimeType(ext));\n res.end(fs.readFileSync(candidate));\n return;\n }\n }\n\n // \u2500\u2500 HMR client script \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\n // Builds hmr-bundle.ts on demand so the browser always gets the latest version.\n if (rawUrl === '/__hmr.js') {\n if (!hmrBundlePromise) {\n const dir = path.dirname(fileURLToPath(import.meta.url));\n const entry = path.join(dir, `hmr-bundle.${dir.endsWith('dist') ? 'js' : 'ts'}`);\n hmrBundlePromise = build({\n entryPoints: [entry],\n write: false,\n format: 'esm',\n minify: true,\n bundle: true,\n external: ['react', 'react-dom/client', 'react/jsx-runtime'],\n }).then(r => r.outputFiles[0].text);\n }\n\n res.setHeader('Content-Type', 'application/javascript');\n res.end(await hmrBundlePromise);\n return;\n }\n\n // \u2500\u2500 HMR SSE stream \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\n // Long-lived connection tracked in hmrClients so hmr.ts can broadcast events\n // to all connected browsers when a file changes.\n if (rawUrl === '/__hmr') {\n // Each full-page reload opens a new SSE connection before the old one has\n // fully closed. After ~6 reloads the browser's connection limit is exhausted\n // and it can't make any new requests \u2014 the server appears to hang.\n // Fix: allow up to 5 SSE connections per IP; when a 6th arrives, destroy\n // the oldest one from that IP to free a slot before accepting the new one.\n const MAX_SSE_PER_IP = 5;\n const remoteAddr = req.socket?.remoteAddress;\n if (remoteAddr) {\n const fromSameIp = [...hmrClients].filter(\n c => (c as any).socket?.remoteAddress === remoteAddr\n );\n if (fromSameIp.length >= MAX_SSE_PER_IP) {\n // Drop the oldest (first in insertion order).\n const oldest = fromSameIp[0];\n oldest.destroy();\n hmrClients.delete(oldest);\n }\n }\n\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.flushHeaders();\n\n res.write('data: {\"type\":\"connected\"}\\n\\n');\n\n hmrClients.add(res);\n req.on('close', () => hmrClients.delete(res));\n return;\n }\n}"],
|
|
5
|
+
"mappings": "AAqBA,SAAS,aAAa;AAEtB,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAK5B,IAAI,mBAA2C;AAI/C,MAAM,aAAa,KAAK,QAAQ,cAAc;AAE9C,eAAO,WACL,KACA,KACe;AAKf,QAAM,SAAS,IAAI,OAAO;AAC1B,QAAM,WAAW,OAAO,MAAM,GAAG,EAAE,CAAC;AAEpC,MAAI,GAAG,WAAW,UAAU,GAAG;AAK7B,UAAM,YAAY,KAAK,KAAK,YAAY,QAAQ;AAKhD,UAAM,aAAa,WAAW,SAAS,KAAK,GAAG,IAAI,aAAa,aAAa,KAAK;AAClF,UAAM,OAAO,UAAU,WAAW,UAAU,KAAK,cAAc;AAE/D,QAAI,CAAC,MAAM;AACT,UAAI,aAAa;AACjB,UAAI,IAAI,aAAa;AACrB;AAAA,IACF;AAIA,QAAI,GAAG,WAAW,SAAS,KAAK,GAAG,SAAS,SAAS,EAAE,OAAO,GAAG;AAC/D,YAAM,MAAM,KAAK,QAAQ,SAAS;AAClC,UAAI,UAAU,gBAAgB,YAAY,GAAG,CAAC;AAC9C,UAAI,IAAI,GAAG,aAAa,SAAS,CAAC;AAClC;AAAA,IACF;AAAA,EACF;AAIA,MAAI,WAAW,aAAa;AAC1B,QAAI,CAAC,kBAAkB;AACrB,YAAM,MAAQ,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AACzD,YAAM,QAAQ,KAAK,KAAK,KAAK,cAAc,IAAI,SAAS,MAAM,IAAI,OAAO,IAAI,EAAE;AAC/E,yBAAmB,MAAM;AAAA,QACvB,aAAa,CAAC,KAAK;AAAA,QACnB,OAAa;AAAA,QACb,QAAa;AAAA,QACb,QAAa;AAAA,QACb,QAAa;AAAA,QACb,UAAa,CAAC,SAAS,oBAAoB,mBAAmB;AAAA,MAChE,CAAC,EAAE,KAAK,OAAK,EAAE,YAAY,CAAC,EAAE,IAAI;AAAA,IACpC;AAEA,QAAI,UAAU,gBAAgB,wBAAwB;AACtD,QAAI,IAAI,MAAM,gBAAgB;AAC9B;AAAA,EACF;AAKA,MAAI,WAAW,UAAU;AAMvB,UAAM,iBAAiB;AACvB,UAAM,aAAa,IAAI,QAAQ;AAC/B,QAAI,YAAY;AACd,YAAM,aAAa,CAAC,GAAG,UAAU,EAAE;AAAA,QACjC,OAAM,EAAU,QAAQ,kBAAkB;AAAA,MAC5C;AACA,UAAI,WAAW,UAAU,gBAAgB;AAEvC,cAAM,SAAS,WAAW,CAAC;AAC3B,eAAO,QAAQ;AACf,mBAAW,OAAO,MAAM;AAAA,MAC1B;AAAA,IACF;AAEA,QAAI,UAAU,gBAAgB,mBAAmB;AACjD,QAAI,UAAU,iBAAiB,UAAU;AACzC,QAAI,UAAU,cAAc,YAAY;AACxC,QAAI,UAAU,+BAA+B,GAAG;AAChD,QAAI,aAAa;AAEjB,QAAI,MAAM,gCAAgC;AAE1C,eAAW,IAAI,GAAG;AAClB,QAAI,GAAG,SAAS,MAAM,WAAW,OAAO,GAAG,CAAC;AAC5C;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nukejs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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",
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
"build": "npx tsx src/builder.ts",
|
|
22
22
|
"dev": "cd playground && node ../bin/index.mjs dev",
|
|
23
23
|
"pack": "npm run build && npm pack",
|
|
24
|
-
"publish": "npm run build && npm publish",
|
|
25
24
|
"test": "npx jest"
|
|
26
25
|
},
|
|
27
26
|
"repository": {
|