nukejs 0.0.17 → 0.0.18
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/Link.d.ts +12 -0
- package/dist/Link.js +15 -0
- package/dist/app.js +112 -0
- package/dist/build-common.js +886 -0
- package/dist/build-node.js +217 -0
- package/dist/build-vercel.js +359 -0
- package/dist/builder.js +95 -0
- package/dist/bundle.d.ts +85 -0
- package/dist/bundle.js +322 -0
- package/dist/bundler.js +112 -0
- package/dist/component-analyzer.js +125 -0
- package/dist/config.js +29 -0
- package/dist/hmr-bundle.js +120 -0
- package/dist/hmr.js +68 -0
- package/dist/html-store.d.ts +128 -0
- package/dist/html-store.js +41 -0
- package/dist/http-server.js +172 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +28 -0
- package/dist/logger.d.ts +59 -0
- package/dist/logger.js +53 -0
- package/dist/metadata.js +42 -0
- package/dist/middleware-loader.js +53 -0
- package/dist/middleware.example.js +57 -0
- package/dist/middleware.js +71 -0
- package/dist/renderer.js +151 -0
- package/dist/request-store.d.ts +80 -0
- package/dist/request-store.js +46 -0
- package/dist/router.js +118 -0
- package/dist/ssr.js +262 -0
- package/dist/store.d.ts +104 -0
- package/dist/store.js +45 -0
- package/dist/use-html.d.ts +64 -0
- package/dist/use-html.js +128 -0
- package/dist/use-request.d.ts +74 -0
- package/dist/use-request.js +48 -0
- package/dist/use-router.d.ts +7 -0
- package/dist/use-router.js +27 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.js +61 -0
- package/package.json +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import {
|
|
5
|
+
analyzeFile,
|
|
6
|
+
walkFiles,
|
|
7
|
+
buildPages,
|
|
8
|
+
bundleApiHandler,
|
|
9
|
+
buildCombinedBundle,
|
|
10
|
+
copyPublicFiles
|
|
11
|
+
} from "./build-common.js";
|
|
12
|
+
const OUT_DIR = path.resolve("dist");
|
|
13
|
+
const API_DIR = path.join(OUT_DIR, "api");
|
|
14
|
+
const PAGES_DIR_ = path.join(OUT_DIR, "pages");
|
|
15
|
+
const STATIC_DIR = path.join(OUT_DIR, "static");
|
|
16
|
+
if (fs.existsSync(OUT_DIR)) {
|
|
17
|
+
fs.rmSync(OUT_DIR, { recursive: true, force: true });
|
|
18
|
+
console.log("\u{1F5D1}\uFE0F Cleaned dist/");
|
|
19
|
+
}
|
|
20
|
+
for (const dir of [API_DIR, PAGES_DIR_, STATIC_DIR])
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
const config = await loadConfig();
|
|
23
|
+
const SERVER_DIR = path.resolve(config.serverDir);
|
|
24
|
+
const PAGES_DIR = path.resolve("./app/pages");
|
|
25
|
+
const PUBLIC_DIR = path.resolve("./app/public");
|
|
26
|
+
const manifest = [];
|
|
27
|
+
function funcPathToFilename(funcPath, prefix) {
|
|
28
|
+
return funcPath.replace(new RegExp(`^\\/${prefix}\\/`), "") + ".mjs";
|
|
29
|
+
}
|
|
30
|
+
const apiFiles = walkFiles(SERVER_DIR);
|
|
31
|
+
if (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);
|
|
32
|
+
const apiRoutes = apiFiles.map((relPath) => ({ ...analyzeFile(relPath, "api"), absPath: path.join(SERVER_DIR, relPath) })).sort((a, b) => b.specificity - a.specificity);
|
|
33
|
+
for (const { srcRegex, paramNames, catchAllNames, funcPath, absPath } of apiRoutes) {
|
|
34
|
+
console.log(` building ${path.relative(SERVER_DIR, absPath)} \u2192 ${funcPath}`);
|
|
35
|
+
const filename = funcPathToFilename(funcPath, "api");
|
|
36
|
+
const outPath = path.join(API_DIR, filename);
|
|
37
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
38
|
+
fs.writeFileSync(outPath, await bundleApiHandler(absPath));
|
|
39
|
+
manifest.push({ srcRegex, paramNames, catchAllNames, handler: path.join("api", filename), type: "api" });
|
|
40
|
+
}
|
|
41
|
+
const { pages: builtPages, has404, has500 } = await buildPages(PAGES_DIR, STATIC_DIR, PAGES_DIR_);
|
|
42
|
+
for (const { srcRegex, paramNames, catchAllNames, funcPath, bundleText } of builtPages) {
|
|
43
|
+
const filename = funcPathToFilename(funcPath, "page");
|
|
44
|
+
const outPath = path.join(PAGES_DIR_, filename);
|
|
45
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
46
|
+
fs.writeFileSync(outPath, bundleText);
|
|
47
|
+
manifest.push({ srcRegex, paramNames, catchAllNames, handler: path.join("pages", filename), type: "page" });
|
|
48
|
+
}
|
|
49
|
+
if (has404) console.log(" built _404.tsx \u2192 pages/_404.mjs");
|
|
50
|
+
if (has500) console.log(" built _500.tsx \u2192 pages/_500.mjs");
|
|
51
|
+
fs.writeFileSync(
|
|
52
|
+
path.join(OUT_DIR, "manifest.json"),
|
|
53
|
+
JSON.stringify({ routes: manifest }, null, 2)
|
|
54
|
+
);
|
|
55
|
+
await buildCombinedBundle(STATIC_DIR);
|
|
56
|
+
copyPublicFiles(PUBLIC_DIR, STATIC_DIR);
|
|
57
|
+
const MIME_MAP_ENTRIES = `
|
|
58
|
+
'.html': 'text/html; charset=utf-8',
|
|
59
|
+
'.htm': 'text/html; charset=utf-8',
|
|
60
|
+
'.css': 'text/css; charset=utf-8',
|
|
61
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
62
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
63
|
+
'.cjs': 'application/javascript; charset=utf-8',
|
|
64
|
+
'.map': 'application/json; charset=utf-8',
|
|
65
|
+
'.json': 'application/json; charset=utf-8',
|
|
66
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
67
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
68
|
+
'.csv': 'text/csv; charset=utf-8',
|
|
69
|
+
'.png': 'image/png',
|
|
70
|
+
'.jpg': 'image/jpeg',
|
|
71
|
+
'.jpeg': 'image/jpeg',
|
|
72
|
+
'.gif': 'image/gif',
|
|
73
|
+
'.webp': 'image/webp',
|
|
74
|
+
'.avif': 'image/avif',
|
|
75
|
+
'.svg': 'image/svg+xml',
|
|
76
|
+
'.ico': 'image/x-icon',
|
|
77
|
+
'.bmp': 'image/bmp',
|
|
78
|
+
'.woff': 'font/woff',
|
|
79
|
+
'.woff2':'font/woff2',
|
|
80
|
+
'.ttf': 'font/ttf',
|
|
81
|
+
'.otf': 'font/otf',
|
|
82
|
+
'.mp4': 'video/mp4',
|
|
83
|
+
'.webm': 'video/webm',
|
|
84
|
+
'.mp3': 'audio/mpeg',
|
|
85
|
+
'.wav': 'audio/wav',
|
|
86
|
+
'.ogg': 'audio/ogg',
|
|
87
|
+
'.pdf': 'application/pdf',
|
|
88
|
+
'.wasm': 'application/wasm',
|
|
89
|
+
`.trim();
|
|
90
|
+
const serverEntry = `import http from 'http';
|
|
91
|
+
import path from 'path';
|
|
92
|
+
import fs from 'fs';
|
|
93
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
94
|
+
|
|
95
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
96
|
+
|
|
97
|
+
const { routes } = JSON.parse(fs.readFileSync(path.join(__dirname, 'manifest.json'), 'utf-8'));
|
|
98
|
+
const compiled = routes.map(r => ({ ...r, regex: new RegExp(r.srcRegex) }));
|
|
99
|
+
|
|
100
|
+
const STATIC_DIR = path.join(__dirname, 'static');
|
|
101
|
+
const MIME_MAP = { ${MIME_MAP_ENTRIES} };
|
|
102
|
+
|
|
103
|
+
const server = http.createServer(async (req, res) => {
|
|
104
|
+
const url = req.url || '/';
|
|
105
|
+
const clean = url.split('?')[0];
|
|
106
|
+
|
|
107
|
+
// 1. Static files \u2014 app/public/ files are copied into STATIC_DIR last at
|
|
108
|
+
// build time (after framework bundles), so they take priority over
|
|
109
|
+
// framework files on name collision. path.join normalises '..' segments
|
|
110
|
+
// before the startsWith guard, preventing directory traversal.
|
|
111
|
+
{
|
|
112
|
+
const candidate = path.join(STATIC_DIR, clean);
|
|
113
|
+
const staticBase = STATIC_DIR.endsWith(path.sep) ? STATIC_DIR : STATIC_DIR + path.sep;
|
|
114
|
+
if (
|
|
115
|
+
candidate.startsWith(staticBase) &&
|
|
116
|
+
candidate !== STATIC_DIR &&
|
|
117
|
+
fs.existsSync(candidate) &&
|
|
118
|
+
fs.statSync(candidate).isFile()
|
|
119
|
+
) {
|
|
120
|
+
res.setHeader('Content-Type', MIME_MAP[path.extname(candidate)] ?? 'application/octet-stream');
|
|
121
|
+
res.end(fs.readFileSync(candidate));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 2. Client-side error \u2014 navigate directly to _500 page.
|
|
127
|
+
{
|
|
128
|
+
const qs = new URL(url, 'http://localhost').searchParams;
|
|
129
|
+
if (qs.has('__clientError')) {
|
|
130
|
+
const e500 = path.join(__dirname, 'pages', '_500.mjs');
|
|
131
|
+
if (fs.existsSync(e500)) {
|
|
132
|
+
try {
|
|
133
|
+
const m500 = await import(pathToFileURL(e500).href);
|
|
134
|
+
const eq = new URLSearchParams();
|
|
135
|
+
eq.set('__errorMessage', qs.get('__clientError') || 'Client error');
|
|
136
|
+
const stack = qs.get('__clientStack');
|
|
137
|
+
if (stack) eq.set('__errorStack', stack);
|
|
138
|
+
req.url = '/_500?' + eq.toString();
|
|
139
|
+
await m500.default(req, res);
|
|
140
|
+
return;
|
|
141
|
+
} catch (e) { console.error('[_500 render error]', e); }
|
|
142
|
+
}
|
|
143
|
+
res.statusCode = 500;
|
|
144
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
145
|
+
res.end('Internal Server Error');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Route dispatch \u2014 API routes appear before page routes in the manifest
|
|
151
|
+
// (built in build-node.ts), so they are matched first.
|
|
152
|
+
for (const { regex, paramNames, catchAllNames, handler } of compiled) {
|
|
153
|
+
const m = clean.match(regex);
|
|
154
|
+
if (!m) continue;
|
|
155
|
+
|
|
156
|
+
const catchAllSet = new Set(catchAllNames);
|
|
157
|
+
const qs = new URLSearchParams(Object.fromEntries(new URL(url, 'http://localhost').searchParams));
|
|
158
|
+
paramNames.forEach((name, i) => {
|
|
159
|
+
const raw = m[i + 1] ?? '';
|
|
160
|
+
if (catchAllSet.has(name)) {
|
|
161
|
+
raw.split('/').filter(Boolean).forEach(seg => qs.append(name, seg));
|
|
162
|
+
} else {
|
|
163
|
+
qs.set(name, raw);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
req.url = clean + (qs.toString() ? '?' + qs.toString() : '');
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const mod = await import(pathToFileURL(path.join(__dirname, handler)).href);
|
|
170
|
+
await mod.default(req, res);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error('[handler error]', err);
|
|
173
|
+
const e500 = path.join(__dirname, 'pages', '_500.mjs');
|
|
174
|
+
if (fs.existsSync(e500)) {
|
|
175
|
+
try {
|
|
176
|
+
const m500 = await import(pathToFileURL(e500).href);
|
|
177
|
+
// Inject error info as query params so _500 page receives them as props.
|
|
178
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
179
|
+
const errStack = err instanceof Error ? err.stack : undefined;
|
|
180
|
+
const errStatus = err?.status ?? err?.statusCode;
|
|
181
|
+
const eq = new URLSearchParams();
|
|
182
|
+
eq.set('__errorMessage', errMsg);
|
|
183
|
+
if (errStack) eq.set('__errorStack', errStack);
|
|
184
|
+
if (errStatus) eq.set('__errorStatus', String(errStatus));
|
|
185
|
+
req.url = '/_500?' + eq.toString();
|
|
186
|
+
await m500.default(req, res);
|
|
187
|
+
return;
|
|
188
|
+
} catch (e) { console.error('[_500 render error]', e); }
|
|
189
|
+
}
|
|
190
|
+
res.statusCode = 500;
|
|
191
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
192
|
+
res.end('Internal Server Error');
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 3. 404 \u2014 serve _404.mjs if built, otherwise plain text.
|
|
198
|
+
const e404 = path.join(__dirname, 'pages', '_404.mjs');
|
|
199
|
+
if (fs.existsSync(e404)) {
|
|
200
|
+
try {
|
|
201
|
+
const m404 = await import(pathToFileURL(e404).href);
|
|
202
|
+
await m404.default(req, res);
|
|
203
|
+
return;
|
|
204
|
+
} catch (err) { console.error('[_404 render error]', err); }
|
|
205
|
+
}
|
|
206
|
+
res.statusCode = 404;
|
|
207
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
208
|
+
res.end('Not found');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const PORT = Number(process.env.PORT ?? 3000);
|
|
212
|
+
server.listen(PORT, () => console.log('nukejs built server listening on http://localhost:' + PORT));
|
|
213
|
+
`;
|
|
214
|
+
fs.writeFileSync(path.join(OUT_DIR, "index.mjs"), serverEntry);
|
|
215
|
+
console.log(`
|
|
216
|
+
\u2713 Node build complete \u2014 ${manifest.length} route(s) \u2192 dist/`);
|
|
217
|
+
console.log(" run with: node dist/index.mjs");
|
|
@@ -0,0 +1,359 @@
|
|
|
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(".vercel/output");
|
|
19
|
+
const FUNCTIONS_DIR = path.join(OUTPUT_DIR, "functions");
|
|
20
|
+
const STATIC_DIR = path.join(OUTPUT_DIR, "static");
|
|
21
|
+
if (fs.existsSync(OUTPUT_DIR)) {
|
|
22
|
+
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
|
|
23
|
+
console.log("\u{1F5D1}\uFE0F Cleaned .vercel/output/");
|
|
24
|
+
}
|
|
25
|
+
for (const dir of [FUNCTIONS_DIR, STATIC_DIR])
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
const config = await loadConfig();
|
|
28
|
+
const SERVER_DIR = path.resolve(config.serverDir);
|
|
29
|
+
const PAGES_DIR = path.resolve("./app/pages");
|
|
30
|
+
const PUBLIC_DIR = path.resolve("./app/public");
|
|
31
|
+
const NODE_BUILTINS = [
|
|
32
|
+
"node:*",
|
|
33
|
+
"http",
|
|
34
|
+
"https",
|
|
35
|
+
"fs",
|
|
36
|
+
"path",
|
|
37
|
+
"url",
|
|
38
|
+
"crypto",
|
|
39
|
+
"stream",
|
|
40
|
+
"buffer",
|
|
41
|
+
"events",
|
|
42
|
+
"util",
|
|
43
|
+
"os",
|
|
44
|
+
"net",
|
|
45
|
+
"tls",
|
|
46
|
+
"child_process",
|
|
47
|
+
"worker_threads",
|
|
48
|
+
"cluster",
|
|
49
|
+
"dgram",
|
|
50
|
+
"dns",
|
|
51
|
+
"readline",
|
|
52
|
+
"zlib",
|
|
53
|
+
"assert",
|
|
54
|
+
"module",
|
|
55
|
+
"perf_hooks",
|
|
56
|
+
"string_decoder",
|
|
57
|
+
"timers",
|
|
58
|
+
"async_hooks",
|
|
59
|
+
"v8",
|
|
60
|
+
"vm"
|
|
61
|
+
];
|
|
62
|
+
const CJS_COMPAT_BANNER = {
|
|
63
|
+
js: `import { createRequire } from 'module';
|
|
64
|
+
const require = createRequire(import.meta.url);`
|
|
65
|
+
};
|
|
66
|
+
function emitVercelFunction(name, bundleText) {
|
|
67
|
+
const funcDir = path.join(FUNCTIONS_DIR, `${name}.func`);
|
|
68
|
+
fs.mkdirSync(funcDir, { recursive: true });
|
|
69
|
+
fs.writeFileSync(path.join(funcDir, "index.mjs"), bundleText);
|
|
70
|
+
fs.writeFileSync(
|
|
71
|
+
path.join(funcDir, ".vc-config.json"),
|
|
72
|
+
JSON.stringify({ runtime: "nodejs20.x", handler: "index.mjs", launcherType: "Nodejs" }, null, 2)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
function makeApiDispatcherSource(routes) {
|
|
76
|
+
const imports = routes.map((r, i) => `import * as __api_${i}__ from ${JSON.stringify(r.absPath)};`).join("\n");
|
|
77
|
+
const routeEntries = routes.map(
|
|
78
|
+
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, mod: __api_${i}__ },`
|
|
79
|
+
).join("\n");
|
|
80
|
+
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
81
|
+
${imports}
|
|
82
|
+
|
|
83
|
+
function enhance(res: ServerResponse) {
|
|
84
|
+
(res as any).json = function(data: any, status = 200) {
|
|
85
|
+
this.statusCode = status;
|
|
86
|
+
this.setHeader('Content-Type', 'application/json');
|
|
87
|
+
this.end(JSON.stringify(data));
|
|
88
|
+
};
|
|
89
|
+
(res as any).status = function(code: number) { this.statusCode = code; return this; };
|
|
90
|
+
return res;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function parseBody(req: IncomingMessage): Promise<any> {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
let body = '';
|
|
96
|
+
req.on('data', (chunk: any) => { body += chunk.toString(); });
|
|
97
|
+
req.on('end', () => {
|
|
98
|
+
try {
|
|
99
|
+
resolve(
|
|
100
|
+
body && req.headers['content-type']?.includes('application/json')
|
|
101
|
+
? JSON.parse(body)
|
|
102
|
+
: body,
|
|
103
|
+
);
|
|
104
|
+
} catch (e) { reject(e); }
|
|
105
|
+
});
|
|
106
|
+
req.on('error', reject);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ROUTES = [
|
|
111
|
+
${routeEntries}
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
115
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
116
|
+
const pathname = url.pathname;
|
|
117
|
+
|
|
118
|
+
for (const route of ROUTES) {
|
|
119
|
+
const m = pathname.match(new RegExp(route.regex));
|
|
120
|
+
if (!m) continue;
|
|
121
|
+
|
|
122
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
123
|
+
const apiRes = enhance(res);
|
|
124
|
+
const apiReq = req as any;
|
|
125
|
+
|
|
126
|
+
apiReq.body = await parseBody(req);
|
|
127
|
+
apiReq.query = Object.fromEntries(url.searchParams);
|
|
128
|
+
apiReq.params = {};
|
|
129
|
+
route.params.forEach((name: string, i: number) => { apiReq.params[name] = m[i + 1]; });
|
|
130
|
+
|
|
131
|
+
const fn = (route.mod as any)[method] ?? (route.mod as any)['default'];
|
|
132
|
+
if (typeof fn !== 'function') {
|
|
133
|
+
(apiRes as any).json({ error: \`Method \${method} not allowed\` }, 405);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
await fn(apiReq, apiRes);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
res.statusCode = 404;
|
|
141
|
+
res.setHeader('Content-Type', 'application/json');
|
|
142
|
+
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
143
|
+
}
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
function makePagesDispatcherSource(routes, errorAdapters = {}) {
|
|
147
|
+
const imports = routes.map((r, i) => `import __page_${i}__ from ${JSON.stringify(r.adapterPath)};`).join("\n");
|
|
148
|
+
const routeEntries = routes.map(
|
|
149
|
+
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, catchAll: ${JSON.stringify(r.catchAllNames)}, handler: __page_${i}__ },`
|
|
150
|
+
).join("\n");
|
|
151
|
+
const error404Import = errorAdapters.adapter404 ? `import __error_404__ from ${JSON.stringify(errorAdapters.adapter404)};` : "";
|
|
152
|
+
const error500Import = errorAdapters.adapter500 ? `import __error_500__ from ${JSON.stringify(errorAdapters.adapter500)};` : "";
|
|
153
|
+
const notFoundHandler = errorAdapters.adapter404 ? ` try { return await __error_404__(req, res); } catch(e) { console.error('[_404 error]', e); }` : ` res.statusCode = 404;
|
|
154
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
155
|
+
res.end('Not Found');`;
|
|
156
|
+
const clientErrorHandler = errorAdapters.adapter500 ? ` try {
|
|
157
|
+
const eq = new URLSearchParams();
|
|
158
|
+
eq.set('__errorMessage', url.searchParams.get('__clientError') || 'Client error');
|
|
159
|
+
const stack = url.searchParams.get('__clientStack');
|
|
160
|
+
if (stack) eq.set('__errorStack', stack);
|
|
161
|
+
req.url = '/_500?' + eq.toString();
|
|
162
|
+
return await __error_500__(req, res);
|
|
163
|
+
} catch(e) { console.error('[_500 client error]', e); }` : ` res.statusCode = 500;
|
|
164
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
165
|
+
res.end('Internal Server Error');`;
|
|
166
|
+
const errorHandler = errorAdapters.adapter500 ? ` try {
|
|
167
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
168
|
+
const errStack = err instanceof Error ? err.stack : undefined;
|
|
169
|
+
const errStatus = err?.status ?? err?.statusCode;
|
|
170
|
+
const eq = new URLSearchParams();
|
|
171
|
+
eq.set('__errorMessage', errMsg);
|
|
172
|
+
if (errStack) eq.set('__errorStack', errStack);
|
|
173
|
+
if (errStatus) eq.set('__errorStatus', String(errStatus));
|
|
174
|
+
req.url = '/_500?' + eq.toString();
|
|
175
|
+
return await __error_500__(req, res);
|
|
176
|
+
} catch(e) { console.error('[_500 error]', e); }` : ` res.statusCode = 500;
|
|
177
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
178
|
+
res.end('Internal Server Error');`;
|
|
179
|
+
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
180
|
+
${imports}
|
|
181
|
+
${error404Import}
|
|
182
|
+
${error500Import}
|
|
183
|
+
|
|
184
|
+
const ROUTES: Array<{
|
|
185
|
+
regex: string;
|
|
186
|
+
params: string[];
|
|
187
|
+
catchAll: string[];
|
|
188
|
+
handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
189
|
+
}> = [
|
|
190
|
+
${routeEntries}
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
194
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
195
|
+
const pathname = url.pathname;
|
|
196
|
+
|
|
197
|
+
// Client-side error \u2014 navigate directly to _500 handler.
|
|
198
|
+
if (url.searchParams.has('__clientError')) {
|
|
199
|
+
${clientErrorHandler}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const route of ROUTES) {
|
|
203
|
+
const m = pathname.match(new RegExp(route.regex));
|
|
204
|
+
if (!m) continue;
|
|
205
|
+
|
|
206
|
+
const catchAllSet = new Set(route.catchAll);
|
|
207
|
+
route.params.forEach((name, i) => {
|
|
208
|
+
const raw = m[i + 1] ?? '';
|
|
209
|
+
if (catchAllSet.has(name)) {
|
|
210
|
+
raw.split('/').filter(Boolean).forEach(seg => url.searchParams.append(name, seg));
|
|
211
|
+
} else {
|
|
212
|
+
url.searchParams.set(name, raw);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
req.url = pathname + (url.search || '');
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
return await route.handler(req, res);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error('[handler error]', err);
|
|
221
|
+
${errorHandler}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
${notFoundHandler}
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
const vercelRoutes = [];
|
|
231
|
+
const apiFiles = walkFiles(SERVER_DIR);
|
|
232
|
+
if (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);
|
|
233
|
+
const apiRoutes = apiFiles.map((relPath) => ({ ...analyzeFile(relPath, "api"), absPath: path.join(SERVER_DIR, relPath) })).sort((a, b) => b.specificity - a.specificity);
|
|
234
|
+
if (apiRoutes.length > 0) {
|
|
235
|
+
const dispatcherPath = path.join(SERVER_DIR, `_api_dispatcher_${randomBytes(4).toString("hex")}.ts`);
|
|
236
|
+
fs.writeFileSync(dispatcherPath, makeApiDispatcherSource(apiRoutes));
|
|
237
|
+
try {
|
|
238
|
+
const result = await build({
|
|
239
|
+
entryPoints: [dispatcherPath],
|
|
240
|
+
bundle: true,
|
|
241
|
+
format: "esm",
|
|
242
|
+
platform: "node",
|
|
243
|
+
target: "node20",
|
|
244
|
+
banner: CJS_COMPAT_BANNER,
|
|
245
|
+
external: NODE_BUILTINS,
|
|
246
|
+
write: false
|
|
247
|
+
});
|
|
248
|
+
emitVercelFunction("api", result.outputFiles[0].text);
|
|
249
|
+
console.log(` built API dispatcher \u2192 api.func (${apiRoutes.length} route(s))`);
|
|
250
|
+
} finally {
|
|
251
|
+
fs.unlinkSync(dispatcherPath);
|
|
252
|
+
}
|
|
253
|
+
for (const { srcRegex } of apiRoutes)
|
|
254
|
+
vercelRoutes.push({ src: srcRegex, dest: "/api" });
|
|
255
|
+
}
|
|
256
|
+
const serverPages = collectServerPages(PAGES_DIR);
|
|
257
|
+
const hasErrorPages = ["_404.tsx", "_500.tsx"].some((f) => fs.existsSync(path.join(PAGES_DIR, f)));
|
|
258
|
+
if (serverPages.length > 0 || hasErrorPages) {
|
|
259
|
+
const globalRegistry = collectGlobalClientRegistry(serverPages, PAGES_DIR);
|
|
260
|
+
const prerenderedHtml = await bundleClientComponents(globalRegistry, PAGES_DIR, STATIC_DIR);
|
|
261
|
+
const prerenderedRecord = Object.fromEntries(prerenderedHtml);
|
|
262
|
+
const tempAdapterPaths = [];
|
|
263
|
+
for (const page of serverPages) {
|
|
264
|
+
const adapterDir = path.dirname(page.absPath);
|
|
265
|
+
const adapterPath = path.join(adapterDir, `_page_adapter_${randomBytes(4).toString("hex")}.ts`);
|
|
266
|
+
const layoutPaths = findPageLayouts(page.absPath, PAGES_DIR);
|
|
267
|
+
const { registry, clientComponentNames } = buildPerPageRegistry(page.absPath, layoutPaths, PAGES_DIR);
|
|
268
|
+
const layoutImports = layoutPaths.map((lp, i) => {
|
|
269
|
+
const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
|
|
270
|
+
return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
|
|
271
|
+
}).join("\n");
|
|
272
|
+
fs.writeFileSync(
|
|
273
|
+
adapterPath,
|
|
274
|
+
makePageAdapterSource({
|
|
275
|
+
pageImport: JSON.stringify("./" + path.basename(page.absPath)),
|
|
276
|
+
layoutImports,
|
|
277
|
+
clientComponentNames,
|
|
278
|
+
allClientIds: [...registry.keys()],
|
|
279
|
+
layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
|
|
280
|
+
prerenderedHtml: prerenderedRecord,
|
|
281
|
+
routeParamNames: page.paramNames,
|
|
282
|
+
catchAllNames: page.catchAllNames
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
tempAdapterPaths.push(adapterPath);
|
|
286
|
+
console.log(` prepared ${path.relative(PAGES_DIR, page.absPath)} \u2192 ${page.funcPath} [page]`);
|
|
287
|
+
}
|
|
288
|
+
const dispatcherRoutes = serverPages.map((page, i) => ({
|
|
289
|
+
adapterPath: tempAdapterPaths[i],
|
|
290
|
+
srcRegex: page.srcRegex,
|
|
291
|
+
paramNames: page.paramNames,
|
|
292
|
+
catchAllNames: page.catchAllNames
|
|
293
|
+
}));
|
|
294
|
+
const errorAdapters = {};
|
|
295
|
+
const errorAdapterPaths = [];
|
|
296
|
+
for (const [statusCode, key] of [[404, "adapter404"], [500, "adapter500"]]) {
|
|
297
|
+
const src = path.join(PAGES_DIR, `_${statusCode}.tsx`);
|
|
298
|
+
if (!fs.existsSync(src)) continue;
|
|
299
|
+
console.log(` building _${statusCode}.tsx \u2192 pages.func [error page]`);
|
|
300
|
+
const adapterDir = path.dirname(src);
|
|
301
|
+
const adapterPath = path.join(adapterDir, `_error_adapter_${randomBytes(4).toString("hex")}.ts`);
|
|
302
|
+
const layoutPaths = findPageLayouts(src, PAGES_DIR);
|
|
303
|
+
const { registry, clientComponentNames } = buildPerPageRegistry(src, layoutPaths, PAGES_DIR);
|
|
304
|
+
const layoutImports = layoutPaths.map((lp, i) => {
|
|
305
|
+
const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
|
|
306
|
+
return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
|
|
307
|
+
}).join("\n");
|
|
308
|
+
fs.writeFileSync(adapterPath, makePageAdapterSource({
|
|
309
|
+
pageImport: JSON.stringify("./" + path.basename(src)),
|
|
310
|
+
layoutImports,
|
|
311
|
+
clientComponentNames,
|
|
312
|
+
allClientIds: [...registry.keys()],
|
|
313
|
+
layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
|
|
314
|
+
prerenderedHtml: prerenderedRecord,
|
|
315
|
+
routeParamNames: [],
|
|
316
|
+
catchAllNames: [],
|
|
317
|
+
statusCode
|
|
318
|
+
}));
|
|
319
|
+
errorAdapters[key] = adapterPath;
|
|
320
|
+
errorAdapterPaths.push(adapterPath);
|
|
321
|
+
}
|
|
322
|
+
const dispatcherPath = path.join(PAGES_DIR, `_pages_dispatcher_${randomBytes(4).toString("hex")}.ts`);
|
|
323
|
+
fs.writeFileSync(dispatcherPath, makePagesDispatcherSource(dispatcherRoutes, errorAdapters));
|
|
324
|
+
try {
|
|
325
|
+
const result = await build({
|
|
326
|
+
entryPoints: [dispatcherPath],
|
|
327
|
+
bundle: true,
|
|
328
|
+
format: "esm",
|
|
329
|
+
platform: "node",
|
|
330
|
+
target: "node20",
|
|
331
|
+
jsx: "automatic",
|
|
332
|
+
banner: CJS_COMPAT_BANNER,
|
|
333
|
+
external: NODE_BUILTINS,
|
|
334
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
335
|
+
write: false
|
|
336
|
+
});
|
|
337
|
+
emitVercelFunction("pages", result.outputFiles[0].text);
|
|
338
|
+
console.log(` built Pages dispatcher \u2192 pages.func (${serverPages.length} page(s))`);
|
|
339
|
+
} finally {
|
|
340
|
+
fs.unlinkSync(dispatcherPath);
|
|
341
|
+
for (const p of tempAdapterPaths) if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
342
|
+
for (const p of errorAdapterPaths) if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
343
|
+
}
|
|
344
|
+
for (const { srcRegex } of serverPages)
|
|
345
|
+
vercelRoutes.push({ src: srcRegex, dest: "/pages" });
|
|
346
|
+
}
|
|
347
|
+
fs.writeFileSync(
|
|
348
|
+
path.join(OUTPUT_DIR, "config.json"),
|
|
349
|
+
JSON.stringify({ version: 3, routes: [{ handle: "filesystem" }, ...vercelRoutes] }, null, 2)
|
|
350
|
+
);
|
|
351
|
+
fs.writeFileSync(
|
|
352
|
+
path.resolve("vercel.json"),
|
|
353
|
+
JSON.stringify({ runtime: "nodejs20.x" }, null, 2)
|
|
354
|
+
);
|
|
355
|
+
await buildCombinedBundle(STATIC_DIR);
|
|
356
|
+
copyPublicFiles(PUBLIC_DIR, STATIC_DIR);
|
|
357
|
+
const fnCount = (apiRoutes.length > 0 ? 1 : 0) + (serverPages.length > 0 ? 1 : 0);
|
|
358
|
+
console.log(`
|
|
359
|
+
\u2713 Vercel build complete \u2014 ${fnCount} function(s) \u2192 .vercel/output`);
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const srcDir = path.resolve(__dirname, "");
|
|
8
|
+
const outDir = path.resolve(__dirname, "../dist");
|
|
9
|
+
function cleanDist(dir) {
|
|
10
|
+
if (!fs.existsSync(dir)) return;
|
|
11
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
12
|
+
console.log(`\u{1F5D1}\uFE0F Cleared ${dir}`);
|
|
13
|
+
}
|
|
14
|
+
function collectFiles(dir, exclude = []) {
|
|
15
|
+
const files = [];
|
|
16
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
17
|
+
const full = path.join(dir, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
if (!exclude.includes(entry.name)) files.push(...collectFiles(full, exclude));
|
|
20
|
+
} else if (/\.[tj]sx?$/.test(entry.name)) {
|
|
21
|
+
files.push(full);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return files;
|
|
25
|
+
}
|
|
26
|
+
function processDist(dir) {
|
|
27
|
+
(function walk(currentDir) {
|
|
28
|
+
fs.readdirSync(currentDir, { withFileTypes: true }).forEach((d) => {
|
|
29
|
+
const fullPath = path.join(currentDir, d.name);
|
|
30
|
+
if (d.isDirectory()) {
|
|
31
|
+
walk(fullPath);
|
|
32
|
+
} else if (fullPath.endsWith(".js")) {
|
|
33
|
+
let content = fs.readFileSync(fullPath, "utf-8");
|
|
34
|
+
content = content.replace(/from\s+['"](\.\/.*?)['"]/g, 'from "$1.js"');
|
|
35
|
+
content = content.replace(/import\(['"](\.\/.*?)['"]\)/g, 'import("$1.js")');
|
|
36
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
})(dir);
|
|
40
|
+
console.log("\u{1F527} Post-processing done: relative imports \u2192 .js extensions.");
|
|
41
|
+
}
|
|
42
|
+
const PUBLIC_STEMS = /* @__PURE__ */ new Set([
|
|
43
|
+
"index",
|
|
44
|
+
"html-store",
|
|
45
|
+
// exported types (TitleValue, LinkTag, MetaTag, …) live here
|
|
46
|
+
"use-html",
|
|
47
|
+
"use-router",
|
|
48
|
+
"use-request",
|
|
49
|
+
"request-store",
|
|
50
|
+
"Link",
|
|
51
|
+
"bundle",
|
|
52
|
+
"store",
|
|
53
|
+
"utils",
|
|
54
|
+
"logger"
|
|
55
|
+
]);
|
|
56
|
+
function prunePrivateDeclarations(dir) {
|
|
57
|
+
(function walk(currentDir) {
|
|
58
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
59
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
walk(fullPath);
|
|
62
|
+
} else if (entry.name.endsWith(".d.ts") || entry.name.endsWith(".d.ts.map")) {
|
|
63
|
+
const stem = entry.name.replace(/\.d\.ts(\.map)?$/, "");
|
|
64
|
+
if (!PUBLIC_STEMS.has(stem)) {
|
|
65
|
+
fs.rmSync(fullPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
})(dir);
|
|
70
|
+
console.log("\u2702\uFE0F Pruned private .d.ts files (kept public API only).");
|
|
71
|
+
}
|
|
72
|
+
async function runBuild() {
|
|
73
|
+
try {
|
|
74
|
+
cleanDist(outDir);
|
|
75
|
+
console.log("\u{1F680} Building sources\u2026");
|
|
76
|
+
await build({
|
|
77
|
+
entryPoints: collectFiles(srcDir),
|
|
78
|
+
outdir: outDir,
|
|
79
|
+
platform: "node",
|
|
80
|
+
format: "esm",
|
|
81
|
+
target: ["node20"],
|
|
82
|
+
packages: "external"
|
|
83
|
+
});
|
|
84
|
+
console.log("\u2705 Build done.");
|
|
85
|
+
processDist(outDir);
|
|
86
|
+
console.log("\u{1F4C4} Generating TypeScript declarations\u2026");
|
|
87
|
+
execSync("tsc --emitDeclarationOnly --declaration --declarationMap false --outDir dist", { stdio: "inherit" });
|
|
88
|
+
prunePrivateDeclarations(outDir);
|
|
89
|
+
console.log("\n\u{1F389} Build complete \u2192 dist/");
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error("\u274C Build failed:", err);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
runBuild();
|