chocola 2.0.0-next.11 → 2.0.0-next.12
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 +17 -10
- package/compiler/component-processor.js +4 -11
- package/compiler/config.js +40 -52
- package/compiler/index.js +61 -134
- package/compiler/module-graph.js +0 -9
- package/compiler/utils.js +102 -116
- package/package.json +56 -57
- package/utils.js +39 -122
- package/bin/chocola.js +0 -227
- package/dev/index.js +0 -210
- package/parser/script.test.js +0 -103
- package/server/index.js +0 -432
package/server/index.js
DELETED
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
import http from "http";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { promises as fs } from "fs";
|
|
4
|
-
import { createHash } from "crypto";
|
|
5
|
-
import zlib from "zlib";
|
|
6
|
-
import { pathToFileURL } from "url";
|
|
7
|
-
|
|
8
|
-
import chalk from "../compiler/chalk.js";
|
|
9
|
-
import { buildModuleGraph } from "../compiler/module-graph.js";
|
|
10
|
-
import { renderPage } from "../compiler/render.js";
|
|
11
|
-
import { loadConfig, resolvePaths } from "../compiler/config.js";
|
|
12
|
-
import { getConfig, isMissingConfigFile, queueConfigWarning, flushConfigWarnings } from "../utils.js";
|
|
13
|
-
|
|
14
|
-
const warnedServerPort = new Set();
|
|
15
|
-
const warnedServerHostname = new Set();
|
|
16
|
-
|
|
17
|
-
const MIME_TYPES = {
|
|
18
|
-
".html": "text/html; charset=utf-8",
|
|
19
|
-
".htm": "text/html; charset=utf-8",
|
|
20
|
-
".js": "text/javascript; charset=utf-8",
|
|
21
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
22
|
-
".css": "text/css; charset=utf-8",
|
|
23
|
-
".csv": "text/csv",
|
|
24
|
-
".json": "application/json",
|
|
25
|
-
".png": "image/png",
|
|
26
|
-
".jpg": "image/jpeg",
|
|
27
|
-
".jpeg": "image/jpeg",
|
|
28
|
-
".bmp": "image/bmp",
|
|
29
|
-
".gif": "image/gif",
|
|
30
|
-
".svg": "image/svg+xml",
|
|
31
|
-
".ico": "image/vnd.microsoft.icon",
|
|
32
|
-
".icon": "image/vnd.microsoft.icon",
|
|
33
|
-
".pdf": "application/pdf",
|
|
34
|
-
".mp3": "audio/mpeg",
|
|
35
|
-
".md": "text/markdown",
|
|
36
|
-
".mp4": "video/mp4",
|
|
37
|
-
".mpeg": "video/mpeg",
|
|
38
|
-
".oga": "audio/ogg",
|
|
39
|
-
".ogv": "video/ogg",
|
|
40
|
-
".tar": "application/x-tar",
|
|
41
|
-
".ttf": "font/ttf",
|
|
42
|
-
".txt": "text/plain",
|
|
43
|
-
".wav": "audio/wav",
|
|
44
|
-
".weba": "audio/webm",
|
|
45
|
-
".webm": "video/webm",
|
|
46
|
-
".webmanifest": "application/manifest+json",
|
|
47
|
-
".webp": "image/webp",
|
|
48
|
-
".xhtml": "application/xhtml+xml",
|
|
49
|
-
".xml": "application/xml",
|
|
50
|
-
".zip": "application/zip",
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
function getMimeType(filePath) {
|
|
54
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
55
|
-
return MIME_TYPES[ext] || "application/octet-stream";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function generateETag(buf) {
|
|
59
|
-
const hash = createHash("sha256").update(buf).digest("hex").slice(0, 32);
|
|
60
|
-
return `W/"${hash}"`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function parseCookies(cookieHeader = "") {
|
|
64
|
-
const out = {};
|
|
65
|
-
if (!cookieHeader) return out;
|
|
66
|
-
for (const part of cookieHeader.split(";")) {
|
|
67
|
-
const idx = part.indexOf("=");
|
|
68
|
-
if (idx === -1) continue;
|
|
69
|
-
const key = part.slice(0, idx).trim();
|
|
70
|
-
const val = part.slice(idx + 1).trim();
|
|
71
|
-
if (key) out[key] = decodeURIComponent(val);
|
|
72
|
-
}
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function acceptsGzip(req) {
|
|
77
|
-
const ae = req.headers["accept-encoding"] || "";
|
|
78
|
-
return ae.includes("gzip");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function shouldCompress(mime) {
|
|
82
|
-
return /^(text\/|application\/(javascript|json|xml|manifest\+json))/.test(mime);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function loadMiddleware(rootDir, middlewarePath) {
|
|
86
|
-
if (!middlewarePath) return [];
|
|
87
|
-
const resolved = path.isAbsolute(middlewarePath)
|
|
88
|
-
? middlewarePath
|
|
89
|
-
: path.join(rootDir, middlewarePath);
|
|
90
|
-
try {
|
|
91
|
-
await fs.access(resolved);
|
|
92
|
-
} catch {
|
|
93
|
-
throw new Error(`[chocola/server] middleware file not found: ${resolved}`);
|
|
94
|
-
}
|
|
95
|
-
const imported = await import(pathToFileURL(resolved).href);
|
|
96
|
-
const raw = imported.default ?? imported;
|
|
97
|
-
if (Array.isArray(raw)) return raw.filter((fn) => typeof fn === "function");
|
|
98
|
-
if (typeof raw === "function") return [raw];
|
|
99
|
-
if (raw && typeof raw === "object") {
|
|
100
|
-
// handle named exports
|
|
101
|
-
const fns = Object.values(raw).filter((v) => typeof v === "function");
|
|
102
|
-
if (fns.length) return fns;
|
|
103
|
-
}
|
|
104
|
-
return [];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function buildRouteTable(graph) {
|
|
108
|
-
const table = new Map();
|
|
109
|
-
// Single page support: index.html
|
|
110
|
-
if (graph.page) {
|
|
111
|
-
table.set("/", graph.page);
|
|
112
|
-
table.set("/index.html", graph.page);
|
|
113
|
-
table.set("/index", graph.page);
|
|
114
|
-
}
|
|
115
|
-
return table;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function sendNotModified(res) {
|
|
119
|
-
res.writeHead(304);
|
|
120
|
-
res.end();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function serveBuffer(req, res, buffer, contentType, etag, mtime) {
|
|
124
|
-
const headers = {
|
|
125
|
-
"Content-Type": contentType,
|
|
126
|
-
"ETag": etag,
|
|
127
|
-
"Last-Modified": mtime.toUTCString(),
|
|
128
|
-
"Cache-Control": "public, max-age=0, must-revalidate",
|
|
129
|
-
"Content-Length": buffer.length,
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const inm = req.headers["if-none-match"];
|
|
133
|
-
if (inm && inm === etag) {
|
|
134
|
-
// Weak comparison is okay for demo; exact match suffices
|
|
135
|
-
sendNotModified(res);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const ims = req.headers["if-modified-since"];
|
|
140
|
-
if (ims) {
|
|
141
|
-
const imsTime = new Date(ims).getTime();
|
|
142
|
-
if (!Number.isNaN(imsTime) && mtime.getTime() <= imsTime) {
|
|
143
|
-
sendNotModified(res);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const doGzip = acceptsGzip(req) && shouldCompress(contentType);
|
|
149
|
-
if (doGzip) {
|
|
150
|
-
const gz = zlib.gzipSync(buffer);
|
|
151
|
-
headers["Content-Encoding"] = "gzip";
|
|
152
|
-
headers["Vary"] = "Accept-Encoding";
|
|
153
|
-
headers["Content-Length"] = gz.length;
|
|
154
|
-
if (req.method === "HEAD") {
|
|
155
|
-
res.writeHead(200, headers);
|
|
156
|
-
res.end();
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
res.writeHead(200, headers);
|
|
160
|
-
res.end(gz);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (req.method === "HEAD") {
|
|
165
|
-
res.writeHead(200, headers);
|
|
166
|
-
res.end();
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
res.writeHead(200, headers);
|
|
170
|
-
res.end(buffer);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function normalizeRootDir(arg, opts) {
|
|
174
|
-
if (typeof arg === "object" && arg !== null && !opts) {
|
|
175
|
-
// createServer({ rootDir }) form
|
|
176
|
-
const o = arg;
|
|
177
|
-
const rd = o.rootDir || o.root || o.dir || process.cwd();
|
|
178
|
-
return { rootDir: rd, opts: o };
|
|
179
|
-
}
|
|
180
|
-
if (typeof arg === "string") {
|
|
181
|
-
return { rootDir: arg, opts: opts || {} };
|
|
182
|
-
}
|
|
183
|
-
// fallback
|
|
184
|
-
return { rootDir: String(arg), opts: opts || {} };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export async function createHandler(rootDirArg, optsArg) {
|
|
188
|
-
const { rootDir, opts } = normalizeRootDir(rootDirArg, optsArg);
|
|
189
|
-
const silent = opts.silent ?? false;
|
|
190
|
-
|
|
191
|
-
const fullConfig = await getConfig(rootDir, { silent });
|
|
192
|
-
const config = await loadConfig(rootDir, { silent });
|
|
193
|
-
const paths = resolvePaths(rootDir, config);
|
|
194
|
-
|
|
195
|
-
const graph = await buildModuleGraph(rootDir);
|
|
196
|
-
|
|
197
|
-
const middlewarePath = opts.middleware ?? fullConfig.server?.middleware ?? null;
|
|
198
|
-
const middlewares = await loadMiddleware(rootDir, middlewarePath);
|
|
199
|
-
|
|
200
|
-
const routeTable = buildRouteTable(graph);
|
|
201
|
-
|
|
202
|
-
// virtual file store for dynamically generated assets (run-*.js, sc-*.css, css-*.css, js-*.js)
|
|
203
|
-
const virtualFiles = new Map(); // pathname -> { buffer, etag, mtime, type }
|
|
204
|
-
|
|
205
|
-
function ingest(result) {
|
|
206
|
-
for (const file of result.files) {
|
|
207
|
-
const key = "/" + file.path;
|
|
208
|
-
const buf = Buffer.from(file.content, "utf-8");
|
|
209
|
-
const etag = generateETag(buf);
|
|
210
|
-
virtualFiles.set(key, {
|
|
211
|
-
buffer: buf,
|
|
212
|
-
etag,
|
|
213
|
-
mtime: new Date(),
|
|
214
|
-
type: getMimeType(file.path),
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Prime virtual files with initial render (ensures assets exist even before first SSR)
|
|
220
|
-
try {
|
|
221
|
-
const initial = await renderPage(graph, {});
|
|
222
|
-
ingest(initial);
|
|
223
|
-
} catch (e) {
|
|
224
|
-
// If initial render fails, continue; per-request render will surface error
|
|
225
|
-
console.warn("[chocola/server] initial render failed:", e?.message || e);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function handler(req, res) {
|
|
229
|
-
try {
|
|
230
|
-
const host = req.headers.host || "localhost";
|
|
231
|
-
const url = new URL(req.url, `http://${host}`);
|
|
232
|
-
let pathname = decodeURIComponent(url.pathname);
|
|
233
|
-
|
|
234
|
-
// Normalize pathname: remove duplicate slashes
|
|
235
|
-
pathname = pathname.replace(/\/+/g, "/");
|
|
236
|
-
// Keep as is; do not strip trailing slash except for route lookup
|
|
237
|
-
|
|
238
|
-
const query = Object.fromEntries(url.searchParams.entries());
|
|
239
|
-
const cookies = parseCookies(req.headers.cookie || "");
|
|
240
|
-
|
|
241
|
-
// 1) SSR route?
|
|
242
|
-
// Try exact, then fallback with/without trailing slash
|
|
243
|
-
let page = routeTable.get(pathname);
|
|
244
|
-
if (!page && pathname.endsWith("/") && pathname.length > 1) {
|
|
245
|
-
page = routeTable.get(pathname.slice(0, -1));
|
|
246
|
-
}
|
|
247
|
-
if (!page && !pathname.endsWith("/")) {
|
|
248
|
-
page = routeTable.get(pathname + "/");
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (page) {
|
|
252
|
-
// Build per-request ctx via middleware
|
|
253
|
-
let ctx = { ...query };
|
|
254
|
-
// Also merge opts.ctx if provided (for programmatic use)
|
|
255
|
-
if (opts.ctx && typeof opts.ctx === "object") Object.assign(ctx, opts.ctx);
|
|
256
|
-
|
|
257
|
-
for (const mw of middlewares) {
|
|
258
|
-
let result;
|
|
259
|
-
try {
|
|
260
|
-
if (mw.length >= 2) {
|
|
261
|
-
result = await mw(req, res);
|
|
262
|
-
} else {
|
|
263
|
-
result = await mw({
|
|
264
|
-
req,
|
|
265
|
-
res,
|
|
266
|
-
params: {},
|
|
267
|
-
query,
|
|
268
|
-
cookies,
|
|
269
|
-
headers: req.headers,
|
|
270
|
-
url,
|
|
271
|
-
pathname,
|
|
272
|
-
searchParams: url.searchParams,
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
} catch (e) {
|
|
276
|
-
console.error("[chocola/server] middleware error:", e);
|
|
277
|
-
if (!res.headersSent && !res.writableEnded) {
|
|
278
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
279
|
-
res.end("Internal middleware error");
|
|
280
|
-
}
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
if (res.writableEnded || res.headersSent) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
if (result && typeof result === "object" && !Array.isArray(result) && !(result instanceof Buffer)) {
|
|
287
|
-
// Merge returned object into ctx
|
|
288
|
-
Object.assign(ctx, result);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
let result;
|
|
293
|
-
try {
|
|
294
|
-
result = await renderPage(graph, ctx);
|
|
295
|
-
} catch (e) {
|
|
296
|
-
if (!res.headersSent && !res.writableEnded) {
|
|
297
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
298
|
-
res.end("Render error: " + (e?.message || e));
|
|
299
|
-
}
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
ingest(result);
|
|
304
|
-
|
|
305
|
-
const htmlBuf = Buffer.from(result.html, "utf-8");
|
|
306
|
-
const etag = generateETag(htmlBuf);
|
|
307
|
-
const mtime = new Date();
|
|
308
|
-
const ctype = "text/html; charset=utf-8";
|
|
309
|
-
serveBuffer(req, res, htmlBuf, ctype, etag, mtime);
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// 2) Virtual files (bundled css, js, runtime, scoped css)
|
|
314
|
-
if (virtualFiles.has(pathname)) {
|
|
315
|
-
const entry = virtualFiles.get(pathname);
|
|
316
|
-
serveBuffer(req, res, entry.buffer, entry.type, entry.etag, entry.mtime);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Also handle "./" prefix that html emits: href="./css-xxx.css" leads to request "/css-xxx.css" after browser resolves, but also handle "./" stripping
|
|
321
|
-
// Already handled by virtualFiles via "/" prefix. For safety, try without leading slash variations
|
|
322
|
-
// Next: try static file from src
|
|
323
|
-
// Remove leading "/" and resolve against src dir
|
|
324
|
-
const relative = pathname.replace(/^\/+/, "");
|
|
325
|
-
// Prevent directory traversal
|
|
326
|
-
const safe = path.normalize(relative).replace(/^(\.\.(\/|\\|$))+/g, "");
|
|
327
|
-
const candidatePaths = [
|
|
328
|
-
path.join(paths.src, safe),
|
|
329
|
-
path.join(paths.src, safe + ".html"),
|
|
330
|
-
];
|
|
331
|
-
|
|
332
|
-
for (const cand of candidatePaths) {
|
|
333
|
-
try {
|
|
334
|
-
const stat = await fs.stat(cand);
|
|
335
|
-
if (stat.isFile()) {
|
|
336
|
-
const buf = await fs.readFile(cand);
|
|
337
|
-
const etag = generateETag(buf);
|
|
338
|
-
const mtime = stat.mtime;
|
|
339
|
-
const ctype = getMimeType(cand);
|
|
340
|
-
serveBuffer(req, res, buf, ctype, etag, mtime);
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (stat.isDirectory()) {
|
|
344
|
-
// try index.html inside
|
|
345
|
-
const idx = path.join(cand, "index.html");
|
|
346
|
-
try {
|
|
347
|
-
const s2 = await fs.stat(idx);
|
|
348
|
-
if (s2.isFile()) {
|
|
349
|
-
const buf = await fs.readFile(idx);
|
|
350
|
-
const etag = generateETag(buf);
|
|
351
|
-
serveBuffer(req, res, buf, getMimeType(idx), etag, s2.mtime);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
} catch {}
|
|
355
|
-
}
|
|
356
|
-
} catch {}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Fallback 404
|
|
360
|
-
if (!res.headersSent && !res.writableEnded) {
|
|
361
|
-
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
362
|
-
res.end("<h1>404 Not Found</h1><p>The requested resource was not found.</p>");
|
|
363
|
-
}
|
|
364
|
-
} catch (err) {
|
|
365
|
-
console.error("[chocola/server] handler error:", err);
|
|
366
|
-
if (!res.headersSent && !res.writableEnded) {
|
|
367
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
368
|
-
res.end("Internal server error");
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Flush config warnings before returning handler so they appear before any subsequent logs
|
|
374
|
-
flushConfigWarnings(rootDir);
|
|
375
|
-
|
|
376
|
-
// Attach helpers for testing / introspection
|
|
377
|
-
handler.graph = graph;
|
|
378
|
-
handler.virtualFiles = virtualFiles;
|
|
379
|
-
handler.routeTable = buildRouteTable(graph);
|
|
380
|
-
handler.paths = paths;
|
|
381
|
-
|
|
382
|
-
return handler;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Alias per lead: createServer({ rootDir }) returns bare http handler
|
|
386
|
-
export const createServer = createHandler;
|
|
387
|
-
export const createServerRenderer = createHandler; // future ssr escape hatch alias
|
|
388
|
-
|
|
389
|
-
export async function serve(rootDirArg, optsArg) {
|
|
390
|
-
const { rootDir } = normalizeRootDir(rootDirArg, optsArg);
|
|
391
|
-
const silent = optsArg?.silent ?? false;
|
|
392
|
-
const fullConfig = await getConfig(rootDir, { silent });
|
|
393
|
-
const serverCfg = fullConfig.server || {};
|
|
394
|
-
const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : null;
|
|
395
|
-
const hasEnvPort = Number.isFinite(envPort) && envPort >= 1 && envPort <= 65535;
|
|
396
|
-
const port = optsArg?.port ?? (hasEnvPort ? envPort : (serverCfg.port ?? 8080));
|
|
397
|
-
const hasHostFlag = optsArg?.hostname != null || optsArg?.host != null;
|
|
398
|
-
const hasHostConfig = serverCfg.hostname != null || serverCfg.host != null;
|
|
399
|
-
const hostname = hasHostFlag ? (optsArg?.hostname ?? optsArg?.host) : (hasEnvPort && !hasHostConfig ? "0.0.0.0" : (serverCfg.hostname ?? serverCfg.host ?? "localhost"));
|
|
400
|
-
|
|
401
|
-
if (!silent) {
|
|
402
|
-
if (isMissingConfigFile(fullConfig)) {
|
|
403
|
-
// top-level already warned
|
|
404
|
-
} else if (fullConfig.server != null) {
|
|
405
|
-
if (serverCfg.port == null && optsArg?.port == null && !warnedServerPort.has(rootDir)) {
|
|
406
|
-
warnedServerPort.add(rootDir);
|
|
407
|
-
queueConfigWarning(rootDir, chalk.bold.yellow("WARNING!"), `server.port not defined in chocola.config.json file: using default ${port} server.port.`);
|
|
408
|
-
}
|
|
409
|
-
if ((serverCfg.hostname == null && serverCfg.host == null) && (optsArg?.hostname == null && optsArg?.host == null) && !warnedServerHostname.has(rootDir)) {
|
|
410
|
-
warnedServerHostname.add(rootDir);
|
|
411
|
-
queueConfigWarning(rootDir, chalk.bold.yellow("WARNING!"), `server.hostname not defined in chocola.config.json file: using default ${hostname} server.hostname.`);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const handler = await createHandler(rootDir, optsArg);
|
|
417
|
-
|
|
418
|
-
flushConfigWarnings(rootDir);
|
|
419
|
-
|
|
420
|
-
const server = http.createServer(handler);
|
|
421
|
-
await new Promise((resolve, reject) => {
|
|
422
|
-
server.once("error", reject);
|
|
423
|
-
server.listen(port, hostname, () => {
|
|
424
|
-
console.log(`Chocola server running at http://${hostname}:${port}/`);
|
|
425
|
-
resolve();
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
return server;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// also export default for convenience
|
|
432
|
-
export default { createHandler, createServer, serve };
|