nukejs 0.0.20 → 0.0.22
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/README.md +37 -7
- package/bin/index.mjs +44 -5
- package/dist/build-cloudflare.js +699 -0
- package/dist/build-common.js +14 -1
- package/dist/build-node.js +53 -0
- package/dist/build-vercel.js +23 -4
- package/dist/bundle.js +3 -1
- package/dist/middleware-loader.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ NukeJS gives you:
|
|
|
40
40
|
| **SPA navigation** | Client-side page transitions after first load |
|
|
41
41
|
| **Hot module replacement** | Instant page updates during development |
|
|
42
42
|
| **Zero config** | Works out of the box; `nuke.config.ts` for overrides |
|
|
43
|
-
| **Deploy anywhere** | Node.js or
|
|
43
|
+
| **Deploy anywhere** | Node.js, Vercel, or Cloudflare — zero config |
|
|
44
44
|
|
|
45
45
|
### The core idea
|
|
46
46
|
|
|
@@ -442,7 +442,8 @@ export async function GET(req: ApiRequest, res: ApiResponse) {
|
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
export async function POST(req: ApiRequest, res: ApiResponse) {
|
|
445
|
-
const
|
|
445
|
+
const body = await req.json();
|
|
446
|
+
const user = await db.createUser(body);
|
|
446
447
|
res.json(user, 201);
|
|
447
448
|
}
|
|
448
449
|
```
|
|
@@ -464,14 +465,23 @@ export async function DELETE(req: ApiRequest, res: ApiResponse) {
|
|
|
464
465
|
|
|
465
466
|
### Request object
|
|
466
467
|
|
|
467
|
-
| Property | Type | Description |
|
|
468
|
+
| Property / Method | Type | Description |
|
|
468
469
|
|---|---|---|
|
|
469
|
-
| `req.
|
|
470
|
+
| `req.json<T>()` | `Promise<T>` | Parse the request body as JSON (10 MB limit, prototype-pollution guard) |
|
|
471
|
+
| `req.text()` | `Promise<string>` | Read the request body as a UTF-8 string (10 MB limit) |
|
|
472
|
+
| `req.buffer()` | `Promise<Buffer>` | Read the request body as a raw `Buffer` — use this for binary or multipart data |
|
|
470
473
|
| `req.params` | `Record<string, string \| string[]>` | Dynamic route segments |
|
|
471
474
|
| `req.query` | `Record<string, string>` | URL search params |
|
|
472
475
|
| `req.method` | `string` | HTTP method |
|
|
473
476
|
| `req.headers` | `IncomingHttpHeaders` | Request headers |
|
|
474
477
|
|
|
478
|
+
> **Multipart / file uploads:** body helpers do not parse `multipart/form-data`. Pipe `req` directly into a multipart parser instead:
|
|
479
|
+
> ```ts
|
|
480
|
+
> import busboy from 'busboy';
|
|
481
|
+
> const bb = busboy({ headers: req.headers });
|
|
482
|
+
> req.pipe(bb);
|
|
483
|
+
> ```
|
|
484
|
+
|
|
475
485
|
### Response object
|
|
476
486
|
|
|
477
487
|
| Method | Description |
|
|
@@ -550,8 +560,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
|
|
550
560
|
| `nuke dev` | Served by the built-in middleware before any API or SSR routing |
|
|
551
561
|
| `nuke build` (Node) | Copied to `dist/static/` and served by the production HTTP server |
|
|
552
562
|
| `nuke build` (Vercel) | Copied to `.vercel/output/static/` — served by Vercel's CDN, no function invocation |
|
|
553
|
-
|
|
554
|
-
On Vercel, public files receive the same zero-latency CDN treatment as `__n.js`.
|
|
563
|
+
| `nuke build` (Cloudflare) | Copied to `.cloudflare/output/static/` — served by Cloudflare's CDN, no Worker invocation |
|
|
555
564
|
|
|
556
565
|
---
|
|
557
566
|
|
|
@@ -1022,7 +1031,28 @@ dist/
|
|
|
1022
1031
|
|
|
1023
1032
|
### Vercel
|
|
1024
1033
|
|
|
1025
|
-
Just import the code from GitHub.
|
|
1034
|
+
Just import the code from GitHub. NukeJS detects the Vercel environment automatically and builds the right output — no configuration needed.
|
|
1035
|
+
|
|
1036
|
+
### Cloudflare Workers & Pages
|
|
1037
|
+
|
|
1038
|
+
Just import the code from GitHub. NukeJS detects the Cloudflare environment automatically and builds the right output — no configuration needed.
|
|
1039
|
+
|
|
1040
|
+
The build output goes to `.cloudflare/output/`:
|
|
1041
|
+
|
|
1042
|
+
```
|
|
1043
|
+
.cloudflare/output/
|
|
1044
|
+
├── _worker.mjs # Single ESM Cloudflare Worker (all routes bundled)
|
|
1045
|
+
└── static/
|
|
1046
|
+
├── __n.js # NukeJS client runtime
|
|
1047
|
+
├── __client-component/ # Bundled "use client" component files
|
|
1048
|
+
└── <app/public files> # Copied from app/public/ at build time
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
Static files in `static/` are served directly by Cloudflare's CDN — the Worker is only invoked for pages and API routes.
|
|
1052
|
+
|
|
1053
|
+
> **Cloudflare Pages (recommended):** Set your build output directory to `.cloudflare/output` in the Pages dashboard. Static assets are served via CDN automatically through the `ASSETS` binding.
|
|
1054
|
+
|
|
1055
|
+
> **Cloudflare Workers:** Use `wrangler deploy` as your deploy command. Static assets are inlined into the worker bundle at build time — no separate CDN step required.
|
|
1026
1056
|
|
|
1027
1057
|
### Environment variables
|
|
1028
1058
|
|
package/bin/index.mjs
CHANGED
|
@@ -106,21 +106,60 @@ if (!arg || arg === 'dev') {
|
|
|
106
106
|
runWithTsx(devScript, { ENVIRONMENT: 'development' });
|
|
107
107
|
|
|
108
108
|
} else if (arg === 'build') {
|
|
109
|
-
// nuke build → run
|
|
109
|
+
// nuke build [--cloudflare|--vercel] → run the appropriate compiled build script.
|
|
110
|
+
//
|
|
111
|
+
// Target selection order:
|
|
112
|
+
// 1. --cloudflare / --vercel flag (explicit CLI override)
|
|
113
|
+
// 2. package.json "nuke": { "target": "cloudflare"|"vercel" } (project config)
|
|
114
|
+
// 3. CF_PAGES / CLOUDFLARE_WORKERS env vars (Cloudflare Pages CI)
|
|
115
|
+
// 4. VERCEL / VERCEL_ENV / NOW_BUILDER env vars (Vercel CI)
|
|
116
|
+
// 5. Default: Node.js build
|
|
117
|
+
|
|
118
|
+
const extraArgs = process.argv.slice(3);
|
|
119
|
+
|
|
120
|
+
let pkgTarget = null;
|
|
121
|
+
try {
|
|
122
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
123
|
+
if (fs.existsSync(pkgPath)) {
|
|
124
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
125
|
+
pkgTarget = pkg?.nuke?.target ?? null;
|
|
126
|
+
}
|
|
127
|
+
} catch { /* malformed package.json — ignore, fall through */ }
|
|
128
|
+
|
|
129
|
+
const isCloudflare = !!(
|
|
130
|
+
extraArgs.includes('--cloudflare') ||
|
|
131
|
+
pkgTarget === 'cloudflare' ||
|
|
132
|
+
process.env.CLOUDFLARE_ACCOUNT_ID || // injected by all Cloudflare CI (Workers + Pages)
|
|
133
|
+
process.env.CLOUDFLARE_API_TOKEN || // injected by all Cloudflare CI
|
|
134
|
+
process.env.WORKERS_CI || // Cloudflare Workers CI
|
|
135
|
+
process.env.CF_PAGES ||
|
|
136
|
+
process.env.CF_PAGES_BRANCH ||
|
|
137
|
+
process.env.CF_PAGES_COMMIT_SHA ||
|
|
138
|
+
process.env.CF_PAGES_URL ||
|
|
139
|
+
process.env.CLOUDFLARE_WORKERS
|
|
140
|
+
);
|
|
141
|
+
|
|
110
142
|
const isVercel = !!(
|
|
143
|
+
extraArgs.includes('--vercel') ||
|
|
144
|
+
pkgTarget === 'vercel' ||
|
|
111
145
|
process.env.VERCEL ||
|
|
112
146
|
process.env.VERCEL_ENV ||
|
|
113
147
|
process.env.NOW_BUILDER
|
|
114
148
|
);
|
|
115
149
|
|
|
116
|
-
|
|
117
|
-
|
|
150
|
+
// Resolve the target once so the spawned build script can read it too.
|
|
151
|
+
const nukeTarget = isCloudflare ? 'cloudflare' : isVercel ? 'vercel' : 'node';
|
|
152
|
+
|
|
153
|
+
if (isCloudflare) {
|
|
154
|
+
runWithNode(path.join(distDir, 'build-cloudflare.js'), { NUKE_TARGET: nukeTarget });
|
|
155
|
+
} else if (isVercel) {
|
|
156
|
+
runWithNode(path.join(distDir, 'build-vercel.js'), { NUKE_TARGET: nukeTarget });
|
|
118
157
|
} else {
|
|
119
|
-
runWithNode(path.join(distDir, 'build-node.js'));
|
|
158
|
+
runWithNode(path.join(distDir, 'build-node.js'), { NUKE_TARGET: nukeTarget });
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
} else {
|
|
123
162
|
console.error(`\n ✖ Unknown command: "${arg}"`);
|
|
124
|
-
console.error(` Usage: nuke [dev|build]\n`);
|
|
163
|
+
console.error(` Usage: nuke [dev|build [--cloudflare|--vercel]]\n`);
|
|
125
164
|
process.exit(1);
|
|
126
165
|
}
|
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { build } from "esbuild";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import {
|
|
7
|
+
walkFiles,
|
|
8
|
+
analyzeFile,
|
|
9
|
+
collectServerPages,
|
|
10
|
+
collectGlobalClientRegistry,
|
|
11
|
+
bundleClientComponents,
|
|
12
|
+
findPageLayouts,
|
|
13
|
+
buildPerPageRegistry,
|
|
14
|
+
makePageAdapterSource,
|
|
15
|
+
buildCombinedBundle,
|
|
16
|
+
copyPublicFiles
|
|
17
|
+
} from "./build-common.js";
|
|
18
|
+
const OUTPUT_DIR = path.resolve(".cloudflare/output");
|
|
19
|
+
const STATIC_DIR = path.join(OUTPUT_DIR, "static");
|
|
20
|
+
if (fs.existsSync(OUTPUT_DIR)) {
|
|
21
|
+
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
|
|
22
|
+
console.log("\u{1F5D1}\uFE0F Cleaned .cloudflare/output/");
|
|
23
|
+
}
|
|
24
|
+
fs.mkdirSync(STATIC_DIR, { recursive: true });
|
|
25
|
+
const config = await loadConfig();
|
|
26
|
+
const SERVER_DIR = path.resolve(config.serverDir);
|
|
27
|
+
const PAGES_DIR = path.resolve("./app/pages");
|
|
28
|
+
const PUBLIC_DIR = path.resolve("./app/public");
|
|
29
|
+
const CF_EXTERNALS = ["cloudflare:*", "__STATIC_CONTENT_MANIFEST"];
|
|
30
|
+
const CF_DEFINE = {
|
|
31
|
+
"process.env.NODE_ENV": '"production"'
|
|
32
|
+
};
|
|
33
|
+
const NODE_SHIM = (
|
|
34
|
+
/* js */
|
|
35
|
+
`
|
|
36
|
+
// \u2500\u2500\u2500 Node req/res shim \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
37
|
+
|
|
38
|
+
class __NodeRequest__ {
|
|
39
|
+
constructor(cfRequest, parsedUrl, bodyBytes) {
|
|
40
|
+
this.url = parsedUrl.pathname + parsedUrl.search;
|
|
41
|
+
this.method = cfRequest.method;
|
|
42
|
+
this.headers = Object.fromEntries(cfRequest.headers.entries());
|
|
43
|
+
this.body = null; // populated externally before handler call
|
|
44
|
+
this.query = Object.fromEntries(parsedUrl.searchParams.entries());
|
|
45
|
+
this.params = {};
|
|
46
|
+
// Pre-read body bytes for stream emulation
|
|
47
|
+
this._bodyBytes = bodyBytes; // Uint8Array | null
|
|
48
|
+
this._dataFns = [];
|
|
49
|
+
this._endFns = [];
|
|
50
|
+
this._errorFns = [];
|
|
51
|
+
this._streamQueued = false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Stream interface \u2014 'data' / 'end' / 'error' \u2014 used by body-parsing
|
|
55
|
+
// middleware and req.json() / req.text() from the API adapter template.
|
|
56
|
+
on(event, fn) {
|
|
57
|
+
if (event === 'data') this._dataFns.push(fn);
|
|
58
|
+
else if (event === 'end') this._endFns.push(fn);
|
|
59
|
+
else if (event === 'error') this._errorFns.push(fn);
|
|
60
|
+
|
|
61
|
+
// Schedule a single microtask flush the first time a listener is added.
|
|
62
|
+
// All listeners registered synchronously in the same tick will be ready
|
|
63
|
+
// by the time the microtask fires.
|
|
64
|
+
if (!this._streamQueued) {
|
|
65
|
+
this._streamQueued = true;
|
|
66
|
+
Promise.resolve().then(() => {
|
|
67
|
+
if (this._bodyBytes && this._bodyBytes.length > 0) {
|
|
68
|
+
for (const fn of this._dataFns) fn(this._bodyBytes);
|
|
69
|
+
}
|
|
70
|
+
for (const fn of this._endFns) fn();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
off(event, fn) {
|
|
77
|
+
if (event === 'data') this._dataFns = this._dataFns.filter(f => f !== fn);
|
|
78
|
+
else if (event === 'end') this._endFns = this._endFns.filter(f => f !== fn);
|
|
79
|
+
else if (event === 'error') this._errorFns = this._errorFns.filter(f => f !== fn);
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
destroy() {}
|
|
84
|
+
resume() { return this; }
|
|
85
|
+
pause() { return this; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class __NodeResponse__ {
|
|
89
|
+
constructor() {
|
|
90
|
+
this.statusCode = 200;
|
|
91
|
+
this._headers = new Headers();
|
|
92
|
+
this._chunks = [];
|
|
93
|
+
this._resolve = null;
|
|
94
|
+
this._promise = new Promise(r => { this._resolve = r; });
|
|
95
|
+
// Attach NukeJS dispatcher helpers directly on construction so handlers
|
|
96
|
+
// receive them regardless of which dispatcher path is used.
|
|
97
|
+
this.json = (data, status = 200) => {
|
|
98
|
+
this.statusCode = status;
|
|
99
|
+
this.setHeader('content-type', 'application/json; charset=utf-8');
|
|
100
|
+
this.end(JSON.stringify(data));
|
|
101
|
+
};
|
|
102
|
+
this.status = (code) => { this.statusCode = code; return this; };
|
|
103
|
+
this.redirect = (location, code = 302) => {
|
|
104
|
+
this.statusCode = code;
|
|
105
|
+
this.setHeader('location', String(location));
|
|
106
|
+
this.end();
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setHeader(name, value) { this._headers.set(String(name), String(value)); }
|
|
111
|
+
getHeader(name) { return this._headers.get(String(name)) ?? undefined; }
|
|
112
|
+
removeHeader(name) { this._headers.delete(String(name)); }
|
|
113
|
+
hasHeader(name) { return this._headers.has(String(name)); }
|
|
114
|
+
|
|
115
|
+
write(chunk) {
|
|
116
|
+
if (chunk == null) return;
|
|
117
|
+
this._chunks.push(typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
end(chunk) {
|
|
121
|
+
if (chunk != null)
|
|
122
|
+
this._chunks.push(typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk));
|
|
123
|
+
this._resolve(
|
|
124
|
+
new Response(this._chunks.join(''), {
|
|
125
|
+
status: this.statusCode,
|
|
126
|
+
headers: this._headers,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Await this inside the fetch handler to get the completed Web Response.
|
|
132
|
+
toResponse() { return this._promise; }
|
|
133
|
+
}
|
|
134
|
+
`
|
|
135
|
+
);
|
|
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
|
+
` : "";
|
|
143
|
+
const imports = routes.map((r, i) => `import * as __api_${i}__ from ${JSON.stringify(r.absPath)};`).join("\n");
|
|
144
|
+
const routeEntries = routes.map(
|
|
145
|
+
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, mod: __api_${i}__ },`
|
|
146
|
+
).join("\n");
|
|
147
|
+
return (
|
|
148
|
+
/* ts */
|
|
149
|
+
`import type { IncomingMessage, ServerResponse } from 'http';
|
|
150
|
+
${imports}
|
|
151
|
+
${middlewareImport}
|
|
152
|
+
|
|
153
|
+
const __CF_API_ROUTES__ = [
|
|
154
|
+
${routeEntries}
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Try to dispatch \`req\` to an API route.
|
|
159
|
+
* Returns true if a route matched (even if the handler threw), false otherwise.
|
|
160
|
+
*/
|
|
161
|
+
export async function __dispatchApi__(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
162
|
+
const url = new URL((req as any).url || '/', 'http://localhost');
|
|
163
|
+
const pathname = url.pathname;
|
|
164
|
+
${middlewareRun}
|
|
165
|
+
for (const route of __CF_API_ROUTES__) {
|
|
166
|
+
const m = pathname.match(new RegExp(route.regex));
|
|
167
|
+
if (!m) continue;
|
|
168
|
+
|
|
169
|
+
const method = ((req.method || 'GET')).toUpperCase();
|
|
170
|
+
const apiReq = req as any;
|
|
171
|
+
const apiRes = res as any;
|
|
172
|
+
|
|
173
|
+
// Populate NukeJS API handler surface on the shim.
|
|
174
|
+
apiReq.query = Object.fromEntries(url.searchParams.entries());
|
|
175
|
+
apiReq.params = {};
|
|
176
|
+
route.params.forEach((name: string, i: number) => { apiReq.params[name] = m[i + 1]; });
|
|
177
|
+
|
|
178
|
+
// Attach .json() / .text() / .buffer() that resolve from the pre-read body.
|
|
179
|
+
const rawBytes: Uint8Array | null = apiReq._bodyBytes ?? null;
|
|
180
|
+
apiReq.text = () => Promise.resolve(rawBytes ? new TextDecoder().decode(rawBytes) : '');
|
|
181
|
+
apiReq.json = () => apiReq.text().then((t: string) => {
|
|
182
|
+
const parsed = t ? JSON.parse(t) : null;
|
|
183
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
184
|
+
delete parsed.__proto__;
|
|
185
|
+
delete parsed.constructor;
|
|
186
|
+
}
|
|
187
|
+
return parsed;
|
|
188
|
+
});
|
|
189
|
+
apiReq.buffer = () => Promise.resolve(rawBytes ?? new Uint8Array(0));
|
|
190
|
+
|
|
191
|
+
const fn = (route.mod as any)[method] ?? (route.mod as any)['default'];
|
|
192
|
+
if (typeof fn !== 'function') {
|
|
193
|
+
apiRes.json({ error: \`Method \${method} not allowed\` }, 405);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
await fn(apiReq, apiRes);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
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
|
+
` : "";
|
|
211
|
+
const imports = routes.map((r, i) => `import __page_${i}__ from ${JSON.stringify(r.adapterPath)};`).join("\n");
|
|
212
|
+
const routeEntries = routes.map(
|
|
213
|
+
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, catchAll: ${JSON.stringify(r.catchAllNames)}, handler: __page_${i}__ },`
|
|
214
|
+
).join("\n");
|
|
215
|
+
const error404Import = errorAdapters2.adapter404 ? `import __error_404__ from ${JSON.stringify(errorAdapters2.adapter404)};` : "";
|
|
216
|
+
const error500Import = errorAdapters2.adapter500 ? `import __error_500__ from ${JSON.stringify(errorAdapters2.adapter500)};` : "";
|
|
217
|
+
const notFoundFallback = errorAdapters2.adapter404 ? ` try { await __error_404__(req, res); return true; } catch(e) { console.error('[_404 error]', e); }` : ` (res as any).statusCode = 404;
|
|
218
|
+
res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
219
|
+
res.end('Not Found');`;
|
|
220
|
+
const clientErrHandler = errorAdapters2.adapter500 ? (
|
|
221
|
+
/* ts */
|
|
222
|
+
` try {
|
|
223
|
+
const eq = new URLSearchParams();
|
|
224
|
+
eq.set('__errorMessage', url.searchParams.get('__clientError') || 'Client error');
|
|
225
|
+
const stack = url.searchParams.get('__clientStack');
|
|
226
|
+
if (stack) eq.set('__errorStack', stack);
|
|
227
|
+
(req as any).url = '/_500?' + eq.toString();
|
|
228
|
+
await __error_500__(req, res);
|
|
229
|
+
return true;
|
|
230
|
+
} catch(e) { console.error('[_500 client error]', e); }`
|
|
231
|
+
) : ` (res as any).statusCode = 500; res.end('Internal Server Error'); return true;`;
|
|
232
|
+
const errHandler = errorAdapters2.adapter500 ? (
|
|
233
|
+
/* ts */
|
|
234
|
+
` try {
|
|
235
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
236
|
+
const errStack = err instanceof Error ? err.stack : undefined;
|
|
237
|
+
const eq = new URLSearchParams();
|
|
238
|
+
eq.set('__errorMessage', errMsg);
|
|
239
|
+
if (errStack) eq.set('__errorStack', errStack);
|
|
240
|
+
(req as any).url = '/_500?' + eq.toString();
|
|
241
|
+
await __error_500__(req, res);
|
|
242
|
+
return true;
|
|
243
|
+
} catch(e) { console.error('[_500 error]', e); }`
|
|
244
|
+
) : ` (res as any).statusCode = 500; res.end('Internal Server Error'); return true;`;
|
|
245
|
+
return (
|
|
246
|
+
/* ts */
|
|
247
|
+
`import type { IncomingMessage, ServerResponse } from 'http';
|
|
248
|
+
${imports}
|
|
249
|
+
${error404Import}
|
|
250
|
+
${error500Import}
|
|
251
|
+
${middlewareImport}
|
|
252
|
+
|
|
253
|
+
const __CF_PAGE_ROUTES__: Array<{
|
|
254
|
+
regex: string;
|
|
255
|
+
params: string[];
|
|
256
|
+
catchAll: string[];
|
|
257
|
+
handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
258
|
+
}> = [
|
|
259
|
+
${routeEntries}
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Try to dispatch \`req\` to a page route.
|
|
264
|
+
* Returns true if a route matched (even if the handler threw), false otherwise.
|
|
265
|
+
*/
|
|
266
|
+
export async function __dispatchPages__(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
267
|
+
const url = new URL((req as any).url || '/', 'http://localhost');
|
|
268
|
+
const pathname = url.pathname;
|
|
269
|
+
${middlewareRun}
|
|
270
|
+
// Client-side error \u2014 forward to _500 page if available.
|
|
271
|
+
if (url.searchParams.has('__clientError')) {
|
|
272
|
+
${clientErrHandler}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const route of __CF_PAGE_ROUTES__) {
|
|
276
|
+
const m = pathname.match(new RegExp(route.regex));
|
|
277
|
+
if (!m) continue;
|
|
278
|
+
|
|
279
|
+
const catchAllSet = new Set(route.catchAll);
|
|
280
|
+
route.params.forEach((name, i) => {
|
|
281
|
+
const raw = m[i + 1] ?? '';
|
|
282
|
+
if (catchAllSet.has(name)) {
|
|
283
|
+
raw.split('/').filter(Boolean).forEach(seg => url.searchParams.append(name, seg));
|
|
284
|
+
} else {
|
|
285
|
+
url.searchParams.set(name, raw);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
(req as any).url = pathname + (url.search || '');
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
await route.handler(req, res);
|
|
292
|
+
return true;
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.error('[page handler error]', err);
|
|
295
|
+
${errHandler}
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
${notFoundFallback}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
function makeWorkerEntrySource(hasApi2, hasPages2, inlineStaticMap2, middlewarePath) {
|
|
307
|
+
const apiImport = hasApi2 ? `import { __dispatchApi__ } from "./cf-api-dispatcher.js";` : "";
|
|
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
|
+
` : "";
|
|
314
|
+
const apiDispatch = hasApi2 ? `if (await __dispatchApi__(nodeReq as any, nodeRes as any)) return nodeRes.toResponse();` : "";
|
|
315
|
+
const pagesDispatch = hasPages2 ? `if (await __dispatchPages__(nodeReq as any, nodeRes as any)) return nodeRes.toResponse();` : "";
|
|
316
|
+
return (
|
|
317
|
+
/* ts */
|
|
318
|
+
`${NODE_SHIM}
|
|
319
|
+
|
|
320
|
+
${apiImport}
|
|
321
|
+
${pagesImport}
|
|
322
|
+
${middlewareImport}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Pre-buffer the request body into a Uint8Array so we can both:
|
|
326
|
+
* a) inject it into req.body (parsed), and
|
|
327
|
+
* b) expose a fake stream interface on the shim (on('data', \u2026)).
|
|
328
|
+
* Returns null for requests without bodies (GET, HEAD, OPTIONS).
|
|
329
|
+
*/
|
|
330
|
+
const __INLINE_STATIC_MAP__ = new Map<string, { ct: string; body: string; text: boolean }>([
|
|
331
|
+
${inlineStaticMap2}
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
async function readBodyBytes(request: Request): Promise<Uint8Array | null> {
|
|
335
|
+
const noBody = ['GET', 'HEAD', 'OPTIONS'].includes(request.method.toUpperCase());
|
|
336
|
+
if (noBody) return null;
|
|
337
|
+
try {
|
|
338
|
+
const buf = await request.arrayBuffer();
|
|
339
|
+
return buf.byteLength > 0 ? new Uint8Array(buf) : null;
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Attempt to parse the raw body bytes according to the Content-Type header.
|
|
347
|
+
* Returns the parsed body (object, string, or null).
|
|
348
|
+
*/
|
|
349
|
+
function parseBodyBytes(bodyBytes: Uint8Array | null, contentType: string): unknown {
|
|
350
|
+
if (!bodyBytes || bodyBytes.length === 0) return null;
|
|
351
|
+
const text = new TextDecoder().decode(bodyBytes);
|
|
352
|
+
try {
|
|
353
|
+
if (contentType.includes('application/json')) {
|
|
354
|
+
const parsed = JSON.parse(text);
|
|
355
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
356
|
+
delete (parsed as any).__proto__;
|
|
357
|
+
delete (parsed as any).constructor;
|
|
358
|
+
}
|
|
359
|
+
return parsed;
|
|
360
|
+
}
|
|
361
|
+
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
362
|
+
return Object.fromEntries(new URLSearchParams(text).entries());
|
|
363
|
+
}
|
|
364
|
+
return text;
|
|
365
|
+
} catch {
|
|
366
|
+
return text;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export default {
|
|
371
|
+
async fetch(request: Request, env: Record<string, any>, ctx: ExecutionContext): Promise<Response> {
|
|
372
|
+
const parsedUrl = new URL(request.url);
|
|
373
|
+
|
|
374
|
+
// \u2500\u2500 1. Static assets via Cloudflare Pages ASSETS binding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
375
|
+
// When deployed with Cloudflare Pages, \`env.ASSETS\` is a fetcher that
|
|
376
|
+
// serves files from the static/ output directory via the CDN. We issue a
|
|
377
|
+
// GET probe (no body) so we never accidentally consume the request body.
|
|
378
|
+
if (env && env.ASSETS) {
|
|
379
|
+
try {
|
|
380
|
+
const probe = new Request(request.url, { method: 'GET', headers: request.headers });
|
|
381
|
+
const staticResp = await (env.ASSETS as Fetcher).fetch(probe);
|
|
382
|
+
if (staticResp.status !== 404) return staticResp;
|
|
383
|
+
} catch (_) {
|
|
384
|
+
// ASSETS binding unavailable or errored \u2014 fall through to inline map.
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// \u2500\u2500 1b. Inline static asset map (standalone Workers fallback) \u2500\u2500\u2500\u2500\u2500\u2500
|
|
389
|
+
// When deployed via wrangler deploy (not Cloudflare Pages), there is
|
|
390
|
+
// no ASSETS binding. Static files are inlined at build time into this map.
|
|
391
|
+
{
|
|
392
|
+
const __inlineAsset__ = __INLINE_STATIC_MAP__.get(parsedUrl.pathname);
|
|
393
|
+
if (__inlineAsset__) {
|
|
394
|
+
const body = __inlineAsset__.text
|
|
395
|
+
? __inlineAsset__.body
|
|
396
|
+
: Uint8Array.from(atob(__inlineAsset__.body), c => c.charCodeAt(0));
|
|
397
|
+
return new Response(body, {
|
|
398
|
+
status: 200,
|
|
399
|
+
headers: { 'content-type': __inlineAsset__.ct },
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// \u2500\u2500 2. Pre-buffer the request body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
405
|
+
const bodyBytes = await readBodyBytes(request);
|
|
406
|
+
const contentType = request.headers.get('content-type') || '';
|
|
407
|
+
const parsedBody = parseBodyBytes(bodyBytes, contentType);
|
|
408
|
+
|
|
409
|
+
// \u2500\u2500 3. Build Node-compatible req / res shims \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
410
|
+
const nodeReq = new ((__NodeRequest__ as any))(request, parsedUrl, bodyBytes);
|
|
411
|
+
nodeReq.body = parsedBody;
|
|
412
|
+
const nodeRes = new ((__NodeResponse__ as any))();
|
|
413
|
+
|
|
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}
|
|
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
|
|
418
|
+
${apiDispatch}
|
|
419
|
+
|
|
420
|
+
// \u2500\u2500 5. Page 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
|
|
421
|
+
${pagesDispatch}
|
|
422
|
+
|
|
423
|
+
// \u2500\u2500 6. No route matched \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
424
|
+
return new Response('Not Found', {
|
|
425
|
+
status: 404,
|
|
426
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.error('[worker unhandled error]', err);
|
|
431
|
+
return new Response('Internal Server Error', {
|
|
432
|
+
status: 500,
|
|
433
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
async function bundleForWorker(entryPath, outPath, extraDefine = {}) {
|
|
442
|
+
const result = await build({
|
|
443
|
+
entryPoints: [entryPath],
|
|
444
|
+
bundle: true,
|
|
445
|
+
format: "esm",
|
|
446
|
+
platform: "browser",
|
|
447
|
+
target: "es2022",
|
|
448
|
+
external: CF_EXTERNALS,
|
|
449
|
+
define: { ...CF_DEFINE, ...extraDefine },
|
|
450
|
+
jsx: "automatic",
|
|
451
|
+
write: false,
|
|
452
|
+
resolveExtensions: [".js", ".mjs", ".ts", ".tsx", ".jsx", ".json"]
|
|
453
|
+
// Do NOT set `banner` — the Node shim is inline in the worker entry source.
|
|
454
|
+
});
|
|
455
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
456
|
+
fs.writeFileSync(outPath, result.outputFiles[0].text);
|
|
457
|
+
}
|
|
458
|
+
const apiFiles = walkFiles(SERVER_DIR);
|
|
459
|
+
if (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);
|
|
460
|
+
const apiRoutes = apiFiles.map((relPath) => ({
|
|
461
|
+
...analyzeFile(relPath, "api"),
|
|
462
|
+
absPath: path.join(SERVER_DIR, relPath)
|
|
463
|
+
})).sort((a, b) => b.specificity - a.specificity);
|
|
464
|
+
const serverPages = collectServerPages(PAGES_DIR);
|
|
465
|
+
await buildCombinedBundle(STATIC_DIR);
|
|
466
|
+
copyPublicFiles(PUBLIC_DIR, STATIC_DIR);
|
|
467
|
+
const TEXT_EXTS = /* @__PURE__ */ new Set([
|
|
468
|
+
".js",
|
|
469
|
+
".mjs",
|
|
470
|
+
".cjs",
|
|
471
|
+
".ts",
|
|
472
|
+
".jsx",
|
|
473
|
+
".tsx",
|
|
474
|
+
".css",
|
|
475
|
+
".html",
|
|
476
|
+
".htm",
|
|
477
|
+
".json",
|
|
478
|
+
".xml",
|
|
479
|
+
".txt",
|
|
480
|
+
".csv",
|
|
481
|
+
".svg",
|
|
482
|
+
".map"
|
|
483
|
+
]);
|
|
484
|
+
const MIME_MAP = {
|
|
485
|
+
".html": "text/html; charset=utf-8",
|
|
486
|
+
".htm": "text/html; charset=utf-8",
|
|
487
|
+
".css": "text/css; charset=utf-8",
|
|
488
|
+
".js": "application/javascript; charset=utf-8",
|
|
489
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
490
|
+
".cjs": "application/javascript; charset=utf-8",
|
|
491
|
+
".map": "application/json; charset=utf-8",
|
|
492
|
+
".json": "application/json; charset=utf-8",
|
|
493
|
+
".xml": "application/xml; charset=utf-8",
|
|
494
|
+
".txt": "text/plain; charset=utf-8",
|
|
495
|
+
".csv": "text/csv; charset=utf-8",
|
|
496
|
+
".svg": "image/svg+xml",
|
|
497
|
+
".png": "image/png",
|
|
498
|
+
".jpg": "image/jpeg",
|
|
499
|
+
".jpeg": "image/jpeg",
|
|
500
|
+
".gif": "image/gif",
|
|
501
|
+
".webp": "image/webp",
|
|
502
|
+
".avif": "image/avif",
|
|
503
|
+
".ico": "image/x-icon",
|
|
504
|
+
".bmp": "image/bmp",
|
|
505
|
+
".woff": "font/woff",
|
|
506
|
+
".woff2": "font/woff2",
|
|
507
|
+
".ttf": "font/ttf",
|
|
508
|
+
".otf": "font/otf",
|
|
509
|
+
".mp4": "video/mp4",
|
|
510
|
+
".webm": "video/webm",
|
|
511
|
+
".mp3": "audio/mpeg",
|
|
512
|
+
".wav": "audio/wav",
|
|
513
|
+
".ogg": "audio/ogg",
|
|
514
|
+
".pdf": "application/pdf",
|
|
515
|
+
".wasm": "application/wasm"
|
|
516
|
+
};
|
|
517
|
+
function walkStaticDir(dir, base = dir) {
|
|
518
|
+
const results = [];
|
|
519
|
+
if (!fs.existsSync(dir)) return results;
|
|
520
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
521
|
+
const abs = path.join(dir, entry.name);
|
|
522
|
+
if (entry.isDirectory()) {
|
|
523
|
+
results.push(...walkStaticDir(abs, base));
|
|
524
|
+
} else {
|
|
525
|
+
results.push({ rel: "/" + path.relative(base, abs).replace(/\\/g, "/"), abs });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return results;
|
|
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)");
|
|
533
|
+
let hasApi = false;
|
|
534
|
+
let hasPages = false;
|
|
535
|
+
if (apiRoutes.length > 0) {
|
|
536
|
+
hasApi = true;
|
|
537
|
+
const dispSrc = makeApiDispatcherSource(apiRoutes);
|
|
538
|
+
const dispPath = path.join(SERVER_DIR, `_cf_api_dispatcher_${randomBytes(4).toString("hex")}.ts`);
|
|
539
|
+
fs.writeFileSync(dispPath, dispSrc);
|
|
540
|
+
try {
|
|
541
|
+
const outPath = path.join(OUTPUT_DIR, "cf-api-dispatcher.js");
|
|
542
|
+
await bundleForWorker(dispPath, outPath);
|
|
543
|
+
console.log(` built API dispatcher \u2192 cf-api-dispatcher.js (${apiRoutes.length} route(s))`);
|
|
544
|
+
} finally {
|
|
545
|
+
fs.unlinkSync(dispPath);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const tempAdapterPaths = [];
|
|
549
|
+
const errorAdapterPaths = [];
|
|
550
|
+
const errorAdapters = {};
|
|
551
|
+
if (serverPages.length > 0 || ["_404.tsx", "_500.tsx"].some((f) => fs.existsSync(path.join(PAGES_DIR, f)))) {
|
|
552
|
+
hasPages = true;
|
|
553
|
+
const globalRegistry = collectGlobalClientRegistry(serverPages, PAGES_DIR);
|
|
554
|
+
const prerenderedHtml = await bundleClientComponents(globalRegistry, PAGES_DIR, STATIC_DIR);
|
|
555
|
+
const prerenderedRecord = Object.fromEntries(prerenderedHtml);
|
|
556
|
+
const dispatcherRoutes = [];
|
|
557
|
+
for (const page of serverPages) {
|
|
558
|
+
const adapterDir = path.dirname(page.absPath);
|
|
559
|
+
const adapterPath = path.join(
|
|
560
|
+
adapterDir,
|
|
561
|
+
`_cf_page_adapter_${randomBytes(4).toString("hex")}.ts`
|
|
562
|
+
);
|
|
563
|
+
const layoutPaths = findPageLayouts(page.absPath, PAGES_DIR);
|
|
564
|
+
const { registry, clientComponentNames } = buildPerPageRegistry(page.absPath, layoutPaths, PAGES_DIR);
|
|
565
|
+
const layoutImports = layoutPaths.map((lp, i) => {
|
|
566
|
+
const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
|
|
567
|
+
return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
|
|
568
|
+
}).join("\n");
|
|
569
|
+
fs.writeFileSync(
|
|
570
|
+
adapterPath,
|
|
571
|
+
makePageAdapterSource({
|
|
572
|
+
pageImport: JSON.stringify("./" + path.basename(page.absPath)),
|
|
573
|
+
layoutImports,
|
|
574
|
+
clientComponentNames,
|
|
575
|
+
allClientIds: [...registry.keys()],
|
|
576
|
+
layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
|
|
577
|
+
prerenderedHtml: prerenderedRecord,
|
|
578
|
+
routeParamNames: page.paramNames,
|
|
579
|
+
catchAllNames: page.catchAllNames
|
|
580
|
+
})
|
|
581
|
+
);
|
|
582
|
+
tempAdapterPaths.push(adapterPath);
|
|
583
|
+
dispatcherRoutes.push({
|
|
584
|
+
adapterPath,
|
|
585
|
+
srcRegex: page.srcRegex,
|
|
586
|
+
paramNames: page.paramNames,
|
|
587
|
+
catchAllNames: page.catchAllNames
|
|
588
|
+
});
|
|
589
|
+
console.log(` prepared ${path.relative(PAGES_DIR, page.absPath)} \u2192 ${page.funcPath} [page]`);
|
|
590
|
+
}
|
|
591
|
+
for (const [statusCode, key] of [[404, "adapter404"], [500, "adapter500"]]) {
|
|
592
|
+
const src = path.join(PAGES_DIR, `_${statusCode}.tsx`);
|
|
593
|
+
if (!fs.existsSync(src)) continue;
|
|
594
|
+
console.log(` building _${statusCode}.tsx \u2192 pages dispatcher [error page]`);
|
|
595
|
+
const adapterDir = path.dirname(src);
|
|
596
|
+
const adapterPath = path.join(
|
|
597
|
+
adapterDir,
|
|
598
|
+
`_cf_error_adapter_${randomBytes(4).toString("hex")}.ts`
|
|
599
|
+
);
|
|
600
|
+
const layoutPaths = findPageLayouts(src, PAGES_DIR);
|
|
601
|
+
const { registry, clientComponentNames } = buildPerPageRegistry(src, layoutPaths, PAGES_DIR);
|
|
602
|
+
const layoutImports = layoutPaths.map((lp, i) => {
|
|
603
|
+
const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
|
|
604
|
+
return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
|
|
605
|
+
}).join("\n");
|
|
606
|
+
fs.writeFileSync(
|
|
607
|
+
adapterPath,
|
|
608
|
+
makePageAdapterSource({
|
|
609
|
+
pageImport: JSON.stringify("./" + path.basename(src)),
|
|
610
|
+
layoutImports,
|
|
611
|
+
clientComponentNames,
|
|
612
|
+
allClientIds: [...registry.keys()],
|
|
613
|
+
layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
|
|
614
|
+
prerenderedHtml: prerenderedRecord,
|
|
615
|
+
routeParamNames: [],
|
|
616
|
+
catchAllNames: [],
|
|
617
|
+
statusCode
|
|
618
|
+
})
|
|
619
|
+
);
|
|
620
|
+
errorAdapters[key] = adapterPath;
|
|
621
|
+
errorAdapterPaths.push(adapterPath);
|
|
622
|
+
}
|
|
623
|
+
const pageDispSrc = makePagesDispatcherSource(dispatcherRoutes, errorAdapters);
|
|
624
|
+
const pageDispPath = path.join(
|
|
625
|
+
PAGES_DIR,
|
|
626
|
+
`_cf_pages_dispatcher_${randomBytes(4).toString("hex")}.ts`
|
|
627
|
+
);
|
|
628
|
+
fs.writeFileSync(pageDispPath, pageDispSrc);
|
|
629
|
+
try {
|
|
630
|
+
const outPath = path.join(OUTPUT_DIR, "cf-pages-dispatcher.js");
|
|
631
|
+
await bundleForWorker(pageDispPath, outPath);
|
|
632
|
+
console.log(` built Pages dispatcher \u2192 cf-pages-dispatcher.js (${serverPages.length} page(s))`);
|
|
633
|
+
} finally {
|
|
634
|
+
fs.unlinkSync(pageDispPath);
|
|
635
|
+
for (const p of tempAdapterPaths) if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
636
|
+
for (const p of errorAdapterPaths) if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const staticEntries = walkStaticDir(STATIC_DIR);
|
|
640
|
+
const inlineStaticMap = staticEntries.map(({ rel, abs }) => {
|
|
641
|
+
const ext = path.extname(rel).toLowerCase();
|
|
642
|
+
const contentType = MIME_MAP[ext] ?? "application/octet-stream";
|
|
643
|
+
const isText = TEXT_EXTS.has(ext);
|
|
644
|
+
const raw = fs.readFileSync(abs);
|
|
645
|
+
if (isText) {
|
|
646
|
+
const escaped = JSON.stringify(raw.toString("utf-8"));
|
|
647
|
+
return ` [${JSON.stringify(rel)}, { ct: ${JSON.stringify(contentType)}, body: ${escaped}, text: true }],`;
|
|
648
|
+
} else {
|
|
649
|
+
const b64 = raw.toString("base64");
|
|
650
|
+
return ` [${JSON.stringify(rel)}, { ct: ${JSON.stringify(contentType)}, body: ${JSON.stringify(b64)}, text: false }],`;
|
|
651
|
+
}
|
|
652
|
+
}).join("\n");
|
|
653
|
+
const workerSrc = makeWorkerEntrySource(hasApi, hasPages, inlineStaticMap, cfMiddlewarePath);
|
|
654
|
+
const workerSrcPath = path.join(
|
|
655
|
+
OUTPUT_DIR,
|
|
656
|
+
`_cf_worker_entry_${randomBytes(4).toString("hex")}.ts`
|
|
657
|
+
);
|
|
658
|
+
fs.writeFileSync(workerSrcPath, workerSrc);
|
|
659
|
+
try {
|
|
660
|
+
await bundleForWorker(workerSrcPath, path.join(OUTPUT_DIR, "_worker.mjs"));
|
|
661
|
+
console.log(` built Worker entry \u2192 .cloudflare/output/_worker.mjs`);
|
|
662
|
+
} finally {
|
|
663
|
+
fs.unlinkSync(workerSrcPath);
|
|
664
|
+
for (const disp of ["cf-api-dispatcher.js", "cf-pages-dispatcher.js"]) {
|
|
665
|
+
const p = path.join(OUTPUT_DIR, disp);
|
|
666
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const projectName = path.basename(process.cwd()).replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
|
670
|
+
const wranglerToml = `# Generated by NukeJS build-cloudflare \u2014 edit as needed.
|
|
671
|
+
# This file is used by \`wrangler deploy\` for standalone Workers deployment.
|
|
672
|
+
# For Cloudflare Pages, configure the build output directory in the Pages
|
|
673
|
+
# dashboard instead (.cloudflare/output).
|
|
674
|
+
|
|
675
|
+
name = "${projectName}"
|
|
676
|
+
main = ".cloudflare/output/_worker.mjs"
|
|
677
|
+
compatibility_date = "${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}"
|
|
678
|
+
|
|
679
|
+
[build]
|
|
680
|
+
command = "nuke build --cloudflare"
|
|
681
|
+
|
|
682
|
+
# Uncomment to add KV, R2, D1, or other bindings:
|
|
683
|
+
# [[kv_namespaces]]
|
|
684
|
+
# binding = "KV"
|
|
685
|
+
# id = "YOUR_KV_NAMESPACE_ID"
|
|
686
|
+
`;
|
|
687
|
+
fs.writeFileSync(path.resolve("wrangler.toml"), wranglerToml);
|
|
688
|
+
const routeCount = apiRoutes.length + serverPages.length;
|
|
689
|
+
const assetCount = staticEntries.length;
|
|
690
|
+
console.log(`
|
|
691
|
+
\u2713 Cloudflare build complete \u2014 ${routeCount} route(s), ${assetCount} static asset(s)
|
|
692
|
+
Worker: .cloudflare/output/_worker.mjs
|
|
693
|
+
Static: .cloudflare/output/static/
|
|
694
|
+
Config: wrangler.toml
|
|
695
|
+
|
|
696
|
+
Deploy options:
|
|
697
|
+
Pages \u2192 wrangler pages deploy .cloudflare/output
|
|
698
|
+
Workers \u2192 wrangler deploy
|
|
699
|
+
`);
|
package/dist/build-common.js
CHANGED
|
@@ -418,6 +418,19 @@ function renderStyleTag(tag: any): string {
|
|
|
418
418
|
return \` <style\${media}>\${tag.content ?? ''}</style>\`;
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
// \u2500\u2500\u2500 HTML minifier \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
422
|
+
// Minifies the final HTML string before sending it to the client.
|
|
423
|
+
// Sentinel comments (<!--n-head-->, <!--/n-head-->, <!--n-body-scripts-->,
|
|
424
|
+
// <!--/n-body-scripts-->) are preserved \u2014 the client runtime needs them for
|
|
425
|
+
// head diffing during soft navigation.
|
|
426
|
+
function minifyHtml(h: string): string {
|
|
427
|
+
return h
|
|
428
|
+
.replace(/<!--(?!(n-head|\\/n-head|n-body-scripts|\\/n-body-scripts))[\\s\\S]*?-->/g, '')
|
|
429
|
+
.replace(/>\\s+</g, '> <')
|
|
430
|
+
.replace(/\\s*\\n\\s*/g, '')
|
|
431
|
+
.trim();
|
|
432
|
+
}
|
|
433
|
+
|
|
421
434
|
// \u2500\u2500\u2500 Renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
422
435
|
const VOID_TAGS = new Set([
|
|
423
436
|
'area','base','br','col','embed','hr','img','input',
|
|
@@ -661,7 +674,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
661
674
|
|
|
662
675
|
res.statusCode = ${statusCode};
|
|
663
676
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
664
|
-
res.end(html);
|
|
677
|
+
res.end(minifyHtml(html));
|
|
665
678
|
} catch (err: any) {
|
|
666
679
|
// Re-throw so the server entry (build-node / build-vercel) can route to
|
|
667
680
|
// the _500 page handler. Do not swallow the error here.
|
package/dist/build-node.js
CHANGED
|
@@ -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
|
|
package/dist/build-vercel.js
CHANGED
|
@@ -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/bundle.js
CHANGED
|
@@ -166,7 +166,9 @@ function headBlock(head) {
|
|
|
166
166
|
return { nodes, closeComment };
|
|
167
167
|
}
|
|
168
168
|
function fingerprint(el) {
|
|
169
|
-
|
|
169
|
+
const attrPart = Array.from(el.attributes).sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}=${a.value}`).join("&");
|
|
170
|
+
const contentPart = el.tagName === "STYLE" ? el.textContent ?? "" : "";
|
|
171
|
+
return el.tagName + "|" + attrPart + (contentPart ? "|" + contentPart : "");
|
|
170
172
|
}
|
|
171
173
|
function syncHeadTags(doc) {
|
|
172
174
|
const live = headBlock(document.head);
|
|
@@ -26,7 +26,12 @@ async function loadMiddleware() {
|
|
|
26
26
|
appDir,
|
|
27
27
|
`middleware.${appDir.endsWith("dist") ? "js" : "ts"}`
|
|
28
28
|
);
|
|
29
|
-
const
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nukejs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
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",
|