@constela/start 0.3.0 → 0.4.0
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.d.ts +214 -3
- package/dist/index.js +490 -413
- package/package.json +6 -5
|
@@ -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.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CompiledProgram } from '@constela/compiler';
|
|
2
|
+
import { LayoutProgram, Program, StaticPathsDefinition, DataSource } from '@constela/core';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Scanned route from file system
|
|
@@ -38,11 +39,22 @@ interface MiddlewareContext {
|
|
|
38
39
|
}
|
|
39
40
|
type MiddlewareNext = () => Promise<Response>;
|
|
40
41
|
type Middleware = (ctx: MiddlewareContext, next: MiddlewareNext) => Promise<Response> | Response;
|
|
42
|
+
/**
|
|
43
|
+
* Function-form page export that receives route params.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // pages/users/[id].ts
|
|
47
|
+
* export default async function(params: Record<string, string>) {
|
|
48
|
+
* const user = await fetchUser(params.id);
|
|
49
|
+
* return createUserPage(user);
|
|
50
|
+
* }
|
|
51
|
+
*/
|
|
52
|
+
type PageExportFunction = (params: Record<string, string>) => Promise<CompiledProgram> | CompiledProgram;
|
|
41
53
|
/**
|
|
42
54
|
* Page module with Constela program
|
|
43
55
|
*/
|
|
44
56
|
interface PageModule {
|
|
45
|
-
default: CompiledProgram;
|
|
57
|
+
default: CompiledProgram | PageExportFunction;
|
|
46
58
|
getStaticPaths?: () => Promise<StaticPathsResult> | StaticPathsResult;
|
|
47
59
|
}
|
|
48
60
|
interface StaticPathsResult {
|
|
@@ -185,14 +197,30 @@ interface BuildResult {
|
|
|
185
197
|
*/
|
|
186
198
|
declare function build(options?: BuildOptions): Promise<BuildResult>;
|
|
187
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Provider function for static paths when getStaticPaths is not available in the module.
|
|
202
|
+
* Used for dependency injection in tests.
|
|
203
|
+
*/
|
|
204
|
+
type StaticPathsProvider = (pattern: string) => StaticPathsResult | null | Promise<StaticPathsResult | null>;
|
|
205
|
+
/**
|
|
206
|
+
* Options for generateStaticPages function
|
|
207
|
+
*/
|
|
208
|
+
interface GenerateStaticPagesOptions {
|
|
209
|
+
/**
|
|
210
|
+
* Optional provider for static paths when module doesn't export getStaticPaths.
|
|
211
|
+
* Primarily used for testing purposes.
|
|
212
|
+
*/
|
|
213
|
+
staticPathsProvider?: StaticPathsProvider;
|
|
214
|
+
}
|
|
188
215
|
/**
|
|
189
216
|
* Generate static pages for SSG routes
|
|
190
217
|
*
|
|
191
218
|
* @param routes - Array of scanned routes
|
|
192
219
|
* @param outDir - Output directory for generated HTML files
|
|
220
|
+
* @param options - Optional configuration including staticPathsProvider for DI
|
|
193
221
|
* @returns Array of generated file paths
|
|
194
222
|
*/
|
|
195
|
-
declare function generateStaticPages(routes: ScannedRoute[], outDir: string): Promise<string[]>;
|
|
223
|
+
declare function generateStaticPages(routes: ScannedRoute[], outDir: string, options?: GenerateStaticPagesOptions): Promise<string[]>;
|
|
196
224
|
|
|
197
225
|
/**
|
|
198
226
|
* Create handler function from API module
|
|
@@ -227,4 +255,187 @@ interface EdgeAdapter {
|
|
|
227
255
|
*/
|
|
228
256
|
declare function createAdapter(options: AdapterOptions): EdgeAdapter;
|
|
229
257
|
|
|
230
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Type guard to check if a page export is a function (dynamic) or a static program
|
|
260
|
+
*
|
|
261
|
+
* @param exported - The default export from a page module
|
|
262
|
+
* @returns true if the export is a function, false if it's a static CompiledProgram
|
|
263
|
+
*/
|
|
264
|
+
declare function isPageExportFunction(exported: CompiledProgram | PageExportFunction): exported is PageExportFunction;
|
|
265
|
+
/**
|
|
266
|
+
* Resolve a page export to a CompiledProgram
|
|
267
|
+
*
|
|
268
|
+
* If the export is a function, it will be called with the route params.
|
|
269
|
+
* If the export is a static CompiledProgram, it will be returned as-is.
|
|
270
|
+
*
|
|
271
|
+
* @param pageDefault - The default export from a page module (function or static program)
|
|
272
|
+
* @param params - Route parameters extracted from the URL
|
|
273
|
+
* @param expectedParams - Optional array of expected parameter names for validation
|
|
274
|
+
* @returns Promise resolving to a CompiledProgram
|
|
275
|
+
* @throws Error if expectedParams is provided and a required param is missing
|
|
276
|
+
*/
|
|
277
|
+
declare function resolvePageExport(pageDefault: CompiledProgram | PageExportFunction, params: Record<string, string>, expectedParams?: string[]): Promise<CompiledProgram>;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Layout Resolver - Scans and resolves layout files for Constela Start
|
|
281
|
+
*
|
|
282
|
+
* This module provides functionality to:
|
|
283
|
+
* - Scan layout directories for layout files
|
|
284
|
+
* - Resolve layout by name
|
|
285
|
+
* - Load and validate layout programs
|
|
286
|
+
* - Compose layouts with page content
|
|
287
|
+
*/
|
|
288
|
+
|
|
289
|
+
interface ScannedLayout {
|
|
290
|
+
name: string;
|
|
291
|
+
file: string;
|
|
292
|
+
}
|
|
293
|
+
interface LayoutInfo {
|
|
294
|
+
name: string;
|
|
295
|
+
file: string;
|
|
296
|
+
program?: LayoutProgram;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Scans a directory for layout files
|
|
300
|
+
*
|
|
301
|
+
* @param layoutsDir - Directory to scan for layouts
|
|
302
|
+
* @returns Array of scanned layouts with name and file path
|
|
303
|
+
* @throws Error if directory doesn't exist or is not a directory
|
|
304
|
+
*/
|
|
305
|
+
declare function scanLayouts(layoutsDir: string): Promise<ScannedLayout[]>;
|
|
306
|
+
/**
|
|
307
|
+
* Resolves a layout by name from scanned layouts
|
|
308
|
+
*
|
|
309
|
+
* @param layoutName - Name of the layout to resolve
|
|
310
|
+
* @param layouts - Array of scanned layouts
|
|
311
|
+
* @returns The matching layout or undefined
|
|
312
|
+
*/
|
|
313
|
+
declare function resolveLayout(layoutName: string, layouts: ScannedLayout[]): ScannedLayout | undefined;
|
|
314
|
+
/**
|
|
315
|
+
* Loads and validates a layout file
|
|
316
|
+
*
|
|
317
|
+
* @param layoutFile - Path to the layout file
|
|
318
|
+
* @returns The loaded LayoutProgram
|
|
319
|
+
* @throws Error if file cannot be loaded or is not a valid layout
|
|
320
|
+
*/
|
|
321
|
+
declare function loadLayout(layoutFile: string): Promise<LayoutProgram>;
|
|
322
|
+
/**
|
|
323
|
+
* LayoutResolver - Manages layout scanning, resolution, and composition
|
|
324
|
+
*/
|
|
325
|
+
declare class LayoutResolver {
|
|
326
|
+
private layoutsDir;
|
|
327
|
+
private layouts;
|
|
328
|
+
private loadedLayouts;
|
|
329
|
+
private initialized;
|
|
330
|
+
constructor(layoutsDir: string);
|
|
331
|
+
/**
|
|
332
|
+
* Initialize the resolver by scanning the layouts directory
|
|
333
|
+
*/
|
|
334
|
+
initialize(): Promise<void>;
|
|
335
|
+
/**
|
|
336
|
+
* Check if a layout exists
|
|
337
|
+
*/
|
|
338
|
+
hasLayout(name: string): boolean;
|
|
339
|
+
/**
|
|
340
|
+
* Get a layout by name
|
|
341
|
+
*
|
|
342
|
+
* @param name - Layout name
|
|
343
|
+
* @returns The layout program or undefined if not found
|
|
344
|
+
*/
|
|
345
|
+
getLayout(name: string): Promise<LayoutProgram | undefined>;
|
|
346
|
+
/**
|
|
347
|
+
* Compose a page with its layout
|
|
348
|
+
*
|
|
349
|
+
* @param page - Page program to compose
|
|
350
|
+
* @returns Composed program (or original if no layout)
|
|
351
|
+
* @throws Error if specified layout is not found
|
|
352
|
+
*/
|
|
353
|
+
composeWithLayout(page: Program): Promise<Program>;
|
|
354
|
+
/**
|
|
355
|
+
* Get all scanned layouts
|
|
356
|
+
*/
|
|
357
|
+
getAll(): ScannedLayout[];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Data Loader for Build-time Content Loading
|
|
362
|
+
*
|
|
363
|
+
* This module provides utilities for loading data sources at build time:
|
|
364
|
+
* - Glob patterns for multiple files
|
|
365
|
+
* - Single file loading
|
|
366
|
+
* - API fetching
|
|
367
|
+
* - MDX/YAML/CSV transformations
|
|
368
|
+
* - Static path generation for SSG
|
|
369
|
+
*/
|
|
370
|
+
|
|
371
|
+
interface GlobResult {
|
|
372
|
+
file: string;
|
|
373
|
+
raw: string;
|
|
374
|
+
frontmatter?: Record<string, unknown>;
|
|
375
|
+
content?: string;
|
|
376
|
+
}
|
|
377
|
+
interface StaticPath {
|
|
378
|
+
params: Record<string, string>;
|
|
379
|
+
data?: unknown;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Transform MDX content - parse frontmatter and content
|
|
383
|
+
*/
|
|
384
|
+
declare function transformMdx(content: string): {
|
|
385
|
+
frontmatter: Record<string, unknown>;
|
|
386
|
+
content: string;
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* Transform YAML content
|
|
390
|
+
*/
|
|
391
|
+
declare function transformYaml(content: string): Record<string, unknown>;
|
|
392
|
+
/**
|
|
393
|
+
* Transform CSV content to array of objects
|
|
394
|
+
*/
|
|
395
|
+
declare function transformCsv(content: string): Record<string, string>[];
|
|
396
|
+
/**
|
|
397
|
+
* Load files matching a glob pattern
|
|
398
|
+
*/
|
|
399
|
+
declare function loadGlob(baseDir: string, pattern: string, transform?: string): Promise<GlobResult[]>;
|
|
400
|
+
/**
|
|
401
|
+
* Load a single file
|
|
402
|
+
*/
|
|
403
|
+
declare function loadFile(baseDir: string, filePath: string, transform?: string): Promise<unknown>;
|
|
404
|
+
/**
|
|
405
|
+
* Load data from API endpoint
|
|
406
|
+
*/
|
|
407
|
+
declare function loadApi(url: string, transform?: string): Promise<unknown>;
|
|
408
|
+
/**
|
|
409
|
+
* Generate static paths from data using params expression
|
|
410
|
+
*/
|
|
411
|
+
declare function generateStaticPaths(data: unknown[], staticPathsDef: StaticPathsDefinition): Promise<StaticPath[]>;
|
|
412
|
+
/**
|
|
413
|
+
* DataLoader class for managing data source loading with caching
|
|
414
|
+
*/
|
|
415
|
+
declare class DataLoader {
|
|
416
|
+
private cache;
|
|
417
|
+
private projectRoot;
|
|
418
|
+
constructor(projectRoot: string);
|
|
419
|
+
/**
|
|
420
|
+
* Load a single data source
|
|
421
|
+
*/
|
|
422
|
+
loadDataSource(name: string, dataSource: DataSource): Promise<unknown>;
|
|
423
|
+
/**
|
|
424
|
+
* Load all data sources
|
|
425
|
+
*/
|
|
426
|
+
loadAllDataSources(dataSources: Record<string, DataSource>): Promise<Record<string, unknown>>;
|
|
427
|
+
/**
|
|
428
|
+
* Clear cache for a specific data source or all caches
|
|
429
|
+
*/
|
|
430
|
+
clearCache(name?: string): void;
|
|
431
|
+
/**
|
|
432
|
+
* Clear all cache entries
|
|
433
|
+
*/
|
|
434
|
+
clearAllCache(): void;
|
|
435
|
+
/**
|
|
436
|
+
* Get the current cache size
|
|
437
|
+
*/
|
|
438
|
+
getCacheSize(): number;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export { type APIContext, type APIModule, type BuildOptions, type ConstelaConfig, DataLoader, type DevServerOptions, type GenerateStaticPagesOptions, type GlobResult, type LayoutInfo, LayoutResolver, type Middleware, type MiddlewareContext, type MiddlewareNext, type PageExportFunction, type PageModule, type ScannedLayout, type ScannedRoute, type StaticFileResult, type StaticPath, type StaticPathsProvider, type StaticPathsResult, build, createAPIHandler, createAdapter, createDevServer, createMiddlewareChain, filePathToPattern, generateStaticPages, generateStaticPaths, getMimeType, isPageExportFunction, isPathSafe, loadApi, loadFile, loadGlob, loadLayout, resolveLayout, resolvePageExport, resolveStaticFile, scanLayouts, scanRoutes, transformCsv, transformMdx, transformYaml };
|