elegance-js 2.1.23 → 2.1.26
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/client/effect.d.ts +27 -0
- package/dist/client/effect.js +37 -0
- package/dist/client/eventListener.d.ts +39 -0
- package/dist/client/eventListener.js +52 -0
- package/dist/client/loadHook.d.ts +34 -0
- package/dist/client/loadHook.js +52 -0
- package/dist/client/observer.d.ts +36 -0
- package/dist/client/observer.js +66 -0
- package/dist/client/runtime.d.ts +105 -0
- package/dist/client/runtime.js +620 -0
- package/dist/client/state.d.ts +40 -0
- package/dist/client/state.js +110 -0
- package/dist/compilation/compiler.d.ts +155 -0
- package/dist/compilation/compiler.js +1153 -0
- package/dist/components/ClientComponent.d.ts +22 -0
- package/dist/components/ClientComponent.js +55 -0
- package/dist/components/Link.d.ts +16 -1
- package/dist/components/Link.js +22 -0
- package/dist/components/Portal.d.ts +2 -0
- package/dist/components/Portal.js +2 -0
- package/dist/elements/element.d.ts +87 -0
- package/dist/elements/element.js +33 -0
- package/dist/elements/element_list.d.ts +7 -0
- package/dist/elements/element_list.js +65 -0
- package/dist/elements/raw.d.ts +14 -0
- package/dist/elements/raw.js +78 -0
- package/dist/elements/specific_props.d.ts +750 -0
- package/dist/global.d.ts +221 -327
- package/dist/index.d.ts +15 -3
- package/dist/index.js +11 -0
- package/dist/server/layout.d.ts +34 -3
- package/dist/server/layout.js +6 -0
- package/dist/server/log.d.ts +12 -0
- package/dist/server/log.js +64 -0
- package/dist/server/page.d.ts +32 -0
- package/dist/server/page.js +6 -0
- package/dist/server/runtime.d.ts +6 -0
- package/dist/server/runtime.js +72 -0
- package/dist/server/server.d.ts +103 -11
- package/dist/server/server.js +709 -0
- package/package.json +13 -13
- package/scripts/bootstrap.js +37 -273
- package/scripts/bootstrap_files/elegance.txt +40 -0
- package/scripts/bootstrap_files/index.txt +3 -0
- package/scripts/bootstrap_files/layout.txt +46 -0
- package/scripts/bootstrap_files/middleware.txt +18 -0
- package/scripts/bootstrap_files/page.txt +123 -0
- package/scripts/bootstrap_files/route.txt +6 -0
- package/scripts/elegance_dev.ts +40 -0
- package/scripts/elegance_prod.ts +40 -0
- package/scripts/elegance_static.ts +24 -0
- package/scripts/prod.js +9 -26
- package/scripts/run.js +13 -0
- package/scripts/static.js +13 -0
- package/dist/build.d.ts +0 -2
- package/dist/build.mjs +0 -202
- package/dist/client/client.d.ts +0 -1
- package/dist/client/client.mjs +0 -574
- package/dist/client/processPageElements.d.ts +0 -1
- package/dist/client/processPageElements.mjs +0 -117
- package/dist/client/render.d.ts +0 -1
- package/dist/client/render.mjs +0 -40
- package/dist/client/watcher.d.ts +0 -1
- package/dist/client/watcher.mjs +0 -26
- package/dist/compilation/compilation.d.ts +0 -139
- package/dist/compilation/compilation.mjs +0 -751
- package/dist/compilation/compiler_process.d.ts +0 -3
- package/dist/compilation/compiler_process.mjs +0 -102
- package/dist/compilation/dynamic_compiler.d.ts +0 -10
- package/dist/compilation/dynamic_compiler.mjs +0 -93
- package/dist/compile_docs.mjs +0 -34
- package/dist/components/Link.mjs +0 -65
- package/dist/global.mjs +0 -0
- package/dist/helpers/ObjectAttributeType.d.ts +0 -7
- package/dist/helpers/ObjectAttributeType.mjs +0 -11
- package/dist/helpers/camelToKebab.d.ts +0 -1
- package/dist/helpers/camelToKebab.mjs +0 -6
- package/dist/index.mjs +0 -3
- package/dist/internal/deprecate.d.ts +0 -1
- package/dist/internal/deprecate.mjs +0 -7
- package/dist/log.d.ts +0 -10
- package/dist/log.mjs +0 -38
- package/dist/server/generateHTMLTemplate.d.ts +0 -12
- package/dist/server/generateHTMLTemplate.mjs +0 -41
- package/dist/server/layout.mjs +0 -19
- package/dist/server/loadHook.d.ts +0 -30
- package/dist/server/loadHook.mjs +0 -50
- package/dist/server/observe.d.ts +0 -19
- package/dist/server/observe.mjs +0 -16
- package/dist/server/render.d.ts +0 -5
- package/dist/server/render.mjs +0 -61
- package/dist/server/server.mjs +0 -429
- package/dist/server/state.d.ts +0 -61
- package/dist/server/state.mjs +0 -146
- package/dist/shared/bindServerElements.mjs +0 -3
- package/dist/shared/serverElements.d.ts +0 -11
- package/dist/shared/serverElements.mjs +0 -164
- package/scripts/dev.js +0 -33
- package/scripts/export.js +0 -20
- package/scripts/ts-arc-dev.js +0 -9
- package/scripts/ts-arc-prod.js +0 -9
- /package/dist/{compile_docs.d.ts → elements/specific_props.js} +0 -0
- /package/dist/{shared/bindServerElements.d.ts → global.js} +0 -0
|
@@ -0,0 +1,709 @@
|
|
|
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
|
+
const gzipAsync = promisify(zlib.gzip);
|
|
15
|
+
function removePrefix(str, prefix) {
|
|
16
|
+
return str.startsWith(prefix) ? str.slice(prefix.length) : str;
|
|
17
|
+
}
|
|
18
|
+
let serverOptions;
|
|
19
|
+
const allAPIRoutes = new Map();
|
|
20
|
+
async function gatherAPIRoutes() {
|
|
21
|
+
await walkDirectory(compilerOptions.pagesDirectory, async (file) => {
|
|
22
|
+
if (file.name !== "route.ts")
|
|
23
|
+
return;
|
|
24
|
+
const pathname = sanitizePathname(relative(compilerOptions.pagesDirectory, file.parentPath));
|
|
25
|
+
const fullPath = join(file.parentPath, file.name);
|
|
26
|
+
const { POST, GET, PUT, DELETE, OPTIONS } = await import("file://" + fullPath);
|
|
27
|
+
const methods = { POST, GET, PUT, DELETE, OPTIONS };
|
|
28
|
+
for (const [name, method] of Object.entries(methods)) {
|
|
29
|
+
if (method && typeof method !== "function") {
|
|
30
|
+
throw new Error(`In file: "${fullPath}":\nThe export ${method} is not of type "function". Got: ${typeof method}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const apiRouteInformation = {
|
|
34
|
+
exports: {
|
|
35
|
+
methods,
|
|
36
|
+
},
|
|
37
|
+
modulePath: fullPath,
|
|
38
|
+
pathname: pathname,
|
|
39
|
+
};
|
|
40
|
+
allAPIRoutes.set(pathname, apiRouteInformation);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function handleAPIRequest(req, res, pathname) {
|
|
44
|
+
const route = allAPIRoutes.get(pathname);
|
|
45
|
+
if (!route) {
|
|
46
|
+
res.statusCode = 404;
|
|
47
|
+
await sendResponse(req, res, "Route does not exist.");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!req.method) {
|
|
51
|
+
res.statusCode = 400;
|
|
52
|
+
await sendResponse(req, res, "Bad request");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const method = route.exports.methods[req.method];
|
|
56
|
+
if (!method) {
|
|
57
|
+
res.statusCode = 405;
|
|
58
|
+
await sendResponse(req, res, "Method not allowed");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
method(req, res);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Go through a directory, including all it's subdirectories,
|
|
65
|
+
* and call callback() for each file.
|
|
66
|
+
*/
|
|
67
|
+
async function walkDirectory(fullPath, callback) {
|
|
68
|
+
const stack = [];
|
|
69
|
+
stack.push(...readdirSync(fullPath, { withFileTypes: true, }));
|
|
70
|
+
while (true) {
|
|
71
|
+
const entry = stack.pop();
|
|
72
|
+
if (!entry)
|
|
73
|
+
break;
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
const fullPath = join(entry.parentPath, entry.name);
|
|
76
|
+
stack.push(...readdirSync(fullPath, { withFileTypes: true, }));
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!entry.isFile())
|
|
80
|
+
continue;
|
|
81
|
+
await callback(entry);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function safePercentDecode(input) {
|
|
85
|
+
return input.replace(/%[0-9A-Fa-f]{2}/g, (m) => String.fromCharCode(parseInt(m.slice(1), 16)));
|
|
86
|
+
}
|
|
87
|
+
function sanitizePathname(pathname = "") {
|
|
88
|
+
if (!pathname)
|
|
89
|
+
return "/";
|
|
90
|
+
pathname = safePercentDecode(pathname);
|
|
91
|
+
pathname = "/" + pathname;
|
|
92
|
+
pathname = pathname.replace(/\/+/g, "/");
|
|
93
|
+
const segments = pathname.split("/");
|
|
94
|
+
const resolved = [];
|
|
95
|
+
for (const segment of segments) {
|
|
96
|
+
if (!segment || segment === ".")
|
|
97
|
+
continue;
|
|
98
|
+
if (segment === "..") {
|
|
99
|
+
resolved.pop();
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
resolved.push(segment);
|
|
103
|
+
}
|
|
104
|
+
const encoded = resolved.map((s) => encodeURIComponent(s));
|
|
105
|
+
return "/" + encoded.join("/");
|
|
106
|
+
}
|
|
107
|
+
function getStatusCodePage(statusCode, pathname) {
|
|
108
|
+
const pages = serverOptions.allStatusCodePages;
|
|
109
|
+
let currentPath = pathname;
|
|
110
|
+
if (!currentPath.startsWith("/")) {
|
|
111
|
+
currentPath = "/" + currentPath;
|
|
112
|
+
}
|
|
113
|
+
while (true) {
|
|
114
|
+
let candidate;
|
|
115
|
+
if (currentPath === "/") {
|
|
116
|
+
candidate = `/${statusCode}`;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
candidate = `${currentPath.replace(/\/$/, "")}/${statusCode}`;
|
|
120
|
+
}
|
|
121
|
+
const pageInfo = pages.get(candidate);
|
|
122
|
+
if (pageInfo) {
|
|
123
|
+
pageInfo.pathname = pathname;
|
|
124
|
+
return pageInfo;
|
|
125
|
+
}
|
|
126
|
+
if (currentPath === "/") {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
const lastSlash = currentPath.lastIndexOf("/");
|
|
130
|
+
if (lastSlash <= 0) {
|
|
131
|
+
currentPath = "/";
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
currentPath = currentPath.slice(0, lastSlash);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function respondWithStatusCodePage(req, res, pathname, statusCode, message) {
|
|
139
|
+
const statusCodePage = getStatusCodePage(statusCode, pathname);
|
|
140
|
+
if (!statusCodePage) {
|
|
141
|
+
res.statusCode = statusCode;
|
|
142
|
+
await sendResponse(req, res, message);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const compiledPage = await compilePage(serverOptions.allLayouts, statusCodePage, { req, res });
|
|
146
|
+
res.statusCode = 200;
|
|
147
|
+
await sendResponse(req, res, compiledPage.pageHTML, "text/html");
|
|
148
|
+
}
|
|
149
|
+
async function respondWithStatusCode(req, res, pathname, statusCode, message) {
|
|
150
|
+
if (serverOptions.allowStatusCodePages === true) {
|
|
151
|
+
return respondWithStatusCodePage(req, res, pathname, statusCode, message);
|
|
152
|
+
}
|
|
153
|
+
res.statusCode = statusCode;
|
|
154
|
+
await sendResponse(req, res, message);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Ensure a given pathname is safe, and does not escape the root directory.
|
|
158
|
+
* @param userInputPath The path to turn into a safe path
|
|
159
|
+
* @returns A safe path, or null if the path was not safe, or does not exist.
|
|
160
|
+
*/
|
|
161
|
+
async function getSafePath(userInputPath) {
|
|
162
|
+
const rootDirectory = resolve(join(compilerOptions.outputDirectory, "DIST"));
|
|
163
|
+
const decodedPath = decodeURIComponent(userInputPath);
|
|
164
|
+
const normalizedPath = normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, '');
|
|
165
|
+
const finalPath = join(rootDirectory, normalizedPath);
|
|
166
|
+
const resolvedFinalPath = resolve(finalPath);
|
|
167
|
+
if (!resolvedFinalPath.startsWith(rootDirectory) || existsSync(resolvedFinalPath) === false) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return resolvedFinalPath;
|
|
171
|
+
}
|
|
172
|
+
async function handlePageRequest(req, res, pathname, pageInformation, matchHit) {
|
|
173
|
+
if (pageInformation.exports.isDynamic) {
|
|
174
|
+
if (serverOptions.allowDynamic === false) {
|
|
175
|
+
return respondWithStatusCode(req, res, pathname, 404, "Page not found.");
|
|
176
|
+
}
|
|
177
|
+
const informationClone = {
|
|
178
|
+
...pageInformation,
|
|
179
|
+
};
|
|
180
|
+
informationClone.pathname = pathname;
|
|
181
|
+
const result = await compilePage(serverOptions.allLayouts, informationClone, { req, res }, matchHit.params);
|
|
182
|
+
if (res.writableEnded || res.headersSent)
|
|
183
|
+
return;
|
|
184
|
+
res.statusCode = 200;
|
|
185
|
+
await sendResponse(req, res, result.pageHTML, "text/html");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const { pageHTML } = serverOptions.builtStaticPages.get(pageInformation.pathname);
|
|
189
|
+
res.statusCode = 200;
|
|
190
|
+
await sendResponse(req, res, pageHTML, "text/html");
|
|
191
|
+
}
|
|
192
|
+
const mimeByExt = {
|
|
193
|
+
".html": "text/html",
|
|
194
|
+
".htm": "text/html",
|
|
195
|
+
".css": "text/css",
|
|
196
|
+
".js": "text/javascript",
|
|
197
|
+
".mjs": "text/javascript",
|
|
198
|
+
".json": "application/json",
|
|
199
|
+
".png": "image/png",
|
|
200
|
+
".jpg": "image/jpeg",
|
|
201
|
+
".jpeg": "image/jpeg",
|
|
202
|
+
".gif": "image/gif",
|
|
203
|
+
".svg": "image/svg+xml",
|
|
204
|
+
".txt": "text/plain",
|
|
205
|
+
".mp4": "video/mp4",
|
|
206
|
+
".webm": "video/webm",
|
|
207
|
+
".mkv": "video/x-matroska",
|
|
208
|
+
".avi": "video/x-msvideo",
|
|
209
|
+
".mov": "video/quicktime",
|
|
210
|
+
".mp3": "audio/mpeg",
|
|
211
|
+
};
|
|
212
|
+
function isCompressible(mime) {
|
|
213
|
+
return mime.startsWith('text/') ||
|
|
214
|
+
mime === 'application/javascript' ||
|
|
215
|
+
mime === 'application/json' ||
|
|
216
|
+
mime === 'image/svg+xml';
|
|
217
|
+
}
|
|
218
|
+
async function handleFileRequest(req, res, pathname) {
|
|
219
|
+
const safePath = await getSafePath(pathname);
|
|
220
|
+
if (!safePath) {
|
|
221
|
+
return respondWithStatusCode(req, res, pathname, 404, "File not found.");
|
|
222
|
+
}
|
|
223
|
+
const stats = statSync(safePath);
|
|
224
|
+
if (stats.isDirectory()) {
|
|
225
|
+
return respondWithStatusCode(req, res, pathname, 404, "File not found.");
|
|
226
|
+
}
|
|
227
|
+
const fileSize = stats.size;
|
|
228
|
+
const ext = safePath.slice(safePath.lastIndexOf(".")).toLowerCase();
|
|
229
|
+
const mime = mimeByExt[ext] ?? "application/octet-stream";
|
|
230
|
+
const acceptEncoding = req.headers["accept-encoding"] || "";
|
|
231
|
+
const rangeHeader = req.headers.range;
|
|
232
|
+
if (!rangeHeader) {
|
|
233
|
+
const useGzip = acceptEncoding.includes('gzip') && isCompressible(mime);
|
|
234
|
+
const head = {
|
|
235
|
+
'Content-Type': mime,
|
|
236
|
+
'Accept-Ranges': 'bytes',
|
|
237
|
+
};
|
|
238
|
+
if (!useGzip) {
|
|
239
|
+
head['Content-Length'] = fileSize;
|
|
240
|
+
}
|
|
241
|
+
if (useGzip) {
|
|
242
|
+
head['Content-Encoding'] = 'gzip';
|
|
243
|
+
head['Vary'] = 'Accept-Encoding';
|
|
244
|
+
}
|
|
245
|
+
res.writeHead(200, head);
|
|
246
|
+
const stream = createReadStream(safePath);
|
|
247
|
+
if (useGzip) {
|
|
248
|
+
const gzip = zlib.createGzip();
|
|
249
|
+
stream.pipe(gzip).pipe(res);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
stream.pipe(res);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const ranges = rangeHeader.replace(/bytes=/, '').split('-');
|
|
257
|
+
let start = parseInt(ranges[0], 10);
|
|
258
|
+
let end = ranges[1] ? parseInt(ranges[1], 10) : fileSize - 1;
|
|
259
|
+
if (isNaN(start))
|
|
260
|
+
start = 0;
|
|
261
|
+
if (isNaN(end) || end >= fileSize)
|
|
262
|
+
end = fileSize - 1;
|
|
263
|
+
if (start >= fileSize || start > end) {
|
|
264
|
+
res.writeHead(416, {
|
|
265
|
+
'Content-Range': `bytes */${fileSize}`,
|
|
266
|
+
});
|
|
267
|
+
res.end();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const contentLength = end - start + 1;
|
|
271
|
+
const headers = {
|
|
272
|
+
'Content-Type': mime,
|
|
273
|
+
'Accept-Ranges': 'bytes',
|
|
274
|
+
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
|
275
|
+
'Content-Length': contentLength,
|
|
276
|
+
};
|
|
277
|
+
res.writeHead(206, headers);
|
|
278
|
+
const stream = createReadStream(safePath, { start, end });
|
|
279
|
+
stream.pipe(res);
|
|
280
|
+
}
|
|
281
|
+
function getPathSubparts(path) {
|
|
282
|
+
const rawParts = path.split('/').filter(Boolean);
|
|
283
|
+
const parts = [...rawParts];
|
|
284
|
+
if (parts.length > 0 && parts[parts.length - 1].includes('.')) {
|
|
285
|
+
parts.pop();
|
|
286
|
+
}
|
|
287
|
+
const result = ['/'];
|
|
288
|
+
let current = '';
|
|
289
|
+
for (const part of parts) {
|
|
290
|
+
current += '/' + part;
|
|
291
|
+
result.push(current);
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
const allMiddleware = new Map();
|
|
296
|
+
async function gatherMiddleware() {
|
|
297
|
+
await walkDirectory(compilerOptions.pagesDirectory, async (file) => {
|
|
298
|
+
if (file.name !== "middleware.ts")
|
|
299
|
+
return;
|
|
300
|
+
const pathname = sanitizePathname(relative(compilerOptions.pagesDirectory, file.parentPath));
|
|
301
|
+
const fullPath = join(file.parentPath, file.name);
|
|
302
|
+
const { middleware } = await import("file://" + fullPath);
|
|
303
|
+
if (!middleware || typeof middleware !== "function") {
|
|
304
|
+
throw new Error(`In file: "${fullPath}":\nThe export middleware is not of type "function". Got: ${typeof middleware}`);
|
|
305
|
+
}
|
|
306
|
+
const middlewareInformation = {
|
|
307
|
+
exports: {
|
|
308
|
+
middleware,
|
|
309
|
+
},
|
|
310
|
+
modulePath: fullPath,
|
|
311
|
+
pathname: pathname,
|
|
312
|
+
};
|
|
313
|
+
allMiddleware.set(pathname, middlewareInformation);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async function runMiddleware(req, res, pathname) {
|
|
317
|
+
const parts = getPathSubparts(pathname);
|
|
318
|
+
const middlewares = [];
|
|
319
|
+
for (const part of parts) {
|
|
320
|
+
if (allMiddleware.has(part) === false) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
middlewares.push(allMiddleware.get(part));
|
|
324
|
+
}
|
|
325
|
+
if (middlewares.length < 1)
|
|
326
|
+
return;
|
|
327
|
+
const next = (idx) => {
|
|
328
|
+
if (idx >= middlewares.length) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const middleware = middlewares[idx];
|
|
332
|
+
const localNext = () => {
|
|
333
|
+
next(idx + 1);
|
|
334
|
+
};
|
|
335
|
+
middleware.exports.middleware(req, res, localNext);
|
|
336
|
+
};
|
|
337
|
+
next(0);
|
|
338
|
+
}
|
|
339
|
+
async function sendResponse(req, res, data, contentType = "text/plain") {
|
|
340
|
+
let buffer = typeof data === "string" ? Buffer.from(data) : data;
|
|
341
|
+
const acceptEncoding = req.headers["accept-encoding"] || "";
|
|
342
|
+
if (acceptEncoding.match(/\bgzip\b/)) {
|
|
343
|
+
try {
|
|
344
|
+
buffer = await gzipAsync(buffer);
|
|
345
|
+
res.setHeader("Content-Encoding", "gzip");
|
|
346
|
+
res.setHeader("Vary", "Accept-Encoding");
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
console.error("Gzip compression error:", err);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
res.setHeader("Content-Type", contentType);
|
|
353
|
+
res.setHeader("Content-Length", buffer.length.toString());
|
|
354
|
+
res.end(buffer);
|
|
355
|
+
}
|
|
356
|
+
async function requestHandler(req, res) {
|
|
357
|
+
if (req.method === "OPTIONS") {
|
|
358
|
+
res.writeHead(204, {
|
|
359
|
+
"Allow": "GET,POST,PUT,DELETE,OPTIONS",
|
|
360
|
+
"Access-Control-Allow-Origin": "*",
|
|
361
|
+
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
362
|
+
"Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "*",
|
|
363
|
+
"Access-Control-Max-Age": "86400",
|
|
364
|
+
});
|
|
365
|
+
res.end();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (!req.url) {
|
|
369
|
+
res.statusCode = 400;
|
|
370
|
+
await sendResponse(req, res, "Bad request.");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const url = new URL(`http://${process.env.HOST ?? 'localhost'}${req.url}`);
|
|
374
|
+
if (serverOptions.base && url.pathname.startsWith(serverOptions.base) === false) {
|
|
375
|
+
res.statusCode = 501;
|
|
376
|
+
await sendResponse(req, res, "Path does not start with basename.");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const pathname = sanitizePathname(serverOptions.base ? removePrefix(serverOptions.base, url.pathname) : url.pathname);
|
|
380
|
+
runMiddleware(req, res, pathname);
|
|
381
|
+
if (res.writableEnded)
|
|
382
|
+
return;
|
|
383
|
+
if (pathname.startsWith("/api/")) {
|
|
384
|
+
return handleAPIRequest(req, res, pathname);
|
|
385
|
+
}
|
|
386
|
+
const matchingPage = matchPathnameToPathParts(pathname, [...serverOptions.allPages.values()].map(v => getPathPattern(v)));
|
|
387
|
+
if (!matchingPage) {
|
|
388
|
+
return handleFileRequest(req, res, pathname);
|
|
389
|
+
}
|
|
390
|
+
handlePageRequest(req, res, pathname, serverOptions.allPages.get(matchingPage.matchedPathname), matchingPage);
|
|
391
|
+
}
|
|
392
|
+
function getPathPattern(value) {
|
|
393
|
+
return {
|
|
394
|
+
pathname: value.pathname,
|
|
395
|
+
pathnameParts: value.pathnameParts,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function escapeRegExp(str) {
|
|
399
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Take a set of pathname parts, like ["blog", ":[postId]"], and turn that into a regex string to match a pathname against.
|
|
403
|
+
* @param pathnameParts Parts to turn into a regex string
|
|
404
|
+
* @returns Regex string
|
|
405
|
+
*/
|
|
406
|
+
function buildRegexStrFromParts(pathnameParts) {
|
|
407
|
+
let patternRegex = '^/';
|
|
408
|
+
let hasPart = false;
|
|
409
|
+
let previousCanSkip = false;
|
|
410
|
+
for (let part of pathnameParts) {
|
|
411
|
+
if (part === '') {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const optional = part.startsWith(':');
|
|
415
|
+
const currentPart = optional ? part.slice(1) : part;
|
|
416
|
+
const isCatchAll = currentPart.startsWith('*') && currentPart.endsWith('*');
|
|
417
|
+
const isDynamic = currentPart.startsWith('[') && currentPart.endsWith(']');
|
|
418
|
+
let matcher;
|
|
419
|
+
if (isCatchAll) {
|
|
420
|
+
matcher = '[^/]+(?:/[^/]+)*';
|
|
421
|
+
}
|
|
422
|
+
else if (isDynamic) {
|
|
423
|
+
matcher = '[^/]+';
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
matcher = escapeRegExp(currentPart);
|
|
427
|
+
}
|
|
428
|
+
if (isCatchAll || isDynamic) {
|
|
429
|
+
const paramName = currentPart.slice(1, -1);
|
|
430
|
+
matcher = `(?<${paramName}>${matcher})`;
|
|
431
|
+
}
|
|
432
|
+
let sep;
|
|
433
|
+
if (hasPart) {
|
|
434
|
+
sep = previousCanSkip ? '/?' : '/';
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
sep = '';
|
|
438
|
+
}
|
|
439
|
+
let addition = sep + matcher;
|
|
440
|
+
if (optional) {
|
|
441
|
+
if (hasPart || sep !== '') {
|
|
442
|
+
addition = '(?:' + sep + matcher + ')?';
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
addition = '(?:' + matcher + ')?';
|
|
446
|
+
}
|
|
447
|
+
previousCanSkip = true;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
previousCanSkip = false;
|
|
451
|
+
}
|
|
452
|
+
patternRegex += addition;
|
|
453
|
+
hasPart = true;
|
|
454
|
+
}
|
|
455
|
+
if (patternRegex === '^/') {
|
|
456
|
+
patternRegex = '^/?';
|
|
457
|
+
}
|
|
458
|
+
patternRegex += '$';
|
|
459
|
+
return patternRegex;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Find a pathname in a given set of pathnames that use the Elegance routing convention - that matches.
|
|
463
|
+
*
|
|
464
|
+
* For example, for an input of `pathname="/recipes/cake"` `allPatterns=[{ pathname: "/recipes/[name]", pathnameParts: ["recipes", "[name]"]}]`
|
|
465
|
+
*
|
|
466
|
+
* You'd get: { matchedPathname: "/recipes/[name]", params: { name: "cake" } }.
|
|
467
|
+
*
|
|
468
|
+
* @param pathname The pathname to find a match for.
|
|
469
|
+
* @param allPatterns Patterns to match against, use getPathPattern to generate.
|
|
470
|
+
* @returns A hit with params, or undefined matchedPathname if none.
|
|
471
|
+
*/
|
|
472
|
+
function matchPathnameToPathParts(pathname, allPatterns) {
|
|
473
|
+
const last = pathname.split('/').pop();
|
|
474
|
+
if (last.includes('.')) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
const candidates = [];
|
|
478
|
+
for (const pattern of allPatterns) {
|
|
479
|
+
const patternParts = pattern.pathnameParts;
|
|
480
|
+
const regexStr = buildRegexStrFromParts(patternParts);
|
|
481
|
+
const regex = new RegExp(regexStr);
|
|
482
|
+
const match = pathname.match(regex);
|
|
483
|
+
if (match) {
|
|
484
|
+
const getBasePart = (p) => p.startsWith(':') ? p.slice(1) : p;
|
|
485
|
+
const isDynamicPart = (p) => p.startsWith(':') || p.startsWith('[') || p.startsWith('*');
|
|
486
|
+
const fixedCount = patternParts.filter(p => p !== '' && !isDynamicPart(p)).length;
|
|
487
|
+
const dynamicSingleCount = patternParts.filter(p => {
|
|
488
|
+
const pp = getBasePart(p);
|
|
489
|
+
return pp.startsWith('[') && pp.endsWith(']');
|
|
490
|
+
}).length;
|
|
491
|
+
const catchallCount = patternParts.filter(p => {
|
|
492
|
+
const pp = getBasePart(p);
|
|
493
|
+
return pp.startsWith('*') && pp.endsWith('*');
|
|
494
|
+
}).length;
|
|
495
|
+
const optionalCount = patternParts.filter(p => p.startsWith(':')).length;
|
|
496
|
+
const totalDynamic = dynamicSingleCount + catchallCount;
|
|
497
|
+
candidates.push({ pattern, fixedCount, dynamicSingleCount, catchallCount, optionalCount, totalDynamic, match });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (candidates.length === 0) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
candidates.sort((a, b) => {
|
|
504
|
+
if (a.fixedCount !== b.fixedCount) {
|
|
505
|
+
return b.fixedCount - a.fixedCount;
|
|
506
|
+
}
|
|
507
|
+
if (a.totalDynamic !== b.totalDynamic) {
|
|
508
|
+
return a.totalDynamic - b.totalDynamic;
|
|
509
|
+
}
|
|
510
|
+
if (a.catchallCount !== b.catchallCount) {
|
|
511
|
+
return a.catchallCount - b.catchallCount;
|
|
512
|
+
}
|
|
513
|
+
if (a.optionalCount !== b.optionalCount) {
|
|
514
|
+
return a.optionalCount - b.optionalCount;
|
|
515
|
+
}
|
|
516
|
+
return 0;
|
|
517
|
+
});
|
|
518
|
+
const best = candidates[0];
|
|
519
|
+
return { matchedPathname: best.pattern.pathname, params: best.match.groups || {} };
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Starts the Elegance server and distributes the DIST directory to the public.
|
|
523
|
+
* If hot-reloading is enabled, this also tells the clients to refresh the page.
|
|
524
|
+
*/
|
|
525
|
+
async function serveProject(startupServerOptions) {
|
|
526
|
+
serverOptions = startupServerOptions;
|
|
527
|
+
if (serverOptions.base && serverOptions.base.startsWith("/") === false) {
|
|
528
|
+
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);
|
|
529
|
+
}
|
|
530
|
+
await gatherMiddleware();
|
|
531
|
+
await gatherAPIRoutes();
|
|
532
|
+
let port = serverOptions.port ?? 3000;
|
|
533
|
+
const server = createServer(requestHandler);
|
|
534
|
+
/** Prefer to sacrifice port desireability in-exchange for getting the thing running */
|
|
535
|
+
server.on("error", (error) => {
|
|
536
|
+
if (error.code === "EADDRINUSE") {
|
|
537
|
+
setTimeout(() => {
|
|
538
|
+
port += 1;
|
|
539
|
+
server.listen(port);
|
|
540
|
+
}, 500);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
server.listen({ port: serverOptions.port, hostname: serverOptions.hostname, }, () => {
|
|
544
|
+
if (compilerOptions.doHotReload) {
|
|
545
|
+
process.send?.("hot-reload-finish");
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
return {
|
|
549
|
+
port,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
/** Get the current query as `URLSearchParams` */
|
|
553
|
+
function getQuery() {
|
|
554
|
+
const store = compilerStore.getStore();
|
|
555
|
+
if (!store) {
|
|
556
|
+
throw new Error("getQuery() cannot be called outside of a page or layout.");
|
|
557
|
+
}
|
|
558
|
+
if (!store.req) {
|
|
559
|
+
throw new Error("getQuery() cannot be used inside of a static page, since it depends on the *request query*.");
|
|
560
|
+
}
|
|
561
|
+
if (!store.req.url) {
|
|
562
|
+
throw new Error("Invalid req.url");
|
|
563
|
+
}
|
|
564
|
+
return new URLSearchParams(new URL(`http://${process.env.HOST ?? 'localhost'}${store.req.url}`).searchParams);
|
|
565
|
+
}
|
|
566
|
+
/** Get the current page's request and response. */
|
|
567
|
+
function getRequest() {
|
|
568
|
+
const store = compilerStore.getStore();
|
|
569
|
+
if (!store) {
|
|
570
|
+
throw new Error("getQuery() cannot be called outside of a page or layout.");
|
|
571
|
+
}
|
|
572
|
+
if (!store.req || !store.res) {
|
|
573
|
+
throw new Error("getQuery() cannot be used inside of a static page, since it depends on the *request query*.");
|
|
574
|
+
}
|
|
575
|
+
return { req: store.req, res: store.res };
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Get the cookies for the current request.
|
|
579
|
+
* Requires a dynamic page.
|
|
580
|
+
*/
|
|
581
|
+
function getCookieStore() {
|
|
582
|
+
const { req, res } = getRequest();
|
|
583
|
+
let cookieMap = null;
|
|
584
|
+
const getCookies = () => {
|
|
585
|
+
if (cookieMap)
|
|
586
|
+
return cookieMap;
|
|
587
|
+
cookieMap = new Map();
|
|
588
|
+
if (req.headers.cookie) {
|
|
589
|
+
req.headers.cookie.split(';').forEach(part => {
|
|
590
|
+
const trimmed = part.trim();
|
|
591
|
+
if (!trimmed)
|
|
592
|
+
return;
|
|
593
|
+
const [name, ...valueParts] = trimmed.split('=');
|
|
594
|
+
if (name) {
|
|
595
|
+
const value = valueParts.join('=').trim();
|
|
596
|
+
cookieMap.set(name, decodeURIComponent(value));
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return cookieMap;
|
|
601
|
+
};
|
|
602
|
+
return {
|
|
603
|
+
/**
|
|
604
|
+
* Get a cookie value by name
|
|
605
|
+
*/
|
|
606
|
+
get(name) {
|
|
607
|
+
return getCookies().get(name);
|
|
608
|
+
},
|
|
609
|
+
/**
|
|
610
|
+
* Check if a cookie exists
|
|
611
|
+
*/
|
|
612
|
+
has(name) {
|
|
613
|
+
return getCookies().has(name);
|
|
614
|
+
},
|
|
615
|
+
/**
|
|
616
|
+
* Get all cookies as a plain object
|
|
617
|
+
*/
|
|
618
|
+
getAll() {
|
|
619
|
+
return Object.fromEntries(getCookies());
|
|
620
|
+
},
|
|
621
|
+
/**
|
|
622
|
+
* Set a cookie
|
|
623
|
+
*
|
|
624
|
+
* @param name Cookie name
|
|
625
|
+
* @param value Cookie value
|
|
626
|
+
* @param options Optional cookie attributes
|
|
627
|
+
*/
|
|
628
|
+
set(name, value, options = {}) {
|
|
629
|
+
let cookieStr = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
630
|
+
if (options.maxAge !== undefined) {
|
|
631
|
+
cookieStr += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
632
|
+
}
|
|
633
|
+
if (options.expires) {
|
|
634
|
+
cookieStr += `; Expires=${options.expires.toUTCString()}`;
|
|
635
|
+
}
|
|
636
|
+
if (options.path) {
|
|
637
|
+
cookieStr += `; Path=${options.path}`;
|
|
638
|
+
}
|
|
639
|
+
if (options.domain) {
|
|
640
|
+
cookieStr += `; Domain=${options.domain}`;
|
|
641
|
+
}
|
|
642
|
+
if (options.secure) {
|
|
643
|
+
cookieStr += `; Secure`;
|
|
644
|
+
}
|
|
645
|
+
if (options.httpOnly) {
|
|
646
|
+
cookieStr += `; HttpOnly`;
|
|
647
|
+
}
|
|
648
|
+
if (options.sameSite) {
|
|
649
|
+
cookieStr += `; SameSite=${options.sameSite}`;
|
|
650
|
+
}
|
|
651
|
+
const existing = res.getHeader('Set-Cookie');
|
|
652
|
+
if (existing) {
|
|
653
|
+
if (Array.isArray(existing)) {
|
|
654
|
+
res.setHeader('Set-Cookie', [...existing, cookieStr]);
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
res.setHeader('Set-Cookie', [existing, cookieStr]);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
res.setHeader('Set-Cookie', cookieStr);
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
/**
|
|
665
|
+
* Delete a cookie (sets it to expire immediately)
|
|
666
|
+
*/
|
|
667
|
+
delete(name, path = '/', domain) {
|
|
668
|
+
this.set(name, '', {
|
|
669
|
+
maxAge: 0,
|
|
670
|
+
expires: new Date(0),
|
|
671
|
+
path,
|
|
672
|
+
domain,
|
|
673
|
+
});
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function redirect(location, statusCode = 302) {
|
|
678
|
+
const { res } = getRequest();
|
|
679
|
+
res.statusCode = statusCode;
|
|
680
|
+
res.setHeader("Location", location);
|
|
681
|
+
res.end();
|
|
682
|
+
}
|
|
683
|
+
const respondWith = {
|
|
684
|
+
async notFound() {
|
|
685
|
+
const { req, res } = getRequest();
|
|
686
|
+
const url = new URL(`http://${process.env.HOST ?? 'localhost'}${req.url}`);
|
|
687
|
+
const pathname = sanitizePathname(url.pathname);
|
|
688
|
+
await respondWithStatusCode(req, res, pathname, 404, "Not found.");
|
|
689
|
+
},
|
|
690
|
+
async notAuthorized() {
|
|
691
|
+
const { req, res } = getRequest();
|
|
692
|
+
const url = new URL(`http://${process.env.HOST ?? 'localhost'}${req.url}`);
|
|
693
|
+
const pathname = sanitizePathname(url.pathname);
|
|
694
|
+
await respondWithStatusCode(req, res, pathname, 401, "Not authorized.");
|
|
695
|
+
},
|
|
696
|
+
async forbidden() {
|
|
697
|
+
const { req, res } = getRequest();
|
|
698
|
+
const url = new URL(`http://${process.env.HOST ?? 'localhost'}${req.url}`);
|
|
699
|
+
const pathname = sanitizePathname(url.pathname);
|
|
700
|
+
await respondWithStatusCode(req, res, pathname, 403, "Forbidden.");
|
|
701
|
+
},
|
|
702
|
+
async internalError() {
|
|
703
|
+
const { req, res } = getRequest();
|
|
704
|
+
const url = new URL(`http://${process.env.HOST ?? 'localhost'}${req.url}`);
|
|
705
|
+
const pathname = sanitizePathname(url.pathname);
|
|
706
|
+
await respondWithStatusCode(req, res, pathname, 500, "Internal server error.");
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
export { serveProject, getQuery, getRequest, getCookieStore, redirect, respondWith };
|