elegance-js 2.1.37 → 3.0.1

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.
Files changed (138) hide show
  1. package/README.md +90 -4
  2. package/bin/bootstrap.js +18 -0
  3. package/bin/run.js +2 -0
  4. package/dist/build/common.d.ts +147 -0
  5. package/dist/build/common.d.ts.map +1 -0
  6. package/dist/build/common.js +599 -0
  7. package/dist/build/dev.d.ts +2 -0
  8. package/dist/build/dev.d.ts.map +1 -0
  9. package/dist/build/dev.js +234 -0
  10. package/dist/build/prod.d.ts +2 -0
  11. package/dist/build/prod.d.ts.map +1 -0
  12. package/dist/build/prod.js +212 -0
  13. package/dist/build/render.d.ts +29 -0
  14. package/dist/build/render.d.ts.map +1 -0
  15. package/dist/build/render.js +234 -0
  16. package/dist/client.d.ts +13 -0
  17. package/dist/client.d.ts.map +1 -0
  18. package/dist/client.js +677 -0
  19. package/dist/config.d.ts +7 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +80 -0
  22. package/dist/constants.d.ts +10 -0
  23. package/dist/constants.d.ts.map +1 -0
  24. package/dist/constants.js +20 -0
  25. package/dist/elements.d.ts +2 -0
  26. package/dist/elements.d.ts.map +1 -0
  27. package/dist/elements.js +14 -0
  28. package/dist/error.d.ts +20 -0
  29. package/dist/error.d.ts.map +1 -0
  30. package/dist/error.js +123 -0
  31. package/dist/globals.d.ts +6 -0
  32. package/dist/globals.d.ts.map +1 -0
  33. package/dist/globals.js +106 -0
  34. package/dist/logger.d.ts +32 -0
  35. package/dist/logger.d.ts.map +1 -0
  36. package/dist/logger.js +72 -0
  37. package/dist/page-tools.d.ts +19 -0
  38. package/dist/page-tools.d.ts.map +1 -0
  39. package/dist/page-tools.js +141 -0
  40. package/dist/processing/oxc.d.ts +17 -0
  41. package/dist/processing/oxc.d.ts.map +1 -0
  42. package/dist/processing/oxc.js +938 -0
  43. package/dist/processing/taglist.d.ts +2 -0
  44. package/dist/processing/taglist.d.ts.map +1 -0
  45. package/dist/processing/taglist.js +215 -0
  46. package/dist/processing/tsx.d.ts +2 -0
  47. package/dist/processing/tsx.d.ts.map +1 -0
  48. package/dist/processing/tsx.js +131 -0
  49. package/dist/run.d.ts +3 -0
  50. package/dist/run.d.ts.map +1 -0
  51. package/dist/run.js +147 -0
  52. package/dist/server/dev.d.ts +2 -0
  53. package/dist/server/dev.d.ts.map +1 -0
  54. package/dist/server/dev.js +10 -0
  55. package/dist/server/prod.d.ts +2 -0
  56. package/dist/server/prod.d.ts.map +1 -0
  57. package/dist/server/prod.js +42 -0
  58. package/dist/server/security.d.ts +64 -0
  59. package/dist/server/security.d.ts.map +1 -0
  60. package/dist/server/security.js +120 -0
  61. package/dist/server/server.d.ts +73 -99
  62. package/dist/server/server.d.ts.map +1 -0
  63. package/dist/server/server.js +830 -680
  64. package/dist/types/component.d.ts +85 -0
  65. package/dist/types/component.d.ts.map +1 -0
  66. package/dist/types/component.js +0 -0
  67. package/dist/types/config.d.ts +12 -0
  68. package/dist/types/config.d.ts.map +1 -0
  69. package/dist/types/config.js +0 -0
  70. package/dist/types/elements.d.ts +412 -0
  71. package/dist/types/elements.d.ts.map +1 -0
  72. package/dist/types/elements.js +0 -0
  73. package/dist/types/index.d.ts +9 -0
  74. package/dist/types/index.d.ts.map +1 -0
  75. package/dist/types/index.js +5 -0
  76. package/dist/types/jsx.d.ts +976 -0
  77. package/dist/types/jsx.d.ts.map +1 -0
  78. package/dist/types/jsx.js +0 -0
  79. package/dist/types/server-actions.d.ts +60 -0
  80. package/dist/types/server-actions.d.ts.map +1 -0
  81. package/dist/types/server-actions.js +0 -0
  82. package/dist/user-utils.d.ts +23 -0
  83. package/dist/user-utils.d.ts.map +1 -0
  84. package/dist/user-utils.js +61 -0
  85. package/package.json +48 -27
  86. package/dist/client/effect.d.ts +0 -27
  87. package/dist/client/effect.js +0 -37
  88. package/dist/client/eventListener.d.ts +0 -39
  89. package/dist/client/eventListener.js +0 -52
  90. package/dist/client/loadHook.d.ts +0 -34
  91. package/dist/client/loadHook.js +0 -52
  92. package/dist/client/observer.d.ts +0 -36
  93. package/dist/client/observer.js +0 -66
  94. package/dist/client/runtime.d.ts +0 -105
  95. package/dist/client/runtime.js +0 -624
  96. package/dist/client/state.d.ts +0 -40
  97. package/dist/client/state.js +0 -110
  98. package/dist/compilation/compiler.d.ts +0 -163
  99. package/dist/compilation/compiler.js +0 -1164
  100. package/dist/components/ClientComponent.d.ts +0 -22
  101. package/dist/components/ClientComponent.js +0 -55
  102. package/dist/components/Link.d.ts +0 -16
  103. package/dist/components/Link.js +0 -21
  104. package/dist/components/Portal.d.ts +0 -2
  105. package/dist/components/Portal.js +0 -2
  106. package/dist/elements/element.d.ts +0 -87
  107. package/dist/elements/element.js +0 -33
  108. package/dist/elements/element_list.d.ts +0 -7
  109. package/dist/elements/element_list.js +0 -65
  110. package/dist/elements/raw.d.ts +0 -14
  111. package/dist/elements/raw.js +0 -78
  112. package/dist/elements/specific_props.d.ts +0 -750
  113. package/dist/elements/specific_props.js +0 -1
  114. package/dist/global.d.ts +0 -229
  115. package/dist/global.js +0 -1
  116. package/dist/index.d.ts +0 -16
  117. package/dist/index.js +0 -12
  118. package/dist/server/layout.d.ts +0 -34
  119. package/dist/server/layout.js +0 -6
  120. package/dist/server/log.d.ts +0 -12
  121. package/dist/server/log.js +0 -64
  122. package/dist/server/page.d.ts +0 -32
  123. package/dist/server/page.js +0 -6
  124. package/dist/server/runtime.d.ts +0 -6
  125. package/dist/server/runtime.js +0 -92
  126. package/scripts/bootstrap.js +0 -95
  127. package/scripts/bootstrap_files/elegance.txt +0 -40
  128. package/scripts/bootstrap_files/index.txt +0 -3
  129. package/scripts/bootstrap_files/layout.txt +0 -46
  130. package/scripts/bootstrap_files/middleware.txt +0 -18
  131. package/scripts/bootstrap_files/page.txt +0 -123
  132. package/scripts/bootstrap_files/route.txt +0 -6
  133. package/scripts/elegance_dev.ts +0 -42
  134. package/scripts/elegance_prod.ts +0 -42
  135. package/scripts/elegance_static.ts +0 -26
  136. package/scripts/prod.js +0 -13
  137. package/scripts/run.js +0 -13
  138. package/scripts/static.js +0 -13
@@ -1,712 +1,862 @@
1
- /**
2
- * The Elegance.JS server.
3
- * This server can be used to run your project.
4
- *
5
- * It's HTTP only, so if you want HTTPS, use a proxy.
6
- */
7
- import { join, normalize, relative, resolve } from "path";
8
- import { compilePage, compilerOptions, compilerStore } from "../compilation/compiler.js";
9
- import { createServer, } from "http";
10
- import { existsSync, readdirSync, statSync, createReadStream } from "fs";
11
- import * as zlib from "zlib";
12
- import { promisify } from "util";
13
- import { URLSearchParams } from "url";
14
- import { formattedLog, LogLevel } from "./log.js";
15
- const gzipAsync = promisify(zlib.gzip);
16
- function removePrefix(str, prefix) {
17
- return str.startsWith(prefix) ? str.slice(prefix.length) : str;
1
+ import { createServer, IncomingMessage, ServerResponse } from "node:http";
2
+ import { readFile, readdir, writeFile, mkdir, stat } from "node:fs/promises";
3
+ import { join, dirname, extname, relative, basename } from "node:path";
4
+ import {
5
+ gzip as gzipCb,
6
+ brotliCompress as brotliCb,
7
+ constants as zlibConstants
8
+ } from "node:zlib";
9
+ import { promisify } from "node:util";
10
+ const gzipAsync = promisify(gzipCb);
11
+ const brotliAsync = promisify(brotliCb);
12
+ import { performance } from "node:perf_hooks";
13
+ import { generateSyntheticBundle } from "../processing/oxc.js";
14
+ import { generatePageHTML, generateDynamicPageHTML, createRenderContext, runWithRenderContext } from "../build/render.js";
15
+ import { loadRouteFromCache, runBuildHooks, preClientMjsPath } from "../build/common.js";
16
+ import { OUT_DIR, DIST_DIR, PAGES_DIR, loadPaths } from "../constants.js";
17
+ import { getConfig } from "../config.js";
18
+ import { createSecurityHeaders } from "./security.js";
19
+ import { isRichError, printError, richError } from "../error.js";
20
+ import { logger } from "../logger.js";
21
+ async function loadServerOptions() {
22
+ await loadPaths();
23
+ const config = await getConfig();
24
+ return config.server;
18
25
  }
19
26
  let serverOptions;
20
- const allAPIRoutes = new Map();
21
- async function gatherAPIRoutes() {
22
- await walkDirectory(compilerOptions.pagesDirectory, async (file) => {
23
- if (file.name !== "route.ts")
24
- return;
25
- const pathname = sanitizePathname(relative(compilerOptions.pagesDirectory, file.parentPath));
26
- const fullPath = join(file.parentPath, file.name);
27
- const { POST, GET, PUT, DELETE, OPTIONS } = await import("file://" + fullPath);
28
- const methods = { POST, GET, PUT, DELETE, OPTIONS };
29
- for (const [name, method] of Object.entries(methods)) {
30
- if (method && typeof method !== "function") {
31
- throw new Error(`In file: "${fullPath}":\nThe export ${method} is not of type "function". Got: ${typeof method}`);
32
- }
33
- }
34
- const apiRouteInformation = {
35
- exports: {
36
- methods,
37
- },
38
- modulePath: fullPath,
39
- pathname: pathname,
40
- };
41
- allAPIRoutes.set(pathname, apiRouteInformation);
42
- });
27
+ const IS_DEV = process.env.ELEGANCE_DEV_MODE === "dev";
28
+ class LRU {
29
+ constructor(max) {
30
+ this.max = max;
31
+ }
32
+ max;
33
+ map = /* @__PURE__ */ new Map();
34
+ get(key) {
35
+ if (!this.map.has(key)) return void 0;
36
+ const v = this.map.get(key);
37
+ this.map.delete(key);
38
+ this.map.set(key, v);
39
+ return v;
40
+ }
41
+ set(key, val) {
42
+ if (this.map.has(key)) this.map.delete(key);
43
+ else if (this.map.size >= this.max) this.map.delete(this.map.keys().next().value);
44
+ this.map.set(key, val);
45
+ }
46
+ has(key) {
47
+ return this.map.has(key);
48
+ }
49
+ delete(key) {
50
+ this.map.delete(key);
51
+ }
43
52
  }
44
- async function handleAPIRequest(req, res, pathname) {
45
- const route = allAPIRoutes.get(pathname);
46
- if (!route) {
47
- res.statusCode = 404;
48
- await sendResponse(req, res, "Route does not exist.");
49
- return;
50
- }
51
- if (!req.method) {
52
- res.statusCode = 400;
53
- await sendResponse(req, res, "Bad request");
54
- return;
55
- }
56
- const method = route.exports.methods[req.method];
57
- if (!method) {
58
- res.statusCode = 405;
59
- await sendResponse(req, res, "Method not allowed");
53
+ const staticCache = /* @__PURE__ */ new Map();
54
+ const dynamicModuleCache = new LRU(256);
55
+ const aotStaticCache = /* @__PURE__ */ new Map();
56
+ const statusCodePageCache = /* @__PURE__ */ new Map();
57
+ const middlewareChainCache = /* @__PURE__ */ new Map();
58
+ const encCache = new LRU(512);
59
+ let staticRouteMap = null;
60
+ let paramRoutes = null;
61
+ let apiRoutesCache = null;
62
+ let middlewareMapCache = null;
63
+ const MIME_TYPES = {
64
+ ".html": "text/html",
65
+ ".js": "application/javascript",
66
+ ".mjs": "application/javascript",
67
+ ".css": "text/css",
68
+ ".png": "image/png",
69
+ ".jpg": "image/jpeg",
70
+ ".svg": "image/svg+xml",
71
+ ".json": "application/json",
72
+ ".ico": "image/x-icon",
73
+ ".txt": "text/plain"
74
+ };
75
+ const GZIP_PARAMS = { level: 6 };
76
+ const BROTLI_PARAMS = { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 4 } };
77
+ const BODY_TIMEOUT_MS = parseInt(process.env.BODY_TIMEOUT_MS ?? "10000", 10);
78
+ let SECURITY_HEADERS;
79
+ async function initSecurityHeaders() {
80
+ const config = await getConfig();
81
+ SECURITY_HEADERS = createSecurityHeaders(config.security);
82
+ }
83
+ function buildCachedFileHeaders(mime, etag, rawLen, gzipLen, brotliLen, cacheControl = "public, max-age=31536000, immutable") {
84
+ const base = {
85
+ ...SECURITY_HEADERS,
86
+ "ETag": etag,
87
+ "Cache-Control": cacheControl,
88
+ "Vary": "Accept-Encoding"
89
+ };
90
+ return {
91
+ raw: { ...base, "Content-Type": mime, "Content-Length": rawLen },
92
+ gzip: { ...base, "Content-Type": mime, "Content-Length": gzipLen, "Content-Encoding": "gzip" },
93
+ brotli: { ...base, "Content-Type": mime, "Content-Length": brotliLen, "Content-Encoding": "br" }
94
+ };
95
+ }
96
+ async function primeStaticCache() {
97
+ async function walk(dir) {
98
+ let entries;
99
+ try {
100
+ entries = await readdir(dir, { withFileTypes: true });
101
+ } catch {
102
+ return;
103
+ }
104
+ await Promise.all(entries.map(async (e) => {
105
+ const full = join(dir, e.name);
106
+ if (e.isDirectory()) {
107
+ await walk(full);
60
108
  return;
61
- }
62
- method(req, res);
109
+ }
110
+ const urlPath = "/" + relative(DIST_DIR, full).replace(/\\/g, "/");
111
+ const [raw, filestat] = await Promise.all([readFile(full), stat(full)]);
112
+ const [gzip, brotli] = await Promise.all([
113
+ gzipAsync(raw, GZIP_PARAMS),
114
+ brotliAsync(raw, BROTLI_PARAMS)
115
+ ]);
116
+ const mime = MIME_TYPES[extname(full)] ?? "application/octet-stream";
117
+ const etag = `"${filestat.size}-${filestat.mtimeMs}"`;
118
+ staticCache.set(urlPath, {
119
+ raw,
120
+ gzip,
121
+ brotli,
122
+ mime,
123
+ etag,
124
+ headers: buildCachedFileHeaders(mime, etag, raw.length, gzip.length, brotli.length)
125
+ });
126
+ }));
127
+ }
128
+ await walk(DIST_DIR);
63
129
  }
64
- /**
65
- * Go through a directory, including all it's subdirectories,
66
- * and call callback() for each file.
67
- */
68
- async function walkDirectory(fullPath, callback) {
69
- const stack = [];
70
- stack.push(...readdirSync(fullPath, { withFileTypes: true, }));
71
- while (true) {
72
- const entry = stack.pop();
73
- if (!entry)
74
- break;
75
- if (entry.isDirectory()) {
76
- const fullPath = join(entry.parentPath, entry.name);
77
- stack.push(...readdirSync(fullPath, { withFileTypes: true, }));
78
- continue;
79
- }
80
- if (!entry.isFile())
81
- continue;
82
- await callback(entry);
83
- }
130
+ function compileRouteMatcher(pattern) {
131
+ const norm = (p) => (p.endsWith("/") && p !== "/" ? p.slice(0, -1) : p) || "/";
132
+ const normalisedPattern = norm(pattern);
133
+ if (!normalisedPattern.includes("[")) {
134
+ return (pathname) => norm(pathname) === normalisedPattern ? {} : null;
135
+ }
136
+ const paramMeta = [];
137
+ const reSource = normalisedPattern.split("/").filter(Boolean).map((seg) => {
138
+ if (seg.startsWith("[...") && seg.endsWith("]")) {
139
+ paramMeta.push({ name: seg.slice(4, -1), catchAll: true, optional: false });
140
+ return "(.+)";
141
+ }
142
+ if (seg.startsWith(":[") && seg.endsWith("]")) {
143
+ paramMeta.push({ name: seg.slice(2, -1), catchAll: false, optional: true });
144
+ return "([^/]+)?";
145
+ }
146
+ if (seg.startsWith("[") && seg.endsWith("]")) {
147
+ paramMeta.push({ name: seg.slice(1, -1), catchAll: false, optional: false });
148
+ return "([^/]+)";
149
+ }
150
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
151
+ }).join("/");
152
+ const re = new RegExp(`^/?${reSource}/?$`);
153
+ return (pathname) => {
154
+ const m = re.exec(pathname);
155
+ if (!m) return null;
156
+ const params = {};
157
+ for (let i = 0; i < paramMeta.length; i++) {
158
+ const { name, catchAll, optional } = paramMeta[i];
159
+ const val = m[i + 1];
160
+ if (catchAll) {
161
+ params[name] = val ? val.split("/") : [];
162
+ } else {
163
+ params[name] = optional && !val ? void 0 : val;
164
+ }
165
+ }
166
+ return params;
167
+ };
84
168
  }
85
- function safePercentDecode(input) {
86
- return input.replace(/%[0-9A-Fa-f]{2}/g, (m) => String.fromCharCode(parseInt(m.slice(1), 16)));
169
+ function routeEntryToMatched(entry) {
170
+ let matcher;
171
+ if (entry.kind === "enumerated") {
172
+ const patternMatcher = compileRouteMatcher(entry.patternPathname);
173
+ const norm = (p) => (p.endsWith("/") && p !== "/" ? p.slice(0, -1) : p) || "/";
174
+ const concreteNorm = norm(entry.pathname);
175
+ matcher = (pathname) => norm(pathname) === concreteNorm ? patternMatcher(pathname) : null;
176
+ } else {
177
+ matcher = compileRouteMatcher(entry.pathname);
178
+ }
179
+ return {
180
+ pathname: entry.pathname,
181
+ pageFile: entry.pageFile,
182
+ layouts: entry.layouts,
183
+ layoutCacheKeys: entry.layoutCacheKeys,
184
+ cacheKey: entry.cacheKey,
185
+ sharedChunkPaths: entry.sharedChunkPaths,
186
+ isDynamic: entry.kind === "dynamic",
187
+ patternPathname: entry.kind === "enumerated" ? entry.patternPathname : void 0,
188
+ matcher
189
+ };
87
190
  }
88
- function sanitizePathname(pathname = "") {
89
- if (!pathname)
90
- return "/";
91
- pathname = safePercentDecode(pathname);
92
- pathname = "/" + pathname;
93
- pathname = pathname.replace(/\/+/g, "/");
94
- const segments = pathname.split("/");
95
- const resolved = [];
96
- for (const segment of segments) {
97
- if (!segment || segment === ".")
98
- continue;
99
- if (segment === "..") {
100
- resolved.pop();
101
- continue;
102
- }
103
- resolved.push(segment);
104
- }
105
- const encoded = resolved.map((s) => encodeURIComponent(s));
106
- return "/" + encoded.join("/");
191
+ async function warmDynamicCaches(manifest) {
192
+ const sMap = /* @__PURE__ */ new Map();
193
+ const pRoutes = [];
194
+ await Promise.all(manifest.routes.map(async (entry) => {
195
+ const route = routeEntryToMatched(entry);
196
+ if (entry.kind === "enumerated") {
197
+ pRoutes.push(route);
198
+ } else {
199
+ sMap.set(entry.pathname, route);
200
+ }
201
+ if (entry.kind === "dynamic") {
202
+ try {
203
+ const compiled = await loadRouteFromCache(route);
204
+ dynamicModuleCache.set(entry.pathname, compiled);
205
+ } catch (err) {
206
+ if (isRichError(err)) throw err;
207
+ throw richError({
208
+ title: "Failed to Load Cached Route",
209
+ cause: `${err}`,
210
+ origin: entry.pageFile,
211
+ doShowStack: false
212
+ });
213
+ }
214
+ }
215
+ if (entry.kind !== "dynamic" && IS_DEV) {
216
+ try {
217
+ const compiled = await loadRouteFromCache(route);
218
+ aotStaticCache.set(entry.pathname, compiled);
219
+ } catch (err) {
220
+ if (isRichError(err)) throw err;
221
+ throw richError({
222
+ title: "Failed to Load Cached Route",
223
+ cause: `${err}`,
224
+ origin: entry.pageFile,
225
+ doShowStack: false
226
+ });
227
+ }
228
+ }
229
+ }));
230
+ await Promise.all(manifest.statusCodePages.map(async (ep) => {
231
+ try {
232
+ const compiled = await loadRouteFromCache({
233
+ pageFile: ep.pageFile,
234
+ layouts: ep.layouts,
235
+ layoutCacheKeys: ep.layoutCacheKeys,
236
+ cacheKey: ep.cacheKey,
237
+ pathname: ""
238
+ });
239
+ const code = parseInt(basename(ep.pageFile), 10);
240
+ statusCodePageCache.set(
241
+ `${dirname(ep.pageFile)}:${isNaN(code) ? 0 : code}`,
242
+ {
243
+ compiled,
244
+ pageFile: ep.pageFile,
245
+ layouts: ep.layouts,
246
+ layoutCacheKeys: ep.layoutCacheKeys,
247
+ cacheKey: ep.cacheKey
248
+ }
249
+ );
250
+ } catch (err) {
251
+ if (isRichError(err)) throw err;
252
+ throw richError({
253
+ title: "Failed to Load Status Code Route",
254
+ cause: err,
255
+ origin: ep.pageFile,
256
+ doShowStack: false
257
+ });
258
+ }
259
+ }));
260
+ return { staticRouteMap: sMap, paramRoutes: pRoutes };
107
261
  }
108
- function getStatusCodePage(statusCode, pathname) {
109
- const pages = serverOptions.allStatusCodePages;
110
- let currentPath = pathname;
111
- if (!currentPath.startsWith("/")) {
112
- currentPath = "/" + currentPath;
113
- }
114
- while (true) {
115
- let candidate;
116
- if (currentPath === "/") {
117
- candidate = `/${statusCode}`;
118
- }
119
- else {
120
- candidate = `${currentPath.replace(/\/$/, "")}/${statusCode}`;
121
- }
122
- const pageInfo = pages.get(candidate);
123
- if (pageInfo) {
124
- pageInfo.pathname = pathname;
125
- return pageInfo;
126
- }
127
- if (currentPath === "/") {
128
- break;
129
- }
130
- const lastSlash = currentPath.lastIndexOf("/");
131
- if (lastSlash <= 0) {
132
- currentPath = "/";
133
- }
134
- else {
135
- currentPath = currentPath.slice(0, lastSlash);
136
- }
137
- }
262
+ async function loadApiRoutes(manifest) {
263
+ if (apiRoutesCache) return apiRoutesCache;
264
+ apiRoutesCache = /* @__PURE__ */ new Map();
265
+ await Promise.all(manifest.apiRoutes.map(async (entry) => {
266
+ apiRoutesCache.set(entry.pathname, await import(entry.file));
267
+ }));
268
+ return apiRoutesCache;
138
269
  }
139
- async function respondWithStatusCodePage(req, res, pathname, statusCode, message) {
140
- const statusCodePage = getStatusCodePage(statusCode, pathname);
141
- if (!statusCodePage) {
142
- res.statusCode = statusCode;
143
- await sendResponse(req, res, message);
144
- return;
145
- }
146
- const compiledPage = await compilePage(serverOptions.allLayouts, statusCodePage, { req, res });
147
- res.statusCode = 200;
148
- await sendResponse(req, res, compiledPage.pageHTML, "text/html");
270
+ async function loadMiddlewareMap(manifest) {
271
+ if (middlewareMapCache) return middlewareMapCache;
272
+ middlewareMapCache = /* @__PURE__ */ new Map();
273
+ await Promise.all(manifest.middlewares.map(async (entry) => {
274
+ const mod = await import(entry.file);
275
+ if (!mod.default) {
276
+ printError(richError({
277
+ title: "Invalid Middleware",
278
+ cause: "Could not get module.default within a middleware file, which is required for the middleware to function.",
279
+ hint: "Did you forget the *default* keyword when exporting your function?",
280
+ origin: entry.file,
281
+ doShowStack: false
282
+ }));
283
+ process.exit(1);
284
+ }
285
+ middlewareMapCache.set(dirname(entry.file), [typeof mod.default === "function" ? mod.default : mod]);
286
+ }));
287
+ return middlewareMapCache;
149
288
  }
150
- async function respondWithStatusCode(req, res, pathname, statusCode, message) {
151
- if (serverOptions.allowStatusCodePages === true) {
152
- return respondWithStatusCodePage(req, res, pathname, statusCode, message);
153
- }
154
- res.statusCode = statusCode;
155
- await sendResponse(req, res, message);
289
+ function getMiddlewareChain(routePattern) {
290
+ if (middlewareChainCache.has(routePattern)) return middlewareChainCache.get(routePattern);
291
+ const dirMap = middlewareMapCache;
292
+ const parts = routePattern.replace(/^\//, "").split("/").filter(Boolean);
293
+ const chain = [];
294
+ let dir = PAGES_DIR;
295
+ if (dirMap.has(dir)) chain.push(...dirMap.get(dir));
296
+ for (const part of parts) {
297
+ dir = join(dir, part);
298
+ if (dirMap.has(dir)) chain.push(...dirMap.get(dir));
299
+ }
300
+ middlewareChainCache.set(routePattern, chain);
301
+ return chain;
156
302
  }
157
- /**
158
- * Ensure a given pathname is safe, and does not escape the root directory.
159
- * @param userInputPath The path to turn into a safe path
160
- * @returns A safe path, or null if the path was not safe, or does not exist.
161
- */
162
- async function getSafePath(userInputPath) {
163
- const rootDirectory = resolve(join(compilerOptions.outputDirectory, "DIST"));
164
- const decodedPath = decodeURIComponent(userInputPath);
165
- const normalizedPath = normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, '');
166
- const finalPath = join(rootDirectory, normalizedPath);
167
- const resolvedFinalPath = resolve(finalPath);
168
- if (!resolvedFinalPath.startsWith(rootDirectory) || existsSync(resolvedFinalPath) === false) {
169
- return null;
170
- }
171
- return resolvedFinalPath;
303
+ function extractPathname(rawUrl) {
304
+ if (!rawUrl) return "/";
305
+ let end = rawUrl.length;
306
+ for (let i = 0; i < rawUrl.length; i++) {
307
+ const ch = rawUrl.charCodeAt(i);
308
+ if (ch === 63 || ch === 35) {
309
+ end = i;
310
+ break;
311
+ }
312
+ }
313
+ const path = rawUrl.slice(0, end) || "/";
314
+ if (path.indexOf("%") !== -1) {
315
+ try {
316
+ return decodeURIComponent(path);
317
+ } catch {
318
+ return path;
319
+ }
320
+ }
321
+ return path;
172
322
  }
173
- async function handlePageRequest(req, res, pathname, pageInformation, matchHit) {
174
- if (pageInformation.exports.isDynamic) {
175
- if (serverOptions.allowDynamic === false) {
176
- return respondWithStatusCode(req, res, pathname, 404, "Page not found.");
177
- }
178
- const informationClone = {
179
- ...pageInformation,
180
- };
181
- informationClone.pathname = pathname;
182
- const result = await compilePage(serverOptions.allLayouts, informationClone, { req, res }, matchHit.params);
183
- if (res.writableEnded || res.headersSent)
184
- return;
185
- res.statusCode = 200;
186
- await sendResponse(req, res, result.pageHTML, "text/html");
187
- return;
188
- }
189
- const { pageHTML } = serverOptions.builtStaticPages.get(pageInformation.pathname);
190
- res.statusCode = 200;
191
- await sendResponse(req, res, pageHTML, "text/html");
323
+ function acceptEncoding(req) {
324
+ const raw = req.headers["accept-encoding"];
325
+ if (!raw) return null;
326
+ const cached = encCache.get(raw);
327
+ if (cached !== void 0) return cached;
328
+ const result = raw.includes("br") ? "br" : raw.includes("gzip") ? "gzip" : null;
329
+ encCache.set(raw, result);
330
+ return result;
192
331
  }
193
- const mimeByExt = {
194
- ".html": "text/html",
195
- ".htm": "text/html",
196
- ".css": "text/css",
197
- ".js": "text/javascript",
198
- ".mjs": "text/javascript",
199
- ".json": "application/json",
200
- ".png": "image/png",
201
- ".jpg": "image/jpeg",
202
- ".jpeg": "image/jpeg",
203
- ".gif": "image/gif",
204
- ".svg": "image/svg+xml",
205
- ".txt": "text/plain",
206
- ".mp4": "video/mp4",
207
- ".webm": "video/webm",
208
- ".mkv": "video/x-matroska",
209
- ".avi": "video/x-msvideo",
210
- ".mov": "video/quicktime",
211
- ".mp3": "audio/mpeg",
212
- };
213
- function isCompressible(mime) {
214
- return mime.startsWith('text/') ||
215
- mime === 'application/javascript' ||
216
- mime === 'application/json' ||
217
- mime === 'image/svg+xml';
332
+ async function buildRouteHTML(compiled, route, params, requestPathname, req, res) {
333
+ const ctx = createRenderContext();
334
+ for (const { id, initial } of compiled.atomSeeds) {
335
+ if (!ctx.atomValues.has(id)) {
336
+ ctx.atomValues.set(id, initial);
337
+ ctx.atomRegistry.push({ id });
338
+ }
339
+ }
340
+ const preClientCode = await readFile(preClientMjsPath(route.cacheKey), "utf-8");
341
+ let html = await runWithRenderContext(ctx, async () => {
342
+ const [rootNode, metaNodes] = await Promise.all([
343
+ compiled.default(params, req, res),
344
+ compiled.metadata(params, req, res)
345
+ ]);
346
+ const getClientCode = () => generateSyntheticBundle(
347
+ preClientCode,
348
+ requestPathname,
349
+ ctx.regions,
350
+ route.layoutCacheKeys
351
+ );
352
+ return generateDynamicPageHTML(rootNode, metaNodes, route, getClientCode, ctx);
353
+ });
354
+ const chunks = route.sharedChunkPaths.filter((chunkPath) => preClientCode.includes(chunkPath));
355
+ if (chunks.length > 0) {
356
+ const preloadTags = chunks.map((p) => `<link rel="modulepreload" href="${p}">`).join("");
357
+ html = html.replace("</head>", `${preloadTags}</head>`);
358
+ }
359
+ return html;
218
360
  }
219
- async function handleFileRequest(req, res, pathname) {
220
- const safePath = await getSafePath(pathname);
221
- if (!safePath) {
222
- return respondWithStatusCode(req, res, pathname, 404, "File not found.");
223
- }
224
- const stats = statSync(safePath);
225
- if (stats.isDirectory()) {
226
- return respondWithStatusCode(req, res, pathname, 404, "File not found.");
227
- }
228
- const fileSize = stats.size;
229
- const ext = safePath.slice(safePath.lastIndexOf(".")).toLowerCase();
230
- const mime = mimeByExt[ext] ?? "application/octet-stream";
231
- const acceptEncoding = req.headers["accept-encoding"] || "";
232
- const rangeHeader = req.headers.range;
233
- if (!rangeHeader) {
234
- const useGzip = acceptEncoding.includes('gzip') && isCompressible(mime);
235
- const head = {
236
- 'Content-Type': mime,
237
- 'Accept-Ranges': 'bytes',
238
- };
239
- if (!useGzip) {
240
- head['Content-Length'] = fileSize;
241
- }
242
- if (useGzip) {
243
- head['Content-Encoding'] = 'gzip';
244
- head['Vary'] = 'Accept-Encoding';
245
- }
246
- res.writeHead(200, head);
247
- const stream = createReadStream(safePath);
248
- if (useGzip) {
249
- const gzip = zlib.createGzip();
250
- stream.pipe(gzip).pipe(res);
251
- }
252
- else {
253
- stream.pipe(res);
254
- }
255
- return;
256
- }
257
- const ranges = rangeHeader.replace(/bytes=/, '').split('-');
258
- let start = parseInt(ranges[0], 10);
259
- let end = ranges[1] ? parseInt(ranges[1], 10) : fileSize - 1;
260
- if (isNaN(start))
261
- start = 0;
262
- if (isNaN(end) || end >= fileSize)
263
- end = fileSize - 1;
264
- if (start >= fileSize || start > end) {
265
- res.writeHead(416, {
266
- 'Content-Range': `bytes */${fileSize}`,
267
- });
268
- res.end();
269
- return;
270
- }
271
- const contentLength = end - start + 1;
272
- const headers = {
273
- 'Content-Type': mime,
274
- 'Accept-Ranges': 'bytes',
275
- 'Content-Range': `bytes ${start}-${end}/${fileSize}`,
276
- 'Content-Length': contentLength,
277
- };
278
- res.writeHead(206, headers);
279
- const stream = createReadStream(safePath, { start, end });
280
- stream.pipe(res);
361
+ async function pipeRouteToResponse(compiled, route, params, requestPathname, req, res, statusCode = 200) {
362
+ const start = IS_DEV ? performance.now() : 0;
363
+ const html = await buildRouteHTML(compiled, route, params, requestPathname, req, res);
364
+ if (IS_DEV) {
365
+ logger.debug(`rendered ${requestPathname} (${(performance.now() - start).toFixed(1)}ms)`);
366
+ }
367
+ if (res.writableEnded || res.destroyed) return;
368
+ const enc = acceptEncoding(req);
369
+ const body = enc === "br" ? await brotliAsync(html, BROTLI_PARAMS) : enc === "gzip" ? await gzipAsync(html, GZIP_PARAMS) : Buffer.from(html);
370
+ const headers = {
371
+ ...SECURITY_HEADERS,
372
+ "Content-Type": "text/html",
373
+ "Content-Length": body.length,
374
+ "Vary": "Accept-Encoding",
375
+ "Cache-Control": "no-store"
376
+ };
377
+ if (enc) headers["Content-Encoding"] = enc;
378
+ res.writeHead(statusCode, headers);
379
+ res.end(body);
281
380
  }
282
- function getPathSubparts(path) {
283
- const rawParts = path.split('/').filter(Boolean);
284
- const parts = [...rawParts];
285
- if (parts.length > 0 && parts[parts.length - 1].includes('.')) {
286
- parts.pop();
287
- }
288
- const result = ['/'];
289
- let current = '';
290
- for (const part of parts) {
291
- current += '/' + part;
292
- result.push(current);
293
- }
381
+ async function lazyBuildStaticPage(compiled, route, params, requestPathname, req, res) {
382
+ const overallStart = performance.now();
383
+ const timings = {};
384
+ await runBuildHooks(compiled, "pre");
385
+ const ctx = createRenderContext();
386
+ for (const { id, initial } of compiled.atomSeeds) {
387
+ if (!ctx.atomValues.has(id)) {
388
+ ctx.atomValues.set(id, initial);
389
+ ctx.atomRegistry.push({ id });
390
+ }
391
+ }
392
+ let t = performance.now();
393
+ const bundleUrl = `${route.pathname === "/" ? "" : route.pathname}/bundle.js`;
394
+ const { fullHtml } = await runWithRenderContext(ctx, async () => {
395
+ const [rootNode, metaNodes] = await Promise.all([
396
+ compiled.default(params, req, res),
397
+ compiled.metadata(params, req, res)
398
+ ]);
399
+ timings["render"] = performance.now() - t;
400
+ t = performance.now();
401
+ const result = await generatePageHTML(
402
+ rootNode,
403
+ metaNodes,
404
+ route,
405
+ bundleUrl,
406
+ ctx,
407
+ route.sharedChunkPaths
408
+ );
409
+ timings["html"] = performance.now() - t;
294
410
  return result;
411
+ });
412
+ const preClientCode = await readFile(preClientMjsPath(route.cacheKey), "utf-8");
413
+ t = performance.now();
414
+ const syntheticCode = generateSyntheticBundle(
415
+ preClientCode,
416
+ requestPathname,
417
+ ctx.regions,
418
+ route.layoutCacheKeys
419
+ );
420
+ timings["bundle"] = performance.now() - t;
421
+ await runBuildHooks(compiled, "post");
422
+ t = performance.now();
423
+ const routeOutDir = route.pathname === "/" ? DIST_DIR : join(DIST_DIR, route.pathname);
424
+ await mkdir(routeOutDir, { recursive: true });
425
+ await Promise.all([
426
+ writeFile(join(routeOutDir, "index.html"), fullHtml),
427
+ writeFile(join(routeOutDir, "bundle.js"), syntheticCode)
428
+ ]);
429
+ timings["write"] = performance.now() - t;
430
+ t = performance.now();
431
+ const htmlBuf = Buffer.from(fullHtml);
432
+ const jsBuf = Buffer.from(syntheticCode);
433
+ const [htmlGzip, htmlBrotli, jsGzip, jsBrotli] = await Promise.all([
434
+ gzipAsync(htmlBuf, GZIP_PARAMS),
435
+ brotliAsync(htmlBuf, BROTLI_PARAMS),
436
+ gzipAsync(jsBuf, GZIP_PARAMS),
437
+ brotliAsync(jsBuf, BROTLI_PARAMS)
438
+ ]);
439
+ timings["compress"] = performance.now() - t;
440
+ const htmlMime = "text/html";
441
+ const jsMime = "application/javascript";
442
+ const now = Date.now();
443
+ const htmlEtag = `"${htmlBuf.length}-${now}"`;
444
+ const jsEtag = `"${jsBuf.length}-${now}"`;
445
+ const htmlCached = {
446
+ raw: htmlBuf,
447
+ gzip: htmlGzip,
448
+ brotli: htmlBrotli,
449
+ mime: htmlMime,
450
+ etag: htmlEtag,
451
+ headers: buildCachedFileHeaders(htmlMime, htmlEtag, htmlBuf.length, htmlGzip.length, htmlBrotli.length, "no-store")
452
+ };
453
+ const jsCached = {
454
+ raw: jsBuf,
455
+ gzip: jsGzip,
456
+ brotli: jsBrotli,
457
+ mime: jsMime,
458
+ etag: jsEtag,
459
+ headers: buildCachedFileHeaders(jsMime, jsEtag, jsBuf.length, jsGzip.length, jsBrotli.length, "no-store")
460
+ };
461
+ staticCache.set(resolveStaticIndexKey(route.pathname), htmlCached);
462
+ staticCache.set(bundleUrl, jsCached);
463
+ const total = performance.now() - overallStart;
464
+ const steps = Object.entries(timings).map(
465
+ ([step, ms], i, arr) => ` ${i === arr.length - 1 ? "\u2514\u2500" : "\u251C\u2500"} ${step}: ${ms.toFixed(1)}ms`
466
+ ).join("\n");
467
+ logger.debug(`built ${route.pathname} (${total.toFixed(1)}ms total)
468
+ ${steps}`);
469
+ serveCachedFile(req, res, htmlCached);
295
470
  }
296
- const allMiddleware = new Map();
297
- async function gatherMiddleware() {
298
- await walkDirectory(compilerOptions.pagesDirectory, async (file) => {
299
- if (file.name !== "middleware.ts")
300
- return;
301
- const pathname = sanitizePathname(relative(compilerOptions.pagesDirectory, file.parentPath));
302
- const fullPath = join(file.parentPath, file.name);
303
- const { middleware } = await import("file://" + fullPath);
304
- if (!middleware || typeof middleware !== "function") {
305
- throw new Error(`In file: "${fullPath}":\nThe export middleware is not of type "function". Got: ${typeof middleware}`);
306
- }
307
- const middlewareInformation = {
308
- exports: {
309
- middleware,
310
- },
311
- modulePath: fullPath,
312
- pathname: pathname,
313
- };
314
- allMiddleware.set(pathname, middlewareInformation);
315
- });
471
+ async function runMiddlewareChain(mws, req, res, final) {
472
+ let i = 0;
473
+ const next = async () => {
474
+ if (i < mws.length) await mws[i++](req, res, next);
475
+ else await final();
476
+ };
477
+ await next();
316
478
  }
317
- async function runMiddleware(req, res, pathname) {
318
- const parts = getPathSubparts(pathname);
319
- const middlewares = [];
320
- for (const part of parts) {
321
- if (allMiddleware.has(part) === false) {
322
- continue;
323
- }
324
- middlewares.push(allMiddleware.get(part));
325
- }
326
- if (middlewares.length < 1)
479
+ async function respondWithStatusCode(code, requestPathname, params, req, res) {
480
+ if (serverOptions.allowStatusCodePages) {
481
+ let bestEntry = null;
482
+ let bestDepth = -1;
483
+ for (const [key, entry] of statusCodePageCache) {
484
+ const colonIdx = key.lastIndexOf(":");
485
+ const dirPath = key.slice(0, colonIdx);
486
+ const keyCode = parseInt(key.slice(colonIdx + 1), 10);
487
+ if (keyCode !== code && keyCode !== 0) continue;
488
+ const relDir = relative(PAGES_DIR, dirPath);
489
+ const dirPattern = relDir === "" ? "/" : `/${relDir.replace(/\\/g, "/")}`;
490
+ const trailPattern = dirPattern === "/" ? "/[...__trail]" : `${dirPattern}/[...__trail]`;
491
+ const exactMatches = compileRouteMatcher(dirPattern)(requestPathname);
492
+ const trailMatches = compileRouteMatcher(trailPattern)(requestPathname);
493
+ if (exactMatches === null && trailMatches === null) continue;
494
+ const depth = dirPattern === "/" ? 0 : dirPattern.split("/").filter(Boolean).length;
495
+ if (depth > bestDepth) {
496
+ bestEntry = entry;
497
+ bestDepth = depth;
498
+ }
499
+ }
500
+ if (bestEntry) {
501
+ const route = {
502
+ pathname: requestPathname,
503
+ pageFile: bestEntry.pageFile,
504
+ layouts: bestEntry.layouts,
505
+ layoutCacheKeys: bestEntry.layoutCacheKeys,
506
+ cacheKey: bestEntry.cacheKey,
507
+ sharedChunkPaths: [],
508
+ isDynamic: true,
509
+ matcher: () => ({})
510
+ };
511
+ await pipeRouteToResponse(bestEntry.compiled, route, params, requestPathname, req, res, code);
512
+ return;
513
+ }
514
+ }
515
+ res.writeHead(code);
516
+ res.end();
517
+ }
518
+ function stripTrailingSlash(p) {
519
+ return p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
520
+ }
521
+ function resolveStaticIndexKey(pathname) {
522
+ if (extname(pathname)) return pathname;
523
+ const clean = pathname.replace(/\/$/, "") || "/";
524
+ return clean === "/" ? "/index.html" : `${clean}/index.html`;
525
+ }
526
+ function serveCachedFile(req, res, cached, statusCode = 200) {
527
+ if (req.headers["if-none-match"] === cached.etag) {
528
+ res.writeHead(304);
529
+ res.end();
530
+ return;
531
+ }
532
+ const enc = acceptEncoding(req);
533
+ if (enc === "br") {
534
+ res.writeHead(statusCode, cached.headers.brotli);
535
+ res.end(cached.brotli);
536
+ } else if (enc === "gzip") {
537
+ res.writeHead(statusCode, cached.headers.gzip);
538
+ res.end(cached.gzip);
539
+ } else {
540
+ res.writeHead(statusCode, cached.headers.raw);
541
+ res.end(cached.raw);
542
+ }
543
+ }
544
+ function getRequestBody(req) {
545
+ return new Promise((resolve, reject) => {
546
+ const chunks = [];
547
+ let totalLen = 0;
548
+ const timer = setTimeout(() => {
549
+ req.socket.destroy();
550
+ reject(new Error("Request body timeout"));
551
+ }, BODY_TIMEOUT_MS);
552
+ req.on("data", (chunk) => {
553
+ totalLen += chunk.length;
554
+ if (totalLen > 1e6) {
555
+ clearTimeout(timer);
556
+ req.socket.destroy();
557
+ reject(new Error("Request body too large"));
327
558
  return;
328
- const next = (idx) => {
329
- if (idx >= middlewares.length) {
330
- return;
331
- }
332
- const middleware = middlewares[idx];
333
- const localNext = () => {
334
- next(idx + 1);
335
- };
336
- middleware.exports.middleware(req, res, localNext);
337
- };
338
- next(0);
559
+ }
560
+ chunks.push(chunk);
561
+ });
562
+ req.on("end", () => {
563
+ clearTimeout(timer);
564
+ const body = Buffer.concat(chunks).toString("utf8");
565
+ try {
566
+ resolve(JSON.parse(body));
567
+ } catch {
568
+ resolve(body);
569
+ }
570
+ });
571
+ req.on("error", (err) => {
572
+ clearTimeout(timer);
573
+ reject(err);
574
+ });
575
+ });
339
576
  }
340
- async function sendResponse(req, res, data, contentType = "text/plain") {
341
- let buffer = typeof data === "string" ? Buffer.from(data) : data;
342
- const acceptEncoding = req.headers["accept-encoding"] || "";
343
- if (acceptEncoding.match(/\bgzip\b/)) {
344
- try {
345
- buffer = await gzipAsync(buffer);
346
- res.setHeader("Content-Encoding", "gzip");
347
- res.setHeader("Vary", "Accept-Encoding");
348
- }
349
- catch (err) {
350
- console.error("Gzip compression error:", err);
351
- }
352
- }
353
- res.setHeader("Content-Type", contentType);
354
- res.setHeader("Content-Length", buffer.length.toString());
355
- res.end(buffer);
577
+ async function runServerAction(req, res) {
578
+ let actionId;
579
+ {
580
+ const header = req.headers["elegance-action"];
581
+ if (!header || Array.isArray(header)) {
582
+ res.statusCode = 400;
583
+ res.end();
584
+ return;
585
+ }
586
+ actionId = header;
587
+ }
588
+ const serverAction = globalThis.__serverActions.find((a) => a.id === actionId);
589
+ if (!serverAction) {
590
+ res.statusCode = 404;
591
+ res.end();
592
+ return;
593
+ }
594
+ if (req.method !== "POST") {
595
+ res.statusCode = 405;
596
+ res.end();
597
+ return;
598
+ }
599
+ const requestParams = await getRequestBody(req);
600
+ if (!requestParams || typeof requestParams !== "object" || Array.isArray(requestParams)) {
601
+ res.statusCode = 400;
602
+ res.end();
603
+ return;
604
+ }
605
+ if (serverAction.props) {
606
+ for (const [key, value] of Object.entries(serverAction.props)) {
607
+ let badRequest2 = function(reason) {
608
+ res.statusCode = 400;
609
+ res.end(reason);
610
+ };
611
+ var badRequest = badRequest2;
612
+ const requestParam = requestParams[key];
613
+ if (requestParam === void 0 || requestParam === null) {
614
+ if (value.required) return badRequest2(`${key} is a required value`);
615
+ continue;
616
+ }
617
+ switch (value.type) {
618
+ case "string":
619
+ if (typeof requestParam !== "string")
620
+ return badRequest2(`${key} is not of type string`);
621
+ if (value.length && requestParam.length < value.length[0])
622
+ return badRequest2(`${key} is too short (min ${value.length[0]})`);
623
+ if (value.length && requestParam.length > value.length[1])
624
+ return badRequest2(`${key} is too long (max ${value.length[1]})`);
625
+ break;
626
+ case "array":
627
+ if (!Array.isArray(requestParam))
628
+ return badRequest2(`${key} is not an Array`);
629
+ if (value.length && requestParam.length < value.length[0])
630
+ return badRequest2(`${key} is too short (min ${value.length[0]})`);
631
+ if (value.length && requestParam.length > value.length[1])
632
+ return badRequest2(`${key} is too long (max ${value.length[1]})`);
633
+ break;
634
+ case "boolean":
635
+ if (typeof requestParam !== "boolean")
636
+ return badRequest2(`${key} is not of type boolean`);
637
+ break;
638
+ case "number":
639
+ if (typeof requestParam !== "number")
640
+ return badRequest2(`${key} is not of type number`);
641
+ if (value.min && requestParam < value.min)
642
+ return badRequest2(`${key} is too small`);
643
+ if (value.max && requestParam > value.max)
644
+ return badRequest2(`${key} is too large`);
645
+ break;
646
+ }
647
+ if (typeof requestParam !== value.type) {
648
+ res.statusCode = 400;
649
+ res.end();
650
+ return;
651
+ }
652
+ }
653
+ }
654
+ try {
655
+ const returnValue = await serverAction.callback({ ...requestParams, req, res });
656
+ if (res.writableEnded || res.destroyed) return;
657
+ res.end(JSON.stringify(returnValue));
658
+ return;
659
+ } catch {
660
+ res.statusCode = 500;
661
+ res.end();
662
+ return;
663
+ }
356
664
  }
357
- async function requestHandler(req, res) {
358
- if (req.method === "OPTIONS") {
359
- res.writeHead(204, {
360
- "Allow": "GET,POST,PUT,DELETE,OPTIONS",
361
- "Access-Control-Allow-Origin": "*",
362
- "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
363
- "Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "*",
364
- "Access-Control-Max-Age": "86400",
365
- });
665
+ async function handleRequest(req, res) {
666
+ req.socket.setNoDelay(IS_DEV);
667
+ const pathname = extractPathname(req.url);
668
+ if (IS_DEV) logger.info(`- ${req.method} : ${pathname}`);
669
+ if (pathname === "/__action") {
670
+ runServerAction(req, res);
671
+ return;
672
+ }
673
+ const cached = staticCache.get(pathname);
674
+ if (cached) {
675
+ serveCachedFile(req, res, cached);
676
+ return;
677
+ }
678
+ const apiModule = apiRoutesCache.get(stripTrailingSlash(pathname));
679
+ if (apiModule) {
680
+ const handler = apiModule[req.method];
681
+ if (typeof handler !== "function") {
682
+ res.writeHead(405);
683
+ res.end();
684
+ return;
685
+ }
686
+ runMiddlewareChain(getMiddlewareChain(pathname), req, res, async () => {
687
+ handler(req, res).catch((e) => {
688
+ res.writeHead(500);
366
689
  res.end();
690
+ printError(richError({
691
+ title: "API Route Threw",
692
+ cause: "An API Route threw an error whilst it was being executed.",
693
+ origin: pathname,
694
+ doShowStack: false
695
+ }));
696
+ logger.error(e);
697
+ });
698
+ });
699
+ return;
700
+ }
701
+ const normalizedPathname = stripTrailingSlash(pathname) || "/";
702
+ let matchedRoute = staticRouteMap.get(normalizedPathname) ?? null;
703
+ let params = {};
704
+ if (!matchedRoute) {
705
+ for (const r of paramRoutes) {
706
+ const match = r.matcher(pathname);
707
+ if (match) {
708
+ matchedRoute = r;
709
+ params = match;
710
+ break;
711
+ }
712
+ }
713
+ }
714
+ await runMiddlewareChain(getMiddlewareChain(matchedRoute?.pathname ?? pathname), req, res, async () => {
715
+ if (!matchedRoute) {
716
+ const staticIndex = staticCache.get(resolveStaticIndexKey(pathname));
717
+ if (staticIndex) {
718
+ serveCachedFile(req, res, staticIndex);
367
719
  return;
368
- }
369
- if (!req.url) {
370
- res.statusCode = 400;
371
- await sendResponse(req, res, "Bad request.");
720
+ }
721
+ await respondWithStatusCode(404, pathname, params, req, res);
722
+ return;
723
+ }
724
+ if (matchedRoute.isDynamic) {
725
+ const compiled = dynamicModuleCache.get(matchedRoute.pathname);
726
+ if (!compiled) {
727
+ printError(richError({
728
+ title: "No Cached Module for Dynamic Route",
729
+ cause: "A dynamic route was not warmed during server startup, this is likely an internal error.",
730
+ origin: matchedRoute.pathname,
731
+ doShowStack: true
732
+ }));
733
+ await respondWithStatusCode(500, pathname, params, req, res);
372
734
  return;
373
- }
374
- const url = new URL(`http://${process.env.HOST ?? 'localhost'}${req.url}`);
375
- if (serverOptions.base && url.pathname.startsWith(serverOptions.base) === false) {
376
- res.statusCode = 501;
377
- await sendResponse(req, res, "Path does not start with basename.");
735
+ }
736
+ await pipeRouteToResponse(compiled, matchedRoute, params, pathname, req, res, 200);
737
+ return;
738
+ }
739
+ if (IS_DEV) {
740
+ const builtHtml = staticCache.get(resolveStaticIndexKey(matchedRoute.pathname));
741
+ if (builtHtml) {
742
+ serveCachedFile(req, res, builtHtml);
378
743
  return;
379
- }
380
- const pathname = sanitizePathname(serverOptions.base ? removePrefix(serverOptions.base, url.pathname) : url.pathname);
381
- runMiddleware(req, res, pathname);
382
- if (res.writableEnded)
744
+ }
745
+ const compiled = aotStaticCache.get(matchedRoute.pathname);
746
+ if (!compiled) {
747
+ printError(richError({
748
+ title: "Internal Error",
749
+ cause: "A static route was not cached during lazy compilation on the server.",
750
+ origin: matchedRoute.pathname,
751
+ doShowStack: false
752
+ }));
753
+ await respondWithStatusCode(500, pathname, params, req, res);
383
754
  return;
384
- if (pathname.startsWith("/api/")) {
385
- return handleAPIRequest(req, res, pathname);
386
- }
387
- const matchingPage = matchPathnameToPathParts(pathname, [...serverOptions.allPages.values()].map(v => getPathPattern(v)));
388
- if (!matchingPage) {
389
- return handleFileRequest(req, res, pathname);
390
- }
391
- handlePageRequest(req, res, pathname, serverOptions.allPages.get(matchingPage.matchedPathname), matchingPage);
392
- }
393
- function getPathPattern(value) {
394
- return {
395
- pathname: value.pathname,
396
- pathnameParts: value.pathnameParts,
397
- };
398
- }
399
- function escapeRegExp(str) {
400
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
401
- }
402
- /**
403
- * Take a set of pathname parts, like ["blog", ":[postId]"], and turn that into a regex string to match a pathname against.
404
- * @param pathnameParts Parts to turn into a regex string
405
- * @returns Regex string
406
- */
407
- function buildRegexStrFromParts(pathnameParts) {
408
- let patternRegex = '^/';
409
- let hasPart = false;
410
- let previousCanSkip = false;
411
- for (let part of pathnameParts) {
412
- if (part === '') {
413
- continue;
414
- }
415
- const optional = part.startsWith(':');
416
- const currentPart = optional ? part.slice(1) : part;
417
- const isCatchAll = currentPart.startsWith('*') && currentPart.endsWith('*');
418
- const isDynamic = currentPart.startsWith('[') && currentPart.endsWith(']');
419
- let matcher;
420
- if (isCatchAll) {
421
- matcher = '[^/]+(?:/[^/]+)*';
422
- }
423
- else if (isDynamic) {
424
- matcher = '[^/]+';
425
- }
426
- else {
427
- matcher = escapeRegExp(currentPart);
428
- }
429
- if (isCatchAll || isDynamic) {
430
- const paramName = currentPart.slice(1, -1);
431
- matcher = `(?<${paramName}>${matcher})`;
432
- }
433
- let sep;
434
- if (hasPart) {
435
- sep = previousCanSkip ? '/?' : '/';
436
- }
437
- else {
438
- sep = '';
439
- }
440
- let addition = sep + matcher;
441
- if (optional) {
442
- if (hasPart || sep !== '') {
443
- addition = '(?:' + sep + matcher + ')?';
444
- }
445
- else {
446
- addition = '(?:' + matcher + ')?';
447
- }
448
- previousCanSkip = true;
449
- }
450
- else {
451
- previousCanSkip = false;
452
- }
453
- patternRegex += addition;
454
- hasPart = true;
455
- }
456
- if (patternRegex === '^/') {
457
- patternRegex = '^/?';
458
- }
459
- patternRegex += '$';
460
- return patternRegex;
755
+ }
756
+ try {
757
+ await lazyBuildStaticPage(compiled, matchedRoute, params, pathname, req, res);
758
+ } catch (e) {
759
+ respondWithStatusCode(500, pathname, {}, req, res);
760
+ if (isRichError(e)) throw e;
761
+ throw richError({
762
+ title: "Failed to lazy build a static page.",
763
+ cause: e,
764
+ doShowStack: true
765
+ });
766
+ }
767
+ return;
768
+ }
769
+ const pageCached = staticCache.get(resolveStaticIndexKey(matchedRoute.pathname));
770
+ if (pageCached) {
771
+ serveCachedFile(req, res, pageCached);
772
+ return;
773
+ }
774
+ await respondWithStatusCode(404, pathname, params, req, res);
775
+ }).catch((e) => {
776
+ if (isRichError(e)) {
777
+ printError(e);
778
+ return;
779
+ }
780
+ printError(richError({
781
+ title: "Failed to run request.",
782
+ cause: e,
783
+ doShowStack: true
784
+ }));
785
+ });
461
786
  }
462
- /**
463
- * Find a pathname in a given set of pathnames that use the Elegance routing convention - that matches.
464
- *
465
- * For example, for an input of `pathname="/recipes/cake"` `allPatterns=[{ pathname: "/recipes/[name]", pathnameParts: ["recipes", "[name]"]}]`
466
- *
467
- * You'd get: { matchedPathname: "/recipes/[name]", params: { name: "cake" } }.
468
- *
469
- * @param pathname The pathname to find a match for.
470
- * @param allPatterns Patterns to match against, use getPathPattern to generate.
471
- * @returns A hit with params, or undefined matchedPathname if none.
472
- */
473
- function matchPathnameToPathParts(pathname, allPatterns) {
474
- const last = pathname.split('/').pop();
475
- if (last.includes('.')) {
476
- return null;
477
- }
478
- const candidates = [];
479
- for (const pattern of allPatterns) {
480
- const patternParts = pattern.pathnameParts;
481
- const regexStr = buildRegexStrFromParts(patternParts);
482
- const regex = new RegExp(regexStr);
483
- const match = pathname.match(regex);
484
- if (match) {
485
- const getBasePart = (p) => p.startsWith(':') ? p.slice(1) : p;
486
- const isDynamicPart = (p) => p.startsWith(':') || p.startsWith('[') || p.startsWith('*');
487
- const fixedCount = patternParts.filter(p => p !== '' && !isDynamicPart(p)).length;
488
- const dynamicSingleCount = patternParts.filter(p => {
489
- const pp = getBasePart(p);
490
- return pp.startsWith('[') && pp.endsWith(']');
491
- }).length;
492
- const catchallCount = patternParts.filter(p => {
493
- const pp = getBasePart(p);
494
- return pp.startsWith('*') && pp.endsWith('*');
495
- }).length;
496
- const optionalCount = patternParts.filter(p => p.startsWith(':')).length;
497
- const totalDynamic = dynamicSingleCount + catchallCount;
498
- candidates.push({ pattern, fixedCount, dynamicSingleCount, catchallCount, optionalCount, totalDynamic, match });
499
- }
500
- }
501
- if (candidates.length === 0) {
502
- return null;
503
- }
504
- candidates.sort((a, b) => {
505
- if (a.fixedCount !== b.fixedCount) {
506
- return b.fixedCount - a.fixedCount;
507
- }
508
- if (a.totalDynamic !== b.totalDynamic) {
509
- return a.totalDynamic - b.totalDynamic;
510
- }
511
- if (a.catchallCount !== b.catchallCount) {
512
- return a.catchallCount - b.catchallCount;
513
- }
514
- if (a.optionalCount !== b.optionalCount) {
515
- return a.optionalCount - b.optionalCount;
516
- }
517
- return 0;
787
+ async function startMainServer() {
788
+ const port = serverOptions.port;
789
+ if (!IS_DEV) {
790
+ return new Promise((resolve, reject) => {
791
+ const srv = createServer(handleRequest);
792
+ srv.keepAliveTimeout = 65e3;
793
+ srv.headersTimeout = 66e3;
794
+ srv.once("error", (err) => {
795
+ if (err.code === "EADDRINUSE") {
796
+ printError(richError({
797
+ title: "Busy Port",
798
+ cause: `The port ${port} is already hogged by another process. Please kill the process hogging it, or change the port.`,
799
+ doShowStack: true
800
+ }));
801
+ process.exit(1);
802
+ }
803
+ reject(err);
804
+ });
805
+ srv.listen(port, "0.0.0.0", () => {
806
+ logger.success(`Live at: http://localhost:${port}`);
807
+ resolve(srv);
808
+ });
518
809
  });
519
- const best = candidates[0];
520
- return { matchedPathname: best.pattern.pathname, params: best.match.groups || {} };
521
- }
522
- /**
523
- * Starts the Elegance server and distributes the DIST directory to the public.
524
- * If hot-reloading is enabled, this also tells the clients to refresh the page.
525
- */
526
- async function serveProject(startupServerOptions) {
527
- serverOptions = startupServerOptions;
528
- if (serverOptions.base && serverOptions.base.startsWith("/") === false) {
529
- throw new Error("Failed to serve the Elegance project, the `base` option in the startUpServerOptions must start with a / in order to be a valid pathname. Currently, it is:" + serverOptions.base);
530
- }
531
- await gatherMiddleware();
532
- await gatherAPIRoutes();
533
- let port = serverOptions.port ?? 3000;
534
- const server = createServer(requestHandler);
535
- /** Prefer to sacrifice port desireability in-exchange for getting the thing running */
536
- server.on("error", (error) => {
537
- if (error.code === "EADDRINUSE") {
538
- setTimeout(() => {
539
- formattedLog(LogLevel.WARN, `${port} was not available, trying port ${port + 1}..`);
540
- port += 1;
541
- server.listen(port);
542
- }, 500);
543
- }
544
- });
545
- server.listen({ port: serverOptions.port, hostname: serverOptions.hostname, }, () => {
546
- if (compilerOptions.doHotReload) {
547
- process.send?.(JSON.stringify({ message: "hot-reload-finish" }));
548
- }
549
- formattedLog(LogLevel.INFO, `Website Live at: http://${serverOptions.hostname}:${port}/`);
550
- });
551
- return {
552
- port,
553
- };
554
- }
555
- /** Get the current query as `URLSearchParams` */
556
- function getQuery() {
557
- const store = compilerStore.getStore();
558
- if (!store) {
559
- throw new Error("getQuery() cannot be called outside of a page or layout.");
560
- }
561
- if (!store.req) {
562
- throw new Error("getQuery() cannot be used inside of a static page, since it depends on the *request query*.");
563
- }
564
- if (!store.req.url) {
565
- throw new Error("Invalid req.url");
566
- }
567
- return new URLSearchParams(new URL(`http://${process.env.HOST ?? 'localhost'}${store.req.url}`).searchParams);
568
- }
569
- /** Get the current page's request and response. */
570
- function getRequest() {
571
- const store = compilerStore.getStore();
572
- if (!store) {
573
- throw new Error("getQuery() cannot be called outside of a page or layout.");
574
- }
575
- if (!store.req || !store.res) {
576
- throw new Error("getQuery() cannot be used inside of a static page, since it depends on the *request query*.");
810
+ }
811
+ for (let i = port; i < port + 100; i++) {
812
+ try {
813
+ return await new Promise((resolve, reject) => {
814
+ const srv = createServer(handleRequest);
815
+ srv.keepAliveTimeout = 65e3;
816
+ srv.headersTimeout = 66e3;
817
+ srv.once("error", reject);
818
+ srv.listen(i, "0.0.0.0", () => {
819
+ logger.success(`Live at: http://localhost:${i}`);
820
+ resolve(srv);
821
+ });
822
+ });
823
+ } catch {
577
824
  }
578
- return { req: store.req, res: store.res };
825
+ }
826
+ throw new Error("Could not find an available port");
579
827
  }
580
- /**
581
- * Get the cookies for the current request.
582
- * Requires a dynamic page.
583
- */
584
- function getCookieStore() {
585
- const { req, res } = getRequest();
586
- let cookieMap = null;
587
- const getCookies = () => {
588
- if (cookieMap)
589
- return cookieMap;
590
- cookieMap = new Map();
591
- if (req.headers.cookie) {
592
- req.headers.cookie.split(';').forEach(part => {
593
- const trimmed = part.trim();
594
- if (!trimmed)
595
- return;
596
- const [name, ...valueParts] = trimmed.split('=');
597
- if (name) {
598
- const value = valueParts.join('=').trim();
599
- cookieMap.set(name, decodeURIComponent(value));
600
- }
601
- });
602
- }
603
- return cookieMap;
604
- };
605
- return {
606
- /**
607
- * Get a cookie value by name
608
- */
609
- get(name) {
610
- return getCookies().get(name);
611
- },
612
- /**
613
- * Check if a cookie exists
614
- */
615
- has(name) {
616
- return getCookies().has(name);
617
- },
618
- /**
619
- * Get all cookies as a plain object
620
- */
621
- getAll() {
622
- return Object.fromEntries(getCookies());
623
- },
624
- /**
625
- * Set a cookie
626
- *
627
- * @param name Cookie name
628
- * @param value Cookie value
629
- * @param options Optional cookie attributes
630
- */
631
- set(name, value, options = {}) {
632
- let cookieStr = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
633
- if (options.maxAge !== undefined) {
634
- cookieStr += `; Max-Age=${Math.floor(options.maxAge)}`;
635
- }
636
- if (options.expires) {
637
- cookieStr += `; Expires=${options.expires.toUTCString()}`;
638
- }
639
- if (options.path) {
640
- cookieStr += `; Path=${options.path}`;
641
- }
642
- if (options.domain) {
643
- cookieStr += `; Domain=${options.domain}`;
644
- }
645
- if (options.secure) {
646
- cookieStr += `; Secure`;
647
- }
648
- if (options.httpOnly) {
649
- cookieStr += `; HttpOnly`;
650
- }
651
- if (options.sameSite) {
652
- cookieStr += `; SameSite=${options.sameSite}`;
653
- }
654
- const existing = res.getHeader('Set-Cookie');
655
- if (existing) {
656
- if (Array.isArray(existing)) {
657
- res.setHeader('Set-Cookie', [...existing, cookieStr]);
658
- }
659
- else {
660
- res.setHeader('Set-Cookie', [existing, cookieStr]);
661
- }
662
- }
663
- else {
664
- res.setHeader('Set-Cookie', cookieStr);
665
- }
666
- },
667
- /**
668
- * Delete a cookie (sets it to expire immediately)
669
- */
670
- delete(name, path = '/', domain) {
671
- this.set(name, '', {
672
- maxAge: 0,
673
- expires: new Date(0),
674
- path,
675
- domain,
676
- });
677
- },
678
- };
828
+ async function serve() {
829
+ serverOptions = await loadServerOptions();
830
+ const manifest = JSON.parse(
831
+ await readFile(join(OUT_DIR, "paths.json"), "utf-8")
832
+ );
833
+ const routes = await warmDynamicCaches(manifest);
834
+ staticRouteMap = routes.staticRouteMap;
835
+ paramRoutes = routes.paramRoutes;
836
+ await Promise.all([
837
+ primeStaticCache(),
838
+ serverOptions.serveAPI ? loadApiRoutes(manifest) : Promise.resolve(),
839
+ loadMiddlewareMap(manifest),
840
+ initSecurityHeaders()
841
+ ]);
842
+ await startMainServer();
843
+ if (process.send) process.send({ type: "ready" });
679
844
  }
680
- function redirect(location, statusCode = 302) {
681
- const { res } = getRequest();
682
- res.statusCode = statusCode;
683
- res.setHeader("Location", location);
684
- res.end();
685
- }
686
- const respondWith = {
687
- async notFound() {
688
- const { req, res } = getRequest();
689
- const url = new URL(`http://${process.env.HOST ?? 'localhost'}${req.url}`);
690
- const pathname = sanitizePathname(url.pathname);
691
- await respondWithStatusCode(req, res, pathname, 404, "Not found.");
692
- },
693
- async notAuthorized() {
694
- const { req, res } = getRequest();
695
- const url = new URL(`http://${process.env.HOST ?? 'localhost'}${req.url}`);
696
- const pathname = sanitizePathname(url.pathname);
697
- await respondWithStatusCode(req, res, pathname, 401, "Not authorized.");
698
- },
699
- async forbidden() {
700
- const { req, res } = getRequest();
701
- const url = new URL(`http://${process.env.HOST ?? 'localhost'}${req.url}`);
702
- const pathname = sanitizePathname(url.pathname);
703
- await respondWithStatusCode(req, res, pathname, 403, "Forbidden.");
704
- },
705
- async internalError() {
706
- const { req, res } = getRequest();
707
- const url = new URL(`http://${process.env.HOST ?? 'localhost'}${req.url}`);
708
- const pathname = sanitizePathname(url.pathname);
709
- await respondWithStatusCode(req, res, pathname, 500, "Internal server error.");
710
- }
845
+ export {
846
+ aotStaticCache,
847
+ apiRoutesCache,
848
+ compileRouteMatcher,
849
+ dynamicModuleCache,
850
+ encCache,
851
+ loadApiRoutes,
852
+ loadMiddlewareMap,
853
+ middlewareChainCache,
854
+ middlewareMapCache,
855
+ paramRoutes,
856
+ primeStaticCache,
857
+ serve,
858
+ staticCache,
859
+ staticRouteMap,
860
+ statusCodePageCache,
861
+ warmDynamicCaches
711
862
  };
712
- export { serveProject, getQuery, getRequest, getCookieStore, redirect, respondWith };