@constela/start 0.3.0 → 0.3.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.
- package/dist/chunk-JXIOHPG5.js +399 -0
- package/dist/cli/index.js +30 -6
- package/dist/index.js +12 -395
- package/package.json +1 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// src/router/file-router.ts
|
|
2
|
+
import fg from "fast-glob";
|
|
3
|
+
import { existsSync, statSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
function filePathToPattern(filePath, _routesDir) {
|
|
6
|
+
let normalized = filePath.replace(/\\/g, "/");
|
|
7
|
+
normalized = normalized.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
8
|
+
const segments = normalized.split("/");
|
|
9
|
+
const processedSegments = segments.map((segment) => {
|
|
10
|
+
if (segment.startsWith("[...") && segment.endsWith("]")) {
|
|
11
|
+
return "*";
|
|
12
|
+
}
|
|
13
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
14
|
+
const paramName = segment.slice(1, -1);
|
|
15
|
+
return `:${paramName}`;
|
|
16
|
+
}
|
|
17
|
+
return segment;
|
|
18
|
+
});
|
|
19
|
+
if (processedSegments.at(-1) === "index") {
|
|
20
|
+
processedSegments.pop();
|
|
21
|
+
}
|
|
22
|
+
const path = "/" + processedSegments.join("/");
|
|
23
|
+
if (path === "/") {
|
|
24
|
+
return "/";
|
|
25
|
+
}
|
|
26
|
+
return path.endsWith("/") ? path.slice(0, -1) : path;
|
|
27
|
+
}
|
|
28
|
+
function extractParams(filePath) {
|
|
29
|
+
const params = [];
|
|
30
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
31
|
+
const regex = /\[(?:\.\.\.)?([^\]]+)\]/g;
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = regex.exec(normalized)) !== null) {
|
|
34
|
+
const paramName = match[1];
|
|
35
|
+
if (paramName !== void 0) {
|
|
36
|
+
params.push(paramName);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return params;
|
|
40
|
+
}
|
|
41
|
+
function determineRouteType(filePath) {
|
|
42
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
43
|
+
const fileName = normalized.split("/").pop() ?? "";
|
|
44
|
+
if (fileName.startsWith("_middleware.")) {
|
|
45
|
+
return "middleware";
|
|
46
|
+
}
|
|
47
|
+
if (normalized.startsWith("api/") || normalized.includes("/api/")) {
|
|
48
|
+
return "api";
|
|
49
|
+
}
|
|
50
|
+
return "page";
|
|
51
|
+
}
|
|
52
|
+
function shouldIncludeRoute(filePath) {
|
|
53
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
54
|
+
const fileName = normalized.split("/").pop() ?? "";
|
|
55
|
+
if (fileName.endsWith(".d.ts")) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (fileName.startsWith("_") && !fileName.startsWith("_middleware.")) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
async function scanRoutes(routesDir) {
|
|
64
|
+
if (!existsSync(routesDir)) {
|
|
65
|
+
throw new Error(`Routes directory does not exist: ${routesDir}`);
|
|
66
|
+
}
|
|
67
|
+
const stats = statSync(routesDir);
|
|
68
|
+
if (!stats.isDirectory()) {
|
|
69
|
+
throw new Error(`Routes path is not a directory: ${routesDir}`);
|
|
70
|
+
}
|
|
71
|
+
const files = await fg("**/*.{ts,tsx,js,jsx}", {
|
|
72
|
+
cwd: routesDir,
|
|
73
|
+
ignore: ["node_modules/**", "**/*.d.ts"],
|
|
74
|
+
onlyFiles: true,
|
|
75
|
+
followSymbolicLinks: false
|
|
76
|
+
});
|
|
77
|
+
const routes = [];
|
|
78
|
+
for (const filePath of files) {
|
|
79
|
+
if (!shouldIncludeRoute(filePath)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const route = {
|
|
83
|
+
file: join(routesDir, filePath),
|
|
84
|
+
pattern: filePathToPattern(filePath, routesDir),
|
|
85
|
+
type: determineRouteType(filePath),
|
|
86
|
+
params: extractParams(filePath)
|
|
87
|
+
};
|
|
88
|
+
routes.push(route);
|
|
89
|
+
}
|
|
90
|
+
routes.sort((a, b) => {
|
|
91
|
+
return compareRoutes(a, b);
|
|
92
|
+
});
|
|
93
|
+
return routes;
|
|
94
|
+
}
|
|
95
|
+
function compareRoutes(a, b) {
|
|
96
|
+
if (a.type === "middleware" && b.type !== "middleware") return -1;
|
|
97
|
+
if (b.type === "middleware" && a.type !== "middleware") return 1;
|
|
98
|
+
const segmentsA = a.pattern.split("/").filter(Boolean);
|
|
99
|
+
const segmentsB = b.pattern.split("/").filter(Boolean);
|
|
100
|
+
const minLen = Math.min(segmentsA.length, segmentsB.length);
|
|
101
|
+
for (let i = 0; i < minLen; i++) {
|
|
102
|
+
const segA = segmentsA[i] ?? "";
|
|
103
|
+
const segB = segmentsB[i] ?? "";
|
|
104
|
+
const typeA = getSegmentType(segA);
|
|
105
|
+
const typeB = getSegmentType(segB);
|
|
106
|
+
if (typeA !== typeB) {
|
|
107
|
+
return typeA - typeB;
|
|
108
|
+
}
|
|
109
|
+
if (typeA === 0 && segA !== segB) {
|
|
110
|
+
return segA.localeCompare(segB);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (segmentsA.length !== segmentsB.length) {
|
|
114
|
+
return segmentsA.length - segmentsB.length;
|
|
115
|
+
}
|
|
116
|
+
return a.pattern.localeCompare(b.pattern);
|
|
117
|
+
}
|
|
118
|
+
function getSegmentType(segment) {
|
|
119
|
+
if (segment === "*") return 2;
|
|
120
|
+
if (segment.startsWith(":")) return 1;
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/static/index.ts
|
|
125
|
+
import { extname, join as join2, normalize, resolve } from "path";
|
|
126
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
127
|
+
var MIME_TYPES = {
|
|
128
|
+
// Images
|
|
129
|
+
".ico": "image/x-icon",
|
|
130
|
+
".png": "image/png",
|
|
131
|
+
".jpg": "image/jpeg",
|
|
132
|
+
".jpeg": "image/jpeg",
|
|
133
|
+
".gif": "image/gif",
|
|
134
|
+
".svg": "image/svg+xml",
|
|
135
|
+
".webp": "image/webp",
|
|
136
|
+
// Fonts
|
|
137
|
+
".woff": "font/woff",
|
|
138
|
+
".woff2": "font/woff2",
|
|
139
|
+
".ttf": "font/ttf",
|
|
140
|
+
".otf": "font/otf",
|
|
141
|
+
".eot": "application/vnd.ms-fontobject",
|
|
142
|
+
// Web assets
|
|
143
|
+
".css": "text/css",
|
|
144
|
+
".js": "text/javascript",
|
|
145
|
+
".json": "application/json",
|
|
146
|
+
".txt": "text/plain",
|
|
147
|
+
".html": "text/html",
|
|
148
|
+
".xml": "application/xml",
|
|
149
|
+
// Other
|
|
150
|
+
".pdf": "application/pdf",
|
|
151
|
+
".mp3": "audio/mpeg",
|
|
152
|
+
".mp4": "video/mp4",
|
|
153
|
+
".webm": "video/webm",
|
|
154
|
+
".map": "application/json"
|
|
155
|
+
};
|
|
156
|
+
var DEFAULT_MIME_TYPE = "application/octet-stream";
|
|
157
|
+
function isPathSafe(pathname) {
|
|
158
|
+
if (!pathname) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (!pathname.startsWith("/")) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if (pathname.includes("\\")) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (pathname.includes("//")) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
if (pathname.includes("\0") || pathname.includes("%00")) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
let decoded;
|
|
174
|
+
try {
|
|
175
|
+
decoded = pathname;
|
|
176
|
+
let prevDecoded = "";
|
|
177
|
+
while (decoded !== prevDecoded) {
|
|
178
|
+
prevDecoded = decoded;
|
|
179
|
+
decoded = decodeURIComponent(decoded);
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (decoded.includes("\0")) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (decoded.includes("..")) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const segments = decoded.split("/").filter(Boolean);
|
|
191
|
+
for (const segment of segments) {
|
|
192
|
+
if (segment.startsWith(".")) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
function getMimeType(filePath) {
|
|
199
|
+
const ext = extname(filePath).toLowerCase();
|
|
200
|
+
return MIME_TYPES[ext] ?? DEFAULT_MIME_TYPE;
|
|
201
|
+
}
|
|
202
|
+
function hasObviousAttackPattern(pathname) {
|
|
203
|
+
if (!pathname) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (!pathname.startsWith("/")) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (pathname.includes("\\")) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
if (pathname.includes("//")) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
if (pathname.includes("\0") || pathname.includes("%00")) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
let decoded;
|
|
219
|
+
try {
|
|
220
|
+
decoded = pathname;
|
|
221
|
+
let prevDecoded = "";
|
|
222
|
+
while (decoded !== prevDecoded) {
|
|
223
|
+
prevDecoded = decoded;
|
|
224
|
+
decoded = decodeURIComponent(decoded);
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
if (decoded.includes("\0")) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
const segments = decoded.split("/").filter(Boolean);
|
|
233
|
+
for (const segment of segments) {
|
|
234
|
+
if (segment.startsWith(".") && segment !== "..") {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (decoded.startsWith("/..")) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
function resolveStaticFile(pathname, publicDir) {
|
|
244
|
+
if (hasObviousAttackPattern(pathname)) {
|
|
245
|
+
return {
|
|
246
|
+
exists: false,
|
|
247
|
+
filePath: null,
|
|
248
|
+
mimeType: null,
|
|
249
|
+
error: "path_traversal"
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
let decodedPathname;
|
|
253
|
+
try {
|
|
254
|
+
decodedPathname = decodeURIComponent(pathname);
|
|
255
|
+
} catch {
|
|
256
|
+
return {
|
|
257
|
+
exists: false,
|
|
258
|
+
filePath: null,
|
|
259
|
+
mimeType: null,
|
|
260
|
+
error: "path_traversal"
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const relativePath = decodedPathname.slice(1);
|
|
264
|
+
const resolvedPath = normalize(join2(publicDir, relativePath));
|
|
265
|
+
const absolutePublicDir = resolve(publicDir);
|
|
266
|
+
const publicDirWithSep = absolutePublicDir.endsWith("/") ? absolutePublicDir : absolutePublicDir + "/";
|
|
267
|
+
if (!resolvedPath.startsWith(publicDirWithSep) && resolvedPath !== absolutePublicDir) {
|
|
268
|
+
return {
|
|
269
|
+
exists: false,
|
|
270
|
+
filePath: null,
|
|
271
|
+
mimeType: null,
|
|
272
|
+
error: "outside_public"
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const mimeType = getMimeType(resolvedPath);
|
|
276
|
+
let exists = false;
|
|
277
|
+
if (existsSync2(resolvedPath)) {
|
|
278
|
+
try {
|
|
279
|
+
const stats = statSync2(resolvedPath);
|
|
280
|
+
exists = stats.isFile();
|
|
281
|
+
} catch {
|
|
282
|
+
exists = false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
exists,
|
|
287
|
+
filePath: resolvedPath,
|
|
288
|
+
mimeType
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/dev/server.ts
|
|
293
|
+
import { createServer } from "http";
|
|
294
|
+
import { createReadStream } from "fs";
|
|
295
|
+
import { join as join3 } from "path";
|
|
296
|
+
var DEFAULT_PORT = 3e3;
|
|
297
|
+
var DEFAULT_HOST = "localhost";
|
|
298
|
+
var DEFAULT_PUBLIC_DIR = "public";
|
|
299
|
+
async function createDevServer(options = {}) {
|
|
300
|
+
const {
|
|
301
|
+
port = DEFAULT_PORT,
|
|
302
|
+
host = DEFAULT_HOST,
|
|
303
|
+
routesDir: _routesDir = "src/routes",
|
|
304
|
+
publicDir = join3(process.cwd(), DEFAULT_PUBLIC_DIR)
|
|
305
|
+
} = options;
|
|
306
|
+
let httpServer = null;
|
|
307
|
+
let actualPort = port;
|
|
308
|
+
const devServer = {
|
|
309
|
+
get port() {
|
|
310
|
+
return actualPort;
|
|
311
|
+
},
|
|
312
|
+
async listen() {
|
|
313
|
+
return new Promise((resolve2, reject) => {
|
|
314
|
+
httpServer = createServer((req, res) => {
|
|
315
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
316
|
+
const pathname = url.pathname;
|
|
317
|
+
const staticResult = resolveStaticFile(pathname, publicDir);
|
|
318
|
+
if (staticResult.error === "path_traversal" || staticResult.error === "outside_public") {
|
|
319
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
320
|
+
res.end("Forbidden");
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (staticResult.exists && staticResult.filePath && staticResult.mimeType) {
|
|
324
|
+
res.writeHead(200, { "Content-Type": staticResult.mimeType });
|
|
325
|
+
const stream = createReadStream(staticResult.filePath);
|
|
326
|
+
stream.pipe(res);
|
|
327
|
+
stream.on("error", () => {
|
|
328
|
+
if (!res.headersSent) {
|
|
329
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
330
|
+
}
|
|
331
|
+
res.end("Internal Server Error");
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (staticResult.filePath && !staticResult.exists) {
|
|
336
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
337
|
+
res.end("Not Found");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
341
|
+
res.end("<html><body>Constela Dev Server</body></html>");
|
|
342
|
+
});
|
|
343
|
+
httpServer.on("error", (err) => {
|
|
344
|
+
reject(err);
|
|
345
|
+
});
|
|
346
|
+
httpServer.listen(port, host, () => {
|
|
347
|
+
const address = httpServer?.address();
|
|
348
|
+
if (address) {
|
|
349
|
+
actualPort = address.port;
|
|
350
|
+
}
|
|
351
|
+
resolve2();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
async close() {
|
|
356
|
+
return new Promise((resolve2, reject) => {
|
|
357
|
+
if (!httpServer) {
|
|
358
|
+
resolve2();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
httpServer.close((err) => {
|
|
362
|
+
if (err) {
|
|
363
|
+
reject(err);
|
|
364
|
+
} else {
|
|
365
|
+
httpServer = null;
|
|
366
|
+
resolve2();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
return devServer;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/build/index.ts
|
|
376
|
+
async function build(options) {
|
|
377
|
+
const outDir = options?.outDir ?? "dist";
|
|
378
|
+
const routesDir = options?.routesDir ?? "src/routes";
|
|
379
|
+
let routes = [];
|
|
380
|
+
try {
|
|
381
|
+
const scannedRoutes = await scanRoutes(routesDir);
|
|
382
|
+
routes = scannedRoutes.map((r) => r.pattern);
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
outDir,
|
|
387
|
+
routes
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export {
|
|
392
|
+
filePathToPattern,
|
|
393
|
+
scanRoutes,
|
|
394
|
+
isPathSafe,
|
|
395
|
+
getMimeType,
|
|
396
|
+
resolveStaticFile,
|
|
397
|
+
createDevServer,
|
|
398
|
+
build
|
|
399
|
+
};
|
package/dist/cli/index.js
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
build,
|
|
3
|
+
createDevServer
|
|
4
|
+
} from "../chunk-JXIOHPG5.js";
|
|
5
|
+
|
|
1
6
|
// src/cli/index.ts
|
|
2
7
|
import { Command } from "commander";
|
|
3
8
|
var devHandler = async (options) => {
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
const port = parseInt(options.port, 10);
|
|
10
|
+
const host = options.host ?? "localhost";
|
|
11
|
+
const server = await createDevServer({ port, host });
|
|
12
|
+
await server.listen();
|
|
13
|
+
console.log(`Development server running at http://${host}:${server.port}`);
|
|
14
|
+
process.on("SIGINT", async () => {
|
|
15
|
+
console.log("\nShutting down server...");
|
|
16
|
+
await server.close();
|
|
17
|
+
process.exit(0);
|
|
18
|
+
});
|
|
19
|
+
return { port: server.port };
|
|
6
20
|
};
|
|
7
|
-
var buildHandler = async () => {
|
|
8
|
-
console.log("
|
|
21
|
+
var buildHandler = async (options) => {
|
|
22
|
+
console.log("Building for production...");
|
|
23
|
+
await build(options.outDir ? { outDir: options.outDir } : {});
|
|
24
|
+
console.log("Build complete");
|
|
9
25
|
};
|
|
10
26
|
var startHandler = async (options) => {
|
|
11
|
-
|
|
12
|
-
|
|
27
|
+
const port = parseInt(options.port, 10);
|
|
28
|
+
const server = await createDevServer({ port, host: "0.0.0.0" });
|
|
29
|
+
await server.listen();
|
|
30
|
+
console.log(`Production server running at http://0.0.0.0:${server.port}`);
|
|
31
|
+
process.on("SIGINT", async () => {
|
|
32
|
+
console.log("\nShutting down server...");
|
|
33
|
+
await server.close();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
});
|
|
36
|
+
return { port: server.port };
|
|
13
37
|
};
|
|
14
38
|
function setDevHandler(handler) {
|
|
15
39
|
devHandler = handler;
|
package/dist/index.js
CHANGED
|
@@ -1,404 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
build,
|
|
3
|
+
createDevServer,
|
|
4
|
+
filePathToPattern,
|
|
5
|
+
getMimeType,
|
|
6
|
+
isPathSafe,
|
|
7
|
+
resolveStaticFile,
|
|
8
|
+
scanRoutes
|
|
9
|
+
} from "./chunk-JXIOHPG5.js";
|
|
1
10
|
import {
|
|
2
11
|
generateHydrationScript,
|
|
3
12
|
renderPage,
|
|
4
13
|
wrapHtml
|
|
5
14
|
} from "./chunk-QLDID7EZ.js";
|
|
6
15
|
|
|
7
|
-
// src/router/file-router.ts
|
|
8
|
-
import fg from "fast-glob";
|
|
9
|
-
import { existsSync, statSync } from "fs";
|
|
10
|
-
import { join } from "path";
|
|
11
|
-
function filePathToPattern(filePath, _routesDir) {
|
|
12
|
-
let normalized = filePath.replace(/\\/g, "/");
|
|
13
|
-
normalized = normalized.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
14
|
-
const segments = normalized.split("/");
|
|
15
|
-
const processedSegments = segments.map((segment) => {
|
|
16
|
-
if (segment.startsWith("[...") && segment.endsWith("]")) {
|
|
17
|
-
return "*";
|
|
18
|
-
}
|
|
19
|
-
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
20
|
-
const paramName = segment.slice(1, -1);
|
|
21
|
-
return `:${paramName}`;
|
|
22
|
-
}
|
|
23
|
-
return segment;
|
|
24
|
-
});
|
|
25
|
-
if (processedSegments.at(-1) === "index") {
|
|
26
|
-
processedSegments.pop();
|
|
27
|
-
}
|
|
28
|
-
const path = "/" + processedSegments.join("/");
|
|
29
|
-
if (path === "/") {
|
|
30
|
-
return "/";
|
|
31
|
-
}
|
|
32
|
-
return path.endsWith("/") ? path.slice(0, -1) : path;
|
|
33
|
-
}
|
|
34
|
-
function extractParams(filePath) {
|
|
35
|
-
const params = [];
|
|
36
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
37
|
-
const regex = /\[(?:\.\.\.)?([^\]]+)\]/g;
|
|
38
|
-
let match;
|
|
39
|
-
while ((match = regex.exec(normalized)) !== null) {
|
|
40
|
-
const paramName = match[1];
|
|
41
|
-
if (paramName !== void 0) {
|
|
42
|
-
params.push(paramName);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return params;
|
|
46
|
-
}
|
|
47
|
-
function determineRouteType(filePath) {
|
|
48
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
49
|
-
const fileName = normalized.split("/").pop() ?? "";
|
|
50
|
-
if (fileName.startsWith("_middleware.")) {
|
|
51
|
-
return "middleware";
|
|
52
|
-
}
|
|
53
|
-
if (normalized.startsWith("api/") || normalized.includes("/api/")) {
|
|
54
|
-
return "api";
|
|
55
|
-
}
|
|
56
|
-
return "page";
|
|
57
|
-
}
|
|
58
|
-
function shouldIncludeRoute(filePath) {
|
|
59
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
60
|
-
const fileName = normalized.split("/").pop() ?? "";
|
|
61
|
-
if (fileName.endsWith(".d.ts")) {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
if (fileName.startsWith("_") && !fileName.startsWith("_middleware.")) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
async function scanRoutes(routesDir) {
|
|
70
|
-
if (!existsSync(routesDir)) {
|
|
71
|
-
throw new Error(`Routes directory does not exist: ${routesDir}`);
|
|
72
|
-
}
|
|
73
|
-
const stats = statSync(routesDir);
|
|
74
|
-
if (!stats.isDirectory()) {
|
|
75
|
-
throw new Error(`Routes path is not a directory: ${routesDir}`);
|
|
76
|
-
}
|
|
77
|
-
const files = await fg("**/*.{ts,tsx,js,jsx}", {
|
|
78
|
-
cwd: routesDir,
|
|
79
|
-
ignore: ["node_modules/**", "**/*.d.ts"],
|
|
80
|
-
onlyFiles: true,
|
|
81
|
-
followSymbolicLinks: false
|
|
82
|
-
});
|
|
83
|
-
const routes = [];
|
|
84
|
-
for (const filePath of files) {
|
|
85
|
-
if (!shouldIncludeRoute(filePath)) {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const route = {
|
|
89
|
-
file: join(routesDir, filePath),
|
|
90
|
-
pattern: filePathToPattern(filePath, routesDir),
|
|
91
|
-
type: determineRouteType(filePath),
|
|
92
|
-
params: extractParams(filePath)
|
|
93
|
-
};
|
|
94
|
-
routes.push(route);
|
|
95
|
-
}
|
|
96
|
-
routes.sort((a, b) => {
|
|
97
|
-
return compareRoutes(a, b);
|
|
98
|
-
});
|
|
99
|
-
return routes;
|
|
100
|
-
}
|
|
101
|
-
function compareRoutes(a, b) {
|
|
102
|
-
if (a.type === "middleware" && b.type !== "middleware") return -1;
|
|
103
|
-
if (b.type === "middleware" && a.type !== "middleware") return 1;
|
|
104
|
-
const segmentsA = a.pattern.split("/").filter(Boolean);
|
|
105
|
-
const segmentsB = b.pattern.split("/").filter(Boolean);
|
|
106
|
-
const minLen = Math.min(segmentsA.length, segmentsB.length);
|
|
107
|
-
for (let i = 0; i < minLen; i++) {
|
|
108
|
-
const segA = segmentsA[i] ?? "";
|
|
109
|
-
const segB = segmentsB[i] ?? "";
|
|
110
|
-
const typeA = getSegmentType(segA);
|
|
111
|
-
const typeB = getSegmentType(segB);
|
|
112
|
-
if (typeA !== typeB) {
|
|
113
|
-
return typeA - typeB;
|
|
114
|
-
}
|
|
115
|
-
if (typeA === 0 && segA !== segB) {
|
|
116
|
-
return segA.localeCompare(segB);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (segmentsA.length !== segmentsB.length) {
|
|
120
|
-
return segmentsA.length - segmentsB.length;
|
|
121
|
-
}
|
|
122
|
-
return a.pattern.localeCompare(b.pattern);
|
|
123
|
-
}
|
|
124
|
-
function getSegmentType(segment) {
|
|
125
|
-
if (segment === "*") return 2;
|
|
126
|
-
if (segment.startsWith(":")) return 1;
|
|
127
|
-
return 0;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// src/dev/server.ts
|
|
131
|
-
import { createServer } from "http";
|
|
132
|
-
import { createReadStream } from "fs";
|
|
133
|
-
import { join as join3 } from "path";
|
|
134
|
-
|
|
135
|
-
// src/static/index.ts
|
|
136
|
-
import { extname, join as join2, normalize, resolve } from "path";
|
|
137
|
-
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
138
|
-
var MIME_TYPES = {
|
|
139
|
-
// Images
|
|
140
|
-
".ico": "image/x-icon",
|
|
141
|
-
".png": "image/png",
|
|
142
|
-
".jpg": "image/jpeg",
|
|
143
|
-
".jpeg": "image/jpeg",
|
|
144
|
-
".gif": "image/gif",
|
|
145
|
-
".svg": "image/svg+xml",
|
|
146
|
-
".webp": "image/webp",
|
|
147
|
-
// Fonts
|
|
148
|
-
".woff": "font/woff",
|
|
149
|
-
".woff2": "font/woff2",
|
|
150
|
-
".ttf": "font/ttf",
|
|
151
|
-
".otf": "font/otf",
|
|
152
|
-
".eot": "application/vnd.ms-fontobject",
|
|
153
|
-
// Web assets
|
|
154
|
-
".css": "text/css",
|
|
155
|
-
".js": "text/javascript",
|
|
156
|
-
".json": "application/json",
|
|
157
|
-
".txt": "text/plain",
|
|
158
|
-
".html": "text/html",
|
|
159
|
-
".xml": "application/xml",
|
|
160
|
-
// Other
|
|
161
|
-
".pdf": "application/pdf",
|
|
162
|
-
".mp3": "audio/mpeg",
|
|
163
|
-
".mp4": "video/mp4",
|
|
164
|
-
".webm": "video/webm",
|
|
165
|
-
".map": "application/json"
|
|
166
|
-
};
|
|
167
|
-
var DEFAULT_MIME_TYPE = "application/octet-stream";
|
|
168
|
-
function isPathSafe(pathname) {
|
|
169
|
-
if (!pathname) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
if (!pathname.startsWith("/")) {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
if (pathname.includes("\\")) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
if (pathname.includes("//")) {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
if (pathname.includes("\0") || pathname.includes("%00")) {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
let decoded;
|
|
185
|
-
try {
|
|
186
|
-
decoded = pathname;
|
|
187
|
-
let prevDecoded = "";
|
|
188
|
-
while (decoded !== prevDecoded) {
|
|
189
|
-
prevDecoded = decoded;
|
|
190
|
-
decoded = decodeURIComponent(decoded);
|
|
191
|
-
}
|
|
192
|
-
} catch {
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
if (decoded.includes("\0")) {
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
if (decoded.includes("..")) {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
const segments = decoded.split("/").filter(Boolean);
|
|
202
|
-
for (const segment of segments) {
|
|
203
|
-
if (segment.startsWith(".")) {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
function getMimeType(filePath) {
|
|
210
|
-
const ext = extname(filePath).toLowerCase();
|
|
211
|
-
return MIME_TYPES[ext] ?? DEFAULT_MIME_TYPE;
|
|
212
|
-
}
|
|
213
|
-
function hasObviousAttackPattern(pathname) {
|
|
214
|
-
if (!pathname) {
|
|
215
|
-
return true;
|
|
216
|
-
}
|
|
217
|
-
if (!pathname.startsWith("/")) {
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
if (pathname.includes("\\")) {
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
if (pathname.includes("//")) {
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
if (pathname.includes("\0") || pathname.includes("%00")) {
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
let decoded;
|
|
230
|
-
try {
|
|
231
|
-
decoded = pathname;
|
|
232
|
-
let prevDecoded = "";
|
|
233
|
-
while (decoded !== prevDecoded) {
|
|
234
|
-
prevDecoded = decoded;
|
|
235
|
-
decoded = decodeURIComponent(decoded);
|
|
236
|
-
}
|
|
237
|
-
} catch {
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
if (decoded.includes("\0")) {
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
const segments = decoded.split("/").filter(Boolean);
|
|
244
|
-
for (const segment of segments) {
|
|
245
|
-
if (segment.startsWith(".") && segment !== "..") {
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
if (decoded.startsWith("/..")) {
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
function resolveStaticFile(pathname, publicDir) {
|
|
255
|
-
if (hasObviousAttackPattern(pathname)) {
|
|
256
|
-
return {
|
|
257
|
-
exists: false,
|
|
258
|
-
filePath: null,
|
|
259
|
-
mimeType: null,
|
|
260
|
-
error: "path_traversal"
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
let decodedPathname;
|
|
264
|
-
try {
|
|
265
|
-
decodedPathname = decodeURIComponent(pathname);
|
|
266
|
-
} catch {
|
|
267
|
-
return {
|
|
268
|
-
exists: false,
|
|
269
|
-
filePath: null,
|
|
270
|
-
mimeType: null,
|
|
271
|
-
error: "path_traversal"
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
const relativePath = decodedPathname.slice(1);
|
|
275
|
-
const resolvedPath = normalize(join2(publicDir, relativePath));
|
|
276
|
-
const absolutePublicDir = resolve(publicDir);
|
|
277
|
-
const publicDirWithSep = absolutePublicDir.endsWith("/") ? absolutePublicDir : absolutePublicDir + "/";
|
|
278
|
-
if (!resolvedPath.startsWith(publicDirWithSep) && resolvedPath !== absolutePublicDir) {
|
|
279
|
-
return {
|
|
280
|
-
exists: false,
|
|
281
|
-
filePath: null,
|
|
282
|
-
mimeType: null,
|
|
283
|
-
error: "outside_public"
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
const mimeType = getMimeType(resolvedPath);
|
|
287
|
-
let exists = false;
|
|
288
|
-
if (existsSync2(resolvedPath)) {
|
|
289
|
-
try {
|
|
290
|
-
const stats = statSync2(resolvedPath);
|
|
291
|
-
exists = stats.isFile();
|
|
292
|
-
} catch {
|
|
293
|
-
exists = false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
exists,
|
|
298
|
-
filePath: resolvedPath,
|
|
299
|
-
mimeType
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// src/dev/server.ts
|
|
304
|
-
var DEFAULT_PORT = 3e3;
|
|
305
|
-
var DEFAULT_HOST = "localhost";
|
|
306
|
-
var DEFAULT_PUBLIC_DIR = "public";
|
|
307
|
-
async function createDevServer(options = {}) {
|
|
308
|
-
const {
|
|
309
|
-
port = DEFAULT_PORT,
|
|
310
|
-
host = DEFAULT_HOST,
|
|
311
|
-
routesDir: _routesDir = "src/routes",
|
|
312
|
-
publicDir = join3(process.cwd(), DEFAULT_PUBLIC_DIR)
|
|
313
|
-
} = options;
|
|
314
|
-
let httpServer = null;
|
|
315
|
-
let actualPort = port;
|
|
316
|
-
const devServer = {
|
|
317
|
-
get port() {
|
|
318
|
-
return actualPort;
|
|
319
|
-
},
|
|
320
|
-
async listen() {
|
|
321
|
-
return new Promise((resolve2, reject) => {
|
|
322
|
-
httpServer = createServer((req, res) => {
|
|
323
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
324
|
-
const pathname = url.pathname;
|
|
325
|
-
const staticResult = resolveStaticFile(pathname, publicDir);
|
|
326
|
-
if (staticResult.error === "path_traversal" || staticResult.error === "outside_public") {
|
|
327
|
-
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
328
|
-
res.end("Forbidden");
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
if (staticResult.exists && staticResult.filePath && staticResult.mimeType) {
|
|
332
|
-
res.writeHead(200, { "Content-Type": staticResult.mimeType });
|
|
333
|
-
const stream = createReadStream(staticResult.filePath);
|
|
334
|
-
stream.pipe(res);
|
|
335
|
-
stream.on("error", () => {
|
|
336
|
-
if (!res.headersSent) {
|
|
337
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
338
|
-
}
|
|
339
|
-
res.end("Internal Server Error");
|
|
340
|
-
});
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (staticResult.filePath && !staticResult.exists) {
|
|
344
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
345
|
-
res.end("Not Found");
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
349
|
-
res.end("<html><body>Constela Dev Server</body></html>");
|
|
350
|
-
});
|
|
351
|
-
httpServer.on("error", (err) => {
|
|
352
|
-
reject(err);
|
|
353
|
-
});
|
|
354
|
-
httpServer.listen(port, host, () => {
|
|
355
|
-
const address = httpServer?.address();
|
|
356
|
-
if (address) {
|
|
357
|
-
actualPort = address.port;
|
|
358
|
-
}
|
|
359
|
-
resolve2();
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
},
|
|
363
|
-
async close() {
|
|
364
|
-
return new Promise((resolve2, reject) => {
|
|
365
|
-
if (!httpServer) {
|
|
366
|
-
resolve2();
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
httpServer.close((err) => {
|
|
370
|
-
if (err) {
|
|
371
|
-
reject(err);
|
|
372
|
-
} else {
|
|
373
|
-
httpServer = null;
|
|
374
|
-
resolve2();
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
return devServer;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// src/build/index.ts
|
|
384
|
-
async function build(options) {
|
|
385
|
-
const outDir = options?.outDir ?? "dist";
|
|
386
|
-
const routesDir = options?.routesDir ?? "src/routes";
|
|
387
|
-
let routes = [];
|
|
388
|
-
try {
|
|
389
|
-
const scannedRoutes = await scanRoutes(routesDir);
|
|
390
|
-
routes = scannedRoutes.map((r) => r.pattern);
|
|
391
|
-
} catch {
|
|
392
|
-
}
|
|
393
|
-
return {
|
|
394
|
-
outDir,
|
|
395
|
-
routes
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
16
|
// src/build/ssg.ts
|
|
400
17
|
import { mkdir, writeFile } from "fs/promises";
|
|
401
|
-
import { join
|
|
18
|
+
import { join, dirname } from "path";
|
|
402
19
|
var defaultProgram = {
|
|
403
20
|
version: "1.0",
|
|
404
21
|
state: {},
|
|
@@ -424,10 +41,10 @@ var testStaticPaths = {
|
|
|
424
41
|
var consumedPatterns = /* @__PURE__ */ new Set();
|
|
425
42
|
function getOutputPath(pattern, outDir) {
|
|
426
43
|
if (pattern === "/") {
|
|
427
|
-
return
|
|
44
|
+
return join(outDir, "index.html");
|
|
428
45
|
}
|
|
429
46
|
const segments = pattern.slice(1).split("/");
|
|
430
|
-
return
|
|
47
|
+
return join(outDir, ...segments, "index.html");
|
|
431
48
|
}
|
|
432
49
|
function resolvePattern(pattern, params) {
|
|
433
50
|
let resolved = pattern;
|