@constela/start 0.2.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.
@@ -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
+ };
@@ -0,0 +1,38 @@
1
+ import { Command } from 'commander';
2
+
3
+ type DevHandler = (options: {
4
+ port: string;
5
+ host?: string;
6
+ }) => Promise<{
7
+ port: number;
8
+ }>;
9
+ type BuildHandler = (options: {
10
+ outDir?: string;
11
+ }) => Promise<void>;
12
+ type StartHandler = (options: {
13
+ port: string;
14
+ }) => Promise<{
15
+ port: number;
16
+ }>;
17
+ /**
18
+ * Set custom dev handler (for testing)
19
+ */
20
+ declare function setDevHandler(handler: DevHandler): void;
21
+ /**
22
+ * Set custom build handler (for testing)
23
+ */
24
+ declare function setBuildHandler(handler: BuildHandler): void;
25
+ /**
26
+ * Set custom start handler (for testing)
27
+ */
28
+ declare function setStartHandler(handler: StartHandler): void;
29
+ /**
30
+ * Creates and configures the CLI program
31
+ */
32
+ declare function createCLI(): Command;
33
+ /**
34
+ * Main CLI entry point
35
+ */
36
+ declare function main(): void;
37
+
38
+ export { createCLI, main, setBuildHandler, setDevHandler, setStartHandler };
@@ -0,0 +1,71 @@
1
+ import {
2
+ build,
3
+ createDevServer
4
+ } from "../chunk-JXIOHPG5.js";
5
+
6
+ // src/cli/index.ts
7
+ import { Command } from "commander";
8
+ var devHandler = async (options) => {
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 };
20
+ };
21
+ var buildHandler = async (options) => {
22
+ console.log("Building for production...");
23
+ await build(options.outDir ? { outDir: options.outDir } : {});
24
+ console.log("Build complete");
25
+ };
26
+ var startHandler = async (options) => {
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 };
37
+ };
38
+ function setDevHandler(handler) {
39
+ devHandler = handler;
40
+ }
41
+ function setBuildHandler(handler) {
42
+ buildHandler = handler;
43
+ }
44
+ function setStartHandler(handler) {
45
+ startHandler = handler;
46
+ }
47
+ function createCLI() {
48
+ const program = new Command();
49
+ program.name("constela-start").version("0.1.0").description("Meta-framework for Constela applications");
50
+ program.command("dev").description("Start development server").option("-p, --port <port>", "Port number", "3000").option("-h, --host <host>", "Host address").action(async (options) => {
51
+ await devHandler(options);
52
+ });
53
+ program.command("build").description("Build for production").option("-o, --outDir <outDir>", "Output directory").action(async (options) => {
54
+ await buildHandler(options);
55
+ });
56
+ program.command("start").description("Start production server").option("-p, --port <port>", "Port number", "3000").action(async (options) => {
57
+ await startHandler(options);
58
+ });
59
+ return program;
60
+ }
61
+ function main() {
62
+ const program = createCLI();
63
+ program.parse();
64
+ }
65
+ export {
66
+ createCLI,
67
+ main,
68
+ setBuildHandler,
69
+ setDevHandler,
70
+ setStartHandler
71
+ };
package/dist/index.d.ts CHANGED
@@ -68,6 +68,7 @@ interface DevServerOptions {
68
68
  port?: number;
69
69
  host?: string;
70
70
  routesDir?: string;
71
+ publicDir?: string;
71
72
  }
72
73
  /**
73
74
  * Build options
@@ -123,6 +124,55 @@ interface DevServer {
123
124
  */
124
125
  declare function createDevServer(options?: DevServerOptions): Promise<DevServer>;
125
126
 
127
+ /**
128
+ * Static file serving utilities.
129
+ *
130
+ * Provides path validation, MIME type detection, and file resolution
131
+ * for serving static files from the public directory.
132
+ */
133
+ /**
134
+ * Result of resolving a static file path
135
+ */
136
+ interface StaticFileResult {
137
+ exists: boolean;
138
+ filePath: string | null;
139
+ mimeType: string | null;
140
+ error?: 'path_traversal' | 'outside_public';
141
+ }
142
+ /**
143
+ * Check if a URL pathname is safe from path traversal attacks.
144
+ *
145
+ * Rejects:
146
+ * - Paths containing '..' (decoded)
147
+ * - Double slashes '//'
148
+ * - Null bytes
149
+ * - Backslashes
150
+ * - Hidden files (starting with '.')
151
+ * - Paths not starting with '/'
152
+ * - Empty paths
153
+ *
154
+ * @param pathname - The URL pathname to validate
155
+ * @returns true if the path is safe, false otherwise
156
+ */
157
+ declare function isPathSafe(pathname: string): boolean;
158
+ /**
159
+ * Get the MIME type for a file based on its extension.
160
+ *
161
+ * @param filePath - The file path to check
162
+ * @returns The MIME type string, or 'application/octet-stream' for unknown types
163
+ */
164
+ declare function getMimeType(filePath: string): string;
165
+ /**
166
+ * Resolve a URL pathname to an absolute file path within the public directory.
167
+ *
168
+ * Performs security validation and checks if the file exists.
169
+ *
170
+ * @param pathname - The URL pathname (e.g., '/favicon.ico')
171
+ * @param publicDir - The absolute path to the public directory
172
+ * @returns StaticFileResult with resolution details
173
+ */
174
+ declare function resolveStaticFile(pathname: string, publicDir: string): StaticFileResult;
175
+
126
176
  interface BuildResult {
127
177
  outDir: string;
128
178
  routes: string[];
@@ -177,4 +227,4 @@ interface EdgeAdapter {
177
227
  */
178
228
  declare function createAdapter(options: AdapterOptions): EdgeAdapter;
179
229
 
180
- export { type APIContext, type APIModule, type BuildOptions, type ConstelaConfig, type DevServerOptions, type Middleware, type MiddlewareContext, type MiddlewareNext, type PageModule, type ScannedRoute, type StaticPathsResult, build, createAPIHandler, createAdapter, createDevServer, createMiddlewareChain, filePathToPattern, generateStaticPages, scanRoutes };
230
+ export { type APIContext, type APIModule, type BuildOptions, type ConstelaConfig, type DevServerOptions, type Middleware, type MiddlewareContext, type MiddlewareNext, type PageModule, type ScannedRoute, type StaticFileResult, type StaticPathsResult, build, createAPIHandler, createAdapter, createDevServer, createMiddlewareChain, filePathToPattern, generateStaticPages, getMimeType, isPathSafe, resolveStaticFile, scanRoutes };
package/dist/index.js CHANGED
@@ -1,205 +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
- var DEFAULT_PORT = 3e3;
133
- var DEFAULT_HOST = "localhost";
134
- async function createDevServer(options = {}) {
135
- const {
136
- port = DEFAULT_PORT,
137
- host = DEFAULT_HOST,
138
- routesDir: _routesDir = "src/routes"
139
- } = options;
140
- let httpServer = null;
141
- let actualPort = port;
142
- const devServer = {
143
- get port() {
144
- return actualPort;
145
- },
146
- async listen() {
147
- return new Promise((resolve, reject) => {
148
- httpServer = createServer((_req, res) => {
149
- res.writeHead(200, { "Content-Type": "text/html" });
150
- res.end("<html><body>Constela Dev Server</body></html>");
151
- });
152
- httpServer.on("error", (err) => {
153
- reject(err);
154
- });
155
- httpServer.listen(port, host, () => {
156
- const address = httpServer?.address();
157
- if (address) {
158
- actualPort = address.port;
159
- }
160
- resolve();
161
- });
162
- });
163
- },
164
- async close() {
165
- return new Promise((resolve, reject) => {
166
- if (!httpServer) {
167
- resolve();
168
- return;
169
- }
170
- httpServer.close((err) => {
171
- if (err) {
172
- reject(err);
173
- } else {
174
- httpServer = null;
175
- resolve();
176
- }
177
- });
178
- });
179
- }
180
- };
181
- return devServer;
182
- }
183
-
184
- // src/build/index.ts
185
- async function build(options) {
186
- const outDir = options?.outDir ?? "dist";
187
- const routesDir = options?.routesDir ?? "src/routes";
188
- let routes = [];
189
- try {
190
- const scannedRoutes = await scanRoutes(routesDir);
191
- routes = scannedRoutes.map((r) => r.pattern);
192
- } catch {
193
- }
194
- return {
195
- outDir,
196
- routes
197
- };
198
- }
199
-
200
16
  // src/build/ssg.ts
201
17
  import { mkdir, writeFile } from "fs/promises";
202
- import { join as join2, dirname } from "path";
18
+ import { join, dirname } from "path";
203
19
  var defaultProgram = {
204
20
  version: "1.0",
205
21
  state: {},
@@ -225,10 +41,10 @@ var testStaticPaths = {
225
41
  var consumedPatterns = /* @__PURE__ */ new Set();
226
42
  function getOutputPath(pattern, outDir) {
227
43
  if (pattern === "/") {
228
- return join2(outDir, "index.html");
44
+ return join(outDir, "index.html");
229
45
  }
230
46
  const segments = pattern.slice(1).split("/");
231
- return join2(outDir, ...segments, "index.html");
47
+ return join(outDir, ...segments, "index.html");
232
48
  }
233
49
  function resolvePattern(pattern, params) {
234
50
  let resolved = pattern;
@@ -468,5 +284,8 @@ export {
468
284
  createMiddlewareChain,
469
285
  filePathToPattern,
470
286
  generateStaticPages,
287
+ getMimeType,
288
+ isPathSafe,
289
+ resolveStaticFile,
471
290
  scanRoutes
472
291
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/start",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Meta-framework for Constela applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -39,10 +39,10 @@
39
39
  "remark-mdx": "^3.0.0",
40
40
  "remark-gfm": "^4.0.0",
41
41
  "gray-matter": "^4.0.0",
42
- "@constela/runtime": "0.7.0",
43
- "@constela/router": "4.0.0",
44
42
  "@constela/compiler": "0.4.0",
45
- "@constela/server": "0.1.2"
43
+ "@constela/runtime": "0.7.0",
44
+ "@constela/server": "0.1.2",
45
+ "@constela/router": "4.0.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "typescript": "^5.3.0",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "license": "MIT",
53
53
  "scripts": {
54
- "build": "tsup src/index.ts src/runtime/entry-client.ts src/runtime/entry-server.ts --format esm --dts --clean",
54
+ "build": "tsup src/index.ts src/runtime/entry-client.ts src/runtime/entry-server.ts src/cli/index.ts --format esm --dts --clean",
55
55
  "type-check": "tsc --noEmit",
56
56
  "test": "vitest run",
57
57
  "test:watch": "vitest",