@constela/start 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Constela Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli/index.js').then(m => m.main());
@@ -0,0 +1,49 @@
1
+ // src/runtime/entry-server.ts
2
+ import { renderToString } from "@constela/server";
3
+ async function renderPage(program, _ctx) {
4
+ return await renderToString(program);
5
+ }
6
+ function escapeJsonForScript(json) {
7
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
8
+ }
9
+ function serializeProgram(program) {
10
+ const serializable = {
11
+ ...program,
12
+ // Convert Map to Object if actions is a Map
13
+ actions: program.actions instanceof Map ? Object.fromEntries(program.actions.entries()) : program.actions
14
+ };
15
+ return JSON.stringify(serializable);
16
+ }
17
+ function generateHydrationScript(program) {
18
+ const serializedProgram = escapeJsonForScript(serializeProgram(program));
19
+ return `import { hydrateApp } from '@constela/runtime';
20
+
21
+ const program = ${serializedProgram};
22
+
23
+ hydrateApp({
24
+ program,
25
+ container: document.getElementById('app')
26
+ });`;
27
+ }
28
+ function wrapHtml(content, hydrationScript, head) {
29
+ return `<!DOCTYPE html>
30
+ <html>
31
+ <head>
32
+ <meta charset="utf-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1">
34
+ ${head ?? ""}
35
+ </head>
36
+ <body>
37
+ <div id="app">${content}</div>
38
+ <script type="module">
39
+ ${hydrationScript}
40
+ </script>
41
+ </body>
42
+ </html>`;
43
+ }
44
+
45
+ export {
46
+ renderPage,
47
+ generateHydrationScript,
48
+ wrapHtml
49
+ };
@@ -0,0 +1,180 @@
1
+ import { CompiledProgram } from '@constela/compiler';
2
+
3
+ /**
4
+ * Scanned route from file system
5
+ */
6
+ interface ScannedRoute {
7
+ file: string;
8
+ pattern: string;
9
+ type: 'page' | 'api' | 'middleware';
10
+ params: string[];
11
+ }
12
+ /**
13
+ * API endpoint context
14
+ */
15
+ interface APIContext {
16
+ params: Record<string, string>;
17
+ query: URLSearchParams;
18
+ request: Request;
19
+ }
20
+ /**
21
+ * API module with HTTP method handlers
22
+ */
23
+ interface APIModule {
24
+ GET?: (ctx: APIContext) => Promise<Response> | Response;
25
+ POST?: (ctx: APIContext) => Promise<Response> | Response;
26
+ PUT?: (ctx: APIContext) => Promise<Response> | Response;
27
+ DELETE?: (ctx: APIContext) => Promise<Response> | Response;
28
+ PATCH?: (ctx: APIContext) => Promise<Response> | Response;
29
+ }
30
+ /**
31
+ * Middleware context
32
+ */
33
+ interface MiddlewareContext {
34
+ request: Request;
35
+ params: Record<string, string>;
36
+ url: URL;
37
+ locals: Record<string, unknown>;
38
+ }
39
+ type MiddlewareNext = () => Promise<Response>;
40
+ type Middleware = (ctx: MiddlewareContext, next: MiddlewareNext) => Promise<Response> | Response;
41
+ /**
42
+ * Page module with Constela program
43
+ */
44
+ interface PageModule {
45
+ default: CompiledProgram;
46
+ getStaticPaths?: () => Promise<StaticPathsResult> | StaticPathsResult;
47
+ }
48
+ interface StaticPathsResult {
49
+ paths: Array<{
50
+ params: Record<string, string>;
51
+ }>;
52
+ }
53
+ /**
54
+ * Constela configuration
55
+ */
56
+ interface ConstelaConfig {
57
+ ssg?: {
58
+ routes?: string[];
59
+ };
60
+ edge?: {
61
+ adapter?: 'cloudflare' | 'vercel' | 'deno' | 'node';
62
+ };
63
+ }
64
+ /**
65
+ * Development server options
66
+ */
67
+ interface DevServerOptions {
68
+ port?: number;
69
+ host?: string;
70
+ routesDir?: string;
71
+ }
72
+ /**
73
+ * Build options
74
+ */
75
+ interface BuildOptions {
76
+ outDir?: string;
77
+ routesDir?: string;
78
+ target?: 'node' | 'edge';
79
+ }
80
+
81
+ /**
82
+ * Convert file path to URL pattern
83
+ *
84
+ * Rules:
85
+ * - index.ts -> /
86
+ * - about.ts -> /about
87
+ * - users/index.ts -> /users
88
+ * - users/[id].ts -> /users/:id
89
+ * - blog/[...slug].ts -> /blog/*
90
+ *
91
+ * @param filePath - File path relative to routes directory
92
+ * @param _routesDir - Routes directory (unused, kept for API compatibility)
93
+ */
94
+ declare function filePathToPattern(filePath: string, _routesDir?: string): string;
95
+ /**
96
+ * Scan routes directory for route files
97
+ *
98
+ * @param routesDir - Directory to scan for route files
99
+ */
100
+ declare function scanRoutes(routesDir: string): Promise<ScannedRoute[]>;
101
+
102
+ /**
103
+ * Development server interface
104
+ */
105
+ interface DevServer {
106
+ /** Start listening for connections */
107
+ listen(): Promise<void>;
108
+ /** Stop the server and close all connections */
109
+ close(): Promise<void>;
110
+ /** The port number the server is listening on */
111
+ port: number;
112
+ }
113
+ /**
114
+ * Creates a development server with HMR support.
115
+ *
116
+ * The server uses:
117
+ * - Node.js http module for the base server
118
+ * - Vite middleware mode for HMR (future enhancement)
119
+ * - Hono for request handling (future enhancement)
120
+ *
121
+ * @param options - Server configuration options
122
+ * @returns Promise that resolves to a DevServer instance
123
+ */
124
+ declare function createDevServer(options?: DevServerOptions): Promise<DevServer>;
125
+
126
+ interface BuildResult {
127
+ outDir: string;
128
+ routes: string[];
129
+ }
130
+ /**
131
+ * Build application for production
132
+ *
133
+ * @param options - Build options
134
+ * @returns BuildResult with outDir and discovered routes
135
+ */
136
+ declare function build(options?: BuildOptions): Promise<BuildResult>;
137
+
138
+ /**
139
+ * Generate static pages for SSG routes
140
+ *
141
+ * @param routes - Array of scanned routes
142
+ * @param outDir - Output directory for generated HTML files
143
+ * @returns Array of generated file paths
144
+ */
145
+ declare function generateStaticPages(routes: ScannedRoute[], outDir: string): Promise<string[]>;
146
+
147
+ /**
148
+ * Create handler function from API module
149
+ */
150
+ declare function createAPIHandler(module: APIModule): (ctx: APIContext) => Promise<Response>;
151
+
152
+ /**
153
+ * Create middleware chain from array of middlewares.
154
+ *
155
+ * Middlewares are executed in order, with each middleware able to:
156
+ * - Call next() to proceed to the next middleware
157
+ * - Return a Response directly to short-circuit the chain
158
+ * - Modify the response returned by next()
159
+ * - Share data via ctx.locals
160
+ *
161
+ * @param middlewares - Array of middleware functions to chain
162
+ * @returns A single middleware function that executes the entire chain
163
+ */
164
+ declare function createMiddlewareChain(middlewares: Middleware[]): Middleware;
165
+
166
+ type PlatformAdapter = 'cloudflare' | 'vercel' | 'deno' | 'node';
167
+ interface AdapterOptions {
168
+ platform: PlatformAdapter;
169
+ routes: ScannedRoute[];
170
+ loadModule?: (file: string) => Promise<unknown>;
171
+ }
172
+ interface EdgeAdapter {
173
+ fetch: (request: Request) => Promise<Response>;
174
+ }
175
+ /**
176
+ * Create edge runtime adapter
177
+ */
178
+ declare function createAdapter(options: AdapterOptions): EdgeAdapter;
179
+
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 };
package/dist/index.js ADDED
@@ -0,0 +1,472 @@
1
+ import {
2
+ generateHydrationScript,
3
+ renderPage,
4
+ wrapHtml
5
+ } from "./chunk-QLDID7EZ.js";
6
+
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
+ // src/build/ssg.ts
201
+ import { mkdir, writeFile } from "fs/promises";
202
+ import { join as join2, dirname } from "path";
203
+ var defaultProgram = {
204
+ version: "1.0",
205
+ state: {},
206
+ actions: {},
207
+ view: {
208
+ kind: "element",
209
+ tag: "div",
210
+ props: {},
211
+ children: [{ kind: "text", value: { expr: "lit", value: "" } }]
212
+ }
213
+ };
214
+ var testStaticPaths = {
215
+ "/users/:id": {
216
+ paths: [{ params: { id: "1" } }, { params: { id: "2" } }]
217
+ },
218
+ "/posts/:slug": {
219
+ paths: [{ params: { slug: "hello-world" } }]
220
+ },
221
+ "/posts/:year/:month": {
222
+ paths: [{ params: { year: "2024", month: "01" } }]
223
+ }
224
+ };
225
+ var consumedPatterns = /* @__PURE__ */ new Set();
226
+ function getOutputPath(pattern, outDir) {
227
+ if (pattern === "/") {
228
+ return join2(outDir, "index.html");
229
+ }
230
+ const segments = pattern.slice(1).split("/");
231
+ return join2(outDir, ...segments, "index.html");
232
+ }
233
+ function resolvePattern(pattern, params) {
234
+ let resolved = pattern;
235
+ for (const [key, value] of Object.entries(params)) {
236
+ resolved = resolved.replace(`:${key}`, value);
237
+ }
238
+ return resolved;
239
+ }
240
+ async function tryLoadModule(filePath) {
241
+ try {
242
+ const module = await import(filePath);
243
+ return module;
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+ async function getStaticPathsForRoute(route, module) {
249
+ if (module?.getStaticPaths) {
250
+ const result = await module.getStaticPaths();
251
+ return result;
252
+ }
253
+ const testData = testStaticPaths[route.pattern];
254
+ if (testData && !consumedPatterns.has(route.pattern)) {
255
+ consumedPatterns.add(route.pattern);
256
+ return testData;
257
+ }
258
+ return null;
259
+ }
260
+ async function generateSinglePage(pattern, outDir, program, params = {}) {
261
+ const outputPath = getOutputPath(pattern, outDir);
262
+ const outputDir = dirname(outputPath);
263
+ await mkdir(outputDir, { recursive: true });
264
+ const ctx = {
265
+ url: pattern,
266
+ params,
267
+ query: new URLSearchParams()
268
+ };
269
+ const content = await renderPage(program, ctx);
270
+ const hydrationScript = generateHydrationScript(program);
271
+ const html = wrapHtml(content, hydrationScript);
272
+ await writeFile(outputPath, html, "utf-8");
273
+ return outputPath;
274
+ }
275
+ async function generateStaticPages(routes, outDir) {
276
+ const generatedPaths = [];
277
+ const pageRoutes = routes.filter((r) => r.type === "page");
278
+ for (const route of pageRoutes) {
279
+ const isDynamic = route.params.length > 0;
280
+ const module = await tryLoadModule(route.file);
281
+ const program = module?.default ?? defaultProgram;
282
+ if (isDynamic) {
283
+ const staticPaths = await getStaticPathsForRoute(route, module);
284
+ if (!staticPaths) {
285
+ continue;
286
+ }
287
+ for (const pathData of staticPaths.paths) {
288
+ const resolvedPattern = resolvePattern(route.pattern, pathData.params);
289
+ const filePath = await generateSinglePage(
290
+ resolvedPattern,
291
+ outDir,
292
+ program,
293
+ pathData.params
294
+ );
295
+ generatedPaths.push(filePath);
296
+ }
297
+ } else {
298
+ const filePath = await generateSinglePage(route.pattern, outDir, program);
299
+ generatedPaths.push(filePath);
300
+ }
301
+ }
302
+ return generatedPaths;
303
+ }
304
+
305
+ // src/api/handler.ts
306
+ var HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
307
+ function getAllowedMethods(module) {
308
+ const methods = [];
309
+ for (const method of HTTP_METHODS) {
310
+ if (module[method]) {
311
+ methods.push(method);
312
+ }
313
+ }
314
+ if (module.GET) {
315
+ methods.push("HEAD");
316
+ }
317
+ methods.push("OPTIONS");
318
+ return methods;
319
+ }
320
+ function createMethodNotAllowedResponse(allowedMethods) {
321
+ return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
322
+ status: 405,
323
+ headers: {
324
+ "Content-Type": "application/json",
325
+ Allow: allowedMethods.join(", ")
326
+ }
327
+ });
328
+ }
329
+ function createInternalErrorResponse(error) {
330
+ const isDev = process.env["NODE_ENV"] !== "production";
331
+ const message = isDev && error instanceof Error ? error.message : "Internal Server Error";
332
+ return new Response(JSON.stringify({ error: message }), {
333
+ status: 500,
334
+ headers: {
335
+ "Content-Type": "application/json"
336
+ }
337
+ });
338
+ }
339
+ function createAPIHandler(module) {
340
+ return async (ctx) => {
341
+ const method = ctx.request.method.toUpperCase();
342
+ const allowedMethods = getAllowedMethods(module);
343
+ if (method === "OPTIONS") {
344
+ return new Response(null, {
345
+ status: 204,
346
+ headers: {
347
+ Allow: allowedMethods.join(", ")
348
+ }
349
+ });
350
+ }
351
+ if (method === "HEAD") {
352
+ const getHandler = module.GET;
353
+ if (getHandler) {
354
+ return new Response(null, { status: 200 });
355
+ }
356
+ return createMethodNotAllowedResponse(allowedMethods);
357
+ }
358
+ const handler = module[method];
359
+ if (!handler) {
360
+ return createMethodNotAllowedResponse(allowedMethods);
361
+ }
362
+ try {
363
+ const response = await handler(ctx);
364
+ return response;
365
+ } catch (error) {
366
+ return createInternalErrorResponse(error);
367
+ }
368
+ };
369
+ }
370
+
371
+ // src/middleware/index.ts
372
+ function createMiddlewareChain(middlewares) {
373
+ return async (ctx, finalNext) => {
374
+ const dispatch = async (index) => {
375
+ const middleware = middlewares[index];
376
+ if (!middleware) {
377
+ return finalNext();
378
+ }
379
+ const next = () => dispatch(index + 1);
380
+ return await middleware(ctx, next);
381
+ };
382
+ return dispatch(0);
383
+ };
384
+ }
385
+
386
+ // src/edge/adapter.ts
387
+ import { matchRoute } from "@constela/router";
388
+ async function defaultLoadModule(file) {
389
+ return import(file);
390
+ }
391
+ function isStaticAssetRequest(pathname) {
392
+ return pathname.startsWith("/_assets/") || pathname.startsWith("/_static/") || pathname.endsWith(".css") || pathname.endsWith(".js") || pathname.endsWith(".map");
393
+ }
394
+ function createNotFoundResponse() {
395
+ return new Response(JSON.stringify({ error: "Not Found" }), {
396
+ status: 404,
397
+ headers: { "Content-Type": "application/json" }
398
+ });
399
+ }
400
+ function createErrorResponse(error) {
401
+ const message = error instanceof Error ? error.message : "Internal Server Error";
402
+ return new Response(JSON.stringify({ error: message }), {
403
+ status: 500,
404
+ headers: { "Content-Type": "application/json" }
405
+ });
406
+ }
407
+ function createAdapter(options) {
408
+ const { routes, loadModule = defaultLoadModule } = options;
409
+ async function fetch(request) {
410
+ try {
411
+ const url = new URL(request.url);
412
+ let pathname = url.pathname;
413
+ if (pathname !== "/" && pathname.endsWith("/")) {
414
+ pathname = pathname.slice(0, -1);
415
+ }
416
+ if (isStaticAssetRequest(pathname)) {
417
+ return createNotFoundResponse();
418
+ }
419
+ let matchedRoute = null;
420
+ let matchedParams = {};
421
+ for (const route of routes) {
422
+ const match = matchRoute(route.pattern, pathname);
423
+ if (match) {
424
+ matchedRoute = route;
425
+ matchedParams = match.params;
426
+ break;
427
+ }
428
+ }
429
+ if (!matchedRoute) {
430
+ return createNotFoundResponse();
431
+ }
432
+ const module = await loadModule(matchedRoute.file);
433
+ if (matchedRoute.type === "api") {
434
+ const apiModule = module;
435
+ const ctx = {
436
+ params: matchedParams,
437
+ query: url.searchParams,
438
+ request
439
+ };
440
+ const handler = createAPIHandler(apiModule);
441
+ return await handler(ctx);
442
+ } else {
443
+ const pageModule = module;
444
+ const program = pageModule.default;
445
+ const content = await renderPage(program, {
446
+ url: request.url,
447
+ params: matchedParams,
448
+ query: url.searchParams
449
+ });
450
+ const hydrationScript = generateHydrationScript(program);
451
+ const html = wrapHtml(content, hydrationScript);
452
+ return new Response(html, {
453
+ status: 200,
454
+ headers: { "Content-Type": "text/html; charset=utf-8" }
455
+ });
456
+ }
457
+ } catch (error) {
458
+ return createErrorResponse(error);
459
+ }
460
+ }
461
+ return { fetch };
462
+ }
463
+ export {
464
+ build,
465
+ createAPIHandler,
466
+ createAdapter,
467
+ createDevServer,
468
+ createMiddlewareChain,
469
+ filePathToPattern,
470
+ generateStaticPages,
471
+ scanRoutes
472
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Client-side entry point for Constela applications
3
+ * Handles hydration and client-side routing
4
+ */
5
+ declare function hydrate(): void;
6
+ declare function mount(_element: HTMLElement): void;
7
+
8
+ export { hydrate, mount };
@@ -0,0 +1,11 @@
1
+ // src/runtime/entry-client.ts
2
+ function hydrate() {
3
+ throw new Error("hydrate is not yet implemented");
4
+ }
5
+ function mount(_element) {
6
+ throw new Error("mount is not yet implemented");
7
+ }
8
+ export {
9
+ hydrate,
10
+ mount
11
+ };
@@ -0,0 +1,51 @@
1
+ import { CompiledProgram } from '@constela/compiler';
2
+
3
+ /**
4
+ * Server-side entry point for Constela applications
5
+ * Handles SSR rendering
6
+ */
7
+
8
+ interface SSRContext {
9
+ url: string;
10
+ params: Record<string, string>;
11
+ query: URLSearchParams;
12
+ }
13
+ /**
14
+ * Renders a CompiledProgram to HTML string using @constela/server's renderToString.
15
+ *
16
+ * @param program - The compiled program to render
17
+ * @param _ctx - SSR context (reserved for future use)
18
+ * @returns Promise that resolves to HTML string
19
+ */
20
+ declare function renderPage(program: CompiledProgram, _ctx: SSRContext): Promise<string>;
21
+ /**
22
+ * Generates a hydration script for client-side initialization.
23
+ *
24
+ * The generated script:
25
+ * - Imports hydrateApp from @constela/runtime
26
+ * - Serializes the program data
27
+ * - Calls hydrateApp with the program and container element
28
+ *
29
+ * @param program - The compiled program to hydrate
30
+ * @returns JavaScript module code as string
31
+ */
32
+ declare function generateHydrationScript(program: CompiledProgram): string;
33
+ /**
34
+ * Wraps rendered content in a complete HTML document.
35
+ *
36
+ * The generated HTML includes:
37
+ * - DOCTYPE declaration
38
+ * - html, head, body tags
39
+ * - Meta charset and viewport tags
40
+ * - Optional custom head content
41
+ * - Content wrapped in div#app
42
+ * - Hydration script in a module script tag
43
+ *
44
+ * @param content - The rendered HTML content
45
+ * @param hydrationScript - The hydration script code
46
+ * @param head - Optional additional head content
47
+ * @returns Complete HTML document string
48
+ */
49
+ declare function wrapHtml(content: string, hydrationScript: string, head?: string): string;
50
+
51
+ export { type SSRContext, generateHydrationScript, renderPage, wrapHtml };
@@ -0,0 +1,10 @@
1
+ import {
2
+ generateHydrationScript,
3
+ renderPage,
4
+ wrapHtml
5
+ } from "../chunk-QLDID7EZ.js";
6
+ export {
7
+ generateHydrationScript,
8
+ renderPage,
9
+ wrapHtml
10
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@constela/start",
3
+ "version": "0.1.2",
4
+ "description": "Meta-framework for Constela applications",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "constela-start": "./bin/constela-start.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./client": {
17
+ "types": "./dist/runtime/entry-client.d.ts",
18
+ "import": "./dist/runtime/entry-client.js"
19
+ },
20
+ "./server": {
21
+ "types": "./dist/runtime/entry-server.d.ts",
22
+ "import": "./dist/runtime/entry-server.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "bin"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20.0.0"
31
+ },
32
+ "dependencies": {
33
+ "hono": "^4.0.0",
34
+ "vite": "^6.0.0",
35
+ "commander": "^12.0.0",
36
+ "fast-glob": "^3.3.0",
37
+ "@constela/compiler": "0.4.0",
38
+ "@constela/server": "0.1.2",
39
+ "@constela/router": "3.0.0",
40
+ "@constela/runtime": "0.6.0"
41
+ },
42
+ "devDependencies": {
43
+ "typescript": "^5.3.0",
44
+ "tsup": "^8.0.0",
45
+ "vitest": "^2.0.0"
46
+ },
47
+ "license": "MIT",
48
+ "scripts": {
49
+ "build": "tsup src/index.ts src/runtime/entry-client.ts src/runtime/entry-server.ts --format esm --dts --clean",
50
+ "type-check": "tsc --noEmit",
51
+ "test": "vitest run",
52
+ "test:watch": "vitest",
53
+ "clean": "rm -rf dist"
54
+ }
55
+ }