@constela/start 1.5.4 → 1.6.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.
@@ -1472,8 +1472,8 @@ async function scanLayouts(layoutsDir) {
1472
1472
  if (!existsSync5(layoutsDir)) {
1473
1473
  throw new Error(`Layouts directory does not exist: ${layoutsDir}`);
1474
1474
  }
1475
- const stat2 = statSync3(layoutsDir);
1476
- if (!stat2.isDirectory()) {
1475
+ const stat3 = statSync3(layoutsDir);
1476
+ if (!stat3.isDirectory()) {
1477
1477
  throw new Error(`Path is not a directory: ${layoutsDir}`);
1478
1478
  }
1479
1479
  const files = await fg3(["**/*.ts", "**/*.tsx", "**/*.json"], {
@@ -1614,11 +1614,11 @@ var LayoutResolver = class {
1614
1614
  };
1615
1615
 
1616
1616
  // src/dev/server.ts
1617
- import { createServer } from "http";
1617
+ import { createServer as createServer2 } from "http";
1618
1618
  import { createReadStream } from "fs";
1619
- import { join as join7, isAbsolute } from "path";
1619
+ import { join as join8, isAbsolute, relative as relative3 } from "path";
1620
1620
  import { createServer as createViteServer } from "vite";
1621
- import { isCookieInitialExpr as isCookieInitialExpr2 } from "@constela/core";
1621
+ import { isCookieInitialExpr as isCookieInitialExpr2, ConstelaError } from "@constela/core";
1622
1622
 
1623
1623
  // src/json-page-loader.ts
1624
1624
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
@@ -2175,6 +2175,243 @@ var JsonPageLoader = class {
2175
2175
 
2176
2176
  // src/dev/server.ts
2177
2177
  import { analyzeLayoutPass, transformLayoutPass, composeLayoutWithPage } from "@constela/compiler";
2178
+
2179
+ // src/dev/hmr-server.ts
2180
+ import { WebSocketServer } from "ws";
2181
+ import { createServer } from "http";
2182
+ function createHMRServer(options) {
2183
+ return new Promise((resolve5, reject) => {
2184
+ const { port } = options;
2185
+ const httpServer = createServer();
2186
+ const wss = new WebSocketServer({ server: httpServer });
2187
+ const clients = /* @__PURE__ */ new Set();
2188
+ let isClosed = false;
2189
+ wss.on("connection", (ws) => {
2190
+ clients.add(ws);
2191
+ const connectedMessage = { type: "connected" };
2192
+ ws.send(JSON.stringify(connectedMessage));
2193
+ ws.on("close", () => {
2194
+ clients.delete(ws);
2195
+ });
2196
+ ws.on("error", () => {
2197
+ clients.delete(ws);
2198
+ });
2199
+ });
2200
+ httpServer.on("error", (error) => {
2201
+ reject(error);
2202
+ });
2203
+ httpServer.listen(port, () => {
2204
+ const address = httpServer.address();
2205
+ const actualPort = address.port;
2206
+ const server = {
2207
+ get port() {
2208
+ return actualPort;
2209
+ },
2210
+ get connectedClients() {
2211
+ return clients.size;
2212
+ },
2213
+ broadcastUpdate(file, program) {
2214
+ if (clients.size === 0) {
2215
+ return;
2216
+ }
2217
+ const message = {
2218
+ type: "update",
2219
+ file,
2220
+ program
2221
+ };
2222
+ const data = JSON.stringify(message);
2223
+ for (const client of clients) {
2224
+ if (client.readyState === client.OPEN) {
2225
+ client.send(data);
2226
+ }
2227
+ }
2228
+ },
2229
+ broadcastError(file, errors) {
2230
+ if (clients.size === 0) {
2231
+ return;
2232
+ }
2233
+ const message = {
2234
+ type: "error",
2235
+ file,
2236
+ errors: errors.map((error) => error.toJSON())
2237
+ };
2238
+ const data = JSON.stringify(message);
2239
+ for (const client of clients) {
2240
+ if (client.readyState === client.OPEN) {
2241
+ client.send(data);
2242
+ }
2243
+ }
2244
+ },
2245
+ close() {
2246
+ return new Promise((resolveClose) => {
2247
+ if (isClosed) {
2248
+ resolveClose();
2249
+ return;
2250
+ }
2251
+ isClosed = true;
2252
+ for (const client of clients) {
2253
+ client.close();
2254
+ }
2255
+ clients.clear();
2256
+ wss.close(() => {
2257
+ httpServer.close(() => {
2258
+ resolveClose();
2259
+ });
2260
+ });
2261
+ });
2262
+ }
2263
+ };
2264
+ resolve5(server);
2265
+ });
2266
+ });
2267
+ }
2268
+
2269
+ // src/dev/watcher.ts
2270
+ import { watch } from "fs";
2271
+ import { stat } from "fs/promises";
2272
+ import { join as join7, relative as relative2 } from "path";
2273
+ import { EventEmitter } from "events";
2274
+ var DEFAULT_DEBOUNCE_MS = 100;
2275
+ var DEFAULT_PATTERNS = ["**/*.json"];
2276
+ function shouldIgnore(filePath, baseDir) {
2277
+ const relativePath = relative2(baseDir, filePath);
2278
+ const segments = relativePath.split("/");
2279
+ for (const segment of segments) {
2280
+ if (segment.startsWith(".")) {
2281
+ return true;
2282
+ }
2283
+ if (segment === "node_modules") {
2284
+ return true;
2285
+ }
2286
+ }
2287
+ return false;
2288
+ }
2289
+ function createPatternMatcher(pattern) {
2290
+ if (pattern.startsWith("**/")) {
2291
+ const suffix = pattern.slice(3);
2292
+ if (suffix.startsWith("*.")) {
2293
+ const ext = suffix.slice(1);
2294
+ return (path) => path.endsWith(ext);
2295
+ }
2296
+ return (path) => path.endsWith(suffix) || path.endsWith("/" + suffix);
2297
+ }
2298
+ if (pattern.startsWith("*.")) {
2299
+ const ext = pattern.slice(1);
2300
+ return (path) => {
2301
+ if (path.includes("/")) {
2302
+ return false;
2303
+ }
2304
+ return path.endsWith(ext);
2305
+ };
2306
+ }
2307
+ return (path) => path === pattern;
2308
+ }
2309
+ function matchesPatterns(filePath, baseDir, matchers) {
2310
+ const relativePath = relative2(baseDir, filePath);
2311
+ for (const matcher of matchers) {
2312
+ if (matcher(relativePath)) {
2313
+ return true;
2314
+ }
2315
+ }
2316
+ return false;
2317
+ }
2318
+ async function createWatcher(options) {
2319
+ const {
2320
+ directory,
2321
+ debounceMs = DEFAULT_DEBOUNCE_MS,
2322
+ patterns = DEFAULT_PATTERNS
2323
+ } = options;
2324
+ try {
2325
+ const stats = await stat(directory);
2326
+ if (!stats.isDirectory()) {
2327
+ throw new Error(`Path is not a directory: ${directory}`);
2328
+ }
2329
+ } catch (error) {
2330
+ if (error.code === "ENOENT") {
2331
+ throw new Error(`Directory does not exist: ${directory}`);
2332
+ }
2333
+ throw error;
2334
+ }
2335
+ const matchers = patterns.map((pattern) => createPatternMatcher(pattern));
2336
+ const emitter = new EventEmitter();
2337
+ const pendingEvents = /* @__PURE__ */ new Map();
2338
+ let isClosed = false;
2339
+ let fsWatcher = null;
2340
+ try {
2341
+ fsWatcher = watch(directory, { recursive: true }, (eventType, filename) => {
2342
+ if (isClosed || !filename) {
2343
+ return;
2344
+ }
2345
+ const absolutePath = join7(directory, filename);
2346
+ if (shouldIgnore(absolutePath, directory)) {
2347
+ return;
2348
+ }
2349
+ if (!matchesPatterns(absolutePath, directory, matchers)) {
2350
+ return;
2351
+ }
2352
+ const existingTimeout = pendingEvents.get(absolutePath);
2353
+ if (existingTimeout) {
2354
+ clearTimeout(existingTimeout);
2355
+ }
2356
+ const timeout = setTimeout(async () => {
2357
+ pendingEvents.delete(absolutePath);
2358
+ if (isClosed) {
2359
+ return;
2360
+ }
2361
+ let changeType = "change";
2362
+ try {
2363
+ await stat(absolutePath);
2364
+ changeType = "change";
2365
+ } catch {
2366
+ changeType = "unlink";
2367
+ }
2368
+ const event = {
2369
+ path: absolutePath,
2370
+ type: changeType
2371
+ };
2372
+ emitter.emit("change", event);
2373
+ }, debounceMs);
2374
+ pendingEvents.set(absolutePath, timeout);
2375
+ });
2376
+ fsWatcher.on("error", (error) => {
2377
+ if (!isClosed) {
2378
+ emitter.emit("error", error);
2379
+ }
2380
+ });
2381
+ } catch (error) {
2382
+ throw error;
2383
+ }
2384
+ const watcher = {
2385
+ on(event, listener) {
2386
+ emitter.on(event, listener);
2387
+ },
2388
+ once(event, listener) {
2389
+ emitter.once(event, listener);
2390
+ },
2391
+ close() {
2392
+ return new Promise((resolve5) => {
2393
+ if (isClosed) {
2394
+ resolve5();
2395
+ return;
2396
+ }
2397
+ isClosed = true;
2398
+ for (const timeout of pendingEvents.values()) {
2399
+ clearTimeout(timeout);
2400
+ }
2401
+ pendingEvents.clear();
2402
+ if (fsWatcher) {
2403
+ fsWatcher.close();
2404
+ fsWatcher = null;
2405
+ }
2406
+ emitter.removeAllListeners();
2407
+ resolve5();
2408
+ });
2409
+ }
2410
+ };
2411
+ return watcher;
2412
+ }
2413
+
2414
+ // src/dev/server.ts
2178
2415
  var DEFAULT_PORT = 3e3;
2179
2416
  var DEFAULT_HOST = "localhost";
2180
2417
  var DEFAULT_PUBLIC_DIR = "public";
@@ -2266,13 +2503,15 @@ async function createDevServer(options = {}) {
2266
2503
  port = DEFAULT_PORT,
2267
2504
  host = DEFAULT_HOST,
2268
2505
  routesDir = DEFAULT_ROUTES_DIR,
2269
- publicDir = join7(process.cwd(), DEFAULT_PUBLIC_DIR),
2506
+ publicDir = join8(process.cwd(), DEFAULT_PUBLIC_DIR),
2270
2507
  layoutsDir,
2271
2508
  css
2272
2509
  } = options;
2273
2510
  let httpServer = null;
2274
2511
  let actualPort = port;
2275
2512
  let viteServer = null;
2513
+ let hmrServer = null;
2514
+ let watcher = null;
2276
2515
  if (css) {
2277
2516
  viteServer = await createViteServer({
2278
2517
  root: process.cwd(),
@@ -2281,7 +2520,7 @@ async function createDevServer(options = {}) {
2281
2520
  logLevel: "silent"
2282
2521
  });
2283
2522
  }
2284
- const absoluteRoutesDir = isAbsolute(routesDir) ? routesDir : join7(process.cwd(), routesDir);
2523
+ const absoluteRoutesDir = isAbsolute(routesDir) ? routesDir : join8(process.cwd(), routesDir);
2285
2524
  let routes = [];
2286
2525
  try {
2287
2526
  routes = await scanRoutes(absoluteRoutesDir);
@@ -2290,7 +2529,7 @@ async function createDevServer(options = {}) {
2290
2529
  }
2291
2530
  let layoutResolver = null;
2292
2531
  if (layoutsDir) {
2293
- const absoluteLayoutsDir = isAbsolute(layoutsDir) ? layoutsDir : join7(process.cwd(), layoutsDir);
2532
+ const absoluteLayoutsDir = isAbsolute(layoutsDir) ? layoutsDir : join8(process.cwd(), layoutsDir);
2294
2533
  layoutResolver = new LayoutResolver(absoluteLayoutsDir);
2295
2534
  await layoutResolver.initialize();
2296
2535
  }
@@ -2298,9 +2537,12 @@ async function createDevServer(options = {}) {
2298
2537
  get port() {
2299
2538
  return actualPort;
2300
2539
  },
2540
+ get hmrPort() {
2541
+ return hmrServer?.port ?? 0;
2542
+ },
2301
2543
  async listen() {
2302
2544
  return new Promise((resolve5, reject) => {
2303
- httpServer = createServer(async (req, res) => {
2545
+ httpServer = createServer2(async (req, res) => {
2304
2546
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
2305
2547
  const pathname = url.pathname;
2306
2548
  if (viteServer) {
@@ -2385,7 +2627,8 @@ async function createDevServer(options = {}) {
2385
2627
  path: pathname
2386
2628
  });
2387
2629
  const cssHead = css ? (Array.isArray(css) ? css : [css]).map((p) => `<link rel="stylesheet" href="/${p}">`).join("\n") : "";
2388
- const head = [metaTags, cssHead].filter(Boolean).join("\n");
2630
+ const hmrScript = hmrServer ? `<script>window.__CONSTELA_HMR_URL__ = "ws://${host}:${hmrServer.port}";</script>` : "";
2631
+ const head = [metaTags, cssHead, hmrScript].filter(Boolean).join("\n");
2389
2632
  const themeState = composedProgram.state?.["theme"];
2390
2633
  let initialTheme;
2391
2634
  if (themeState) {
@@ -2458,16 +2701,74 @@ h1 { color: #666; }
2458
2701
  httpServer.on("error", (err) => {
2459
2702
  reject(err);
2460
2703
  });
2461
- httpServer.listen(port, host, () => {
2704
+ httpServer.listen(port, host, async () => {
2462
2705
  const address = httpServer?.address();
2463
2706
  if (address) {
2464
2707
  actualPort = address.port;
2465
2708
  }
2709
+ try {
2710
+ hmrServer = await createHMRServer({ port: 0 });
2711
+ watcher = await createWatcher({
2712
+ directory: absoluteRoutesDir,
2713
+ patterns: ["**/*.json"]
2714
+ });
2715
+ watcher.on("change", async (event) => {
2716
+ const projectRoot = process.cwd();
2717
+ const pageLoader = new JsonPageLoader(projectRoot);
2718
+ try {
2719
+ const relativePath = relative3(projectRoot, event.path).replace(/\\/g, "/");
2720
+ const pageInfo = await pageLoader.loadPage(relativePath);
2721
+ const program = await convertToCompiledProgram(pageInfo);
2722
+ if (hmrServer) {
2723
+ hmrServer.broadcastUpdate(event.path, program);
2724
+ }
2725
+ } catch (error) {
2726
+ if (hmrServer) {
2727
+ if (error instanceof ConstelaError) {
2728
+ hmrServer.broadcastError(event.path, [error]);
2729
+ } else {
2730
+ const genericError = {
2731
+ code: "COMPILE_ERROR",
2732
+ message: error instanceof Error ? error.message : String(error),
2733
+ path: event.path,
2734
+ severity: "error",
2735
+ suggestion: void 0,
2736
+ expected: void 0,
2737
+ actual: void 0,
2738
+ context: void 0,
2739
+ name: "ConstelaError",
2740
+ toJSON: () => ({
2741
+ code: "COMPILE_ERROR",
2742
+ message: error instanceof Error ? error.message : String(error),
2743
+ path: event.path,
2744
+ severity: "error",
2745
+ suggestion: void 0,
2746
+ expected: void 0,
2747
+ actual: void 0,
2748
+ context: void 0
2749
+ })
2750
+ };
2751
+ hmrServer.broadcastError(event.path, [genericError]);
2752
+ }
2753
+ }
2754
+ }
2755
+ });
2756
+ } catch (hmrError) {
2757
+ console.warn("HMR initialization failed:", hmrError);
2758
+ }
2466
2759
  resolve5();
2467
2760
  });
2468
2761
  });
2469
2762
  },
2470
2763
  async close() {
2764
+ if (watcher) {
2765
+ await watcher.close();
2766
+ watcher = null;
2767
+ }
2768
+ if (hmrServer) {
2769
+ await hmrServer.close();
2770
+ hmrServer = null;
2771
+ }
2471
2772
  if (viteServer) {
2472
2773
  await viteServer.close();
2473
2774
  viteServer = null;
@@ -2495,7 +2796,7 @@ h1 { color: #666; }
2495
2796
  // src/build/index.ts
2496
2797
  import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
2497
2798
  import { mkdir as mkdir2, writeFile, cp, readdir } from "fs/promises";
2498
- import { join as join9, dirname as dirname5, relative as relative3, basename as basename4, isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
2799
+ import { join as join10, dirname as dirname5, relative as relative5, basename as basename4, isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
2499
2800
  import { isCookieInitialExpr as isCookieInitialExpr3 } from "@constela/core";
2500
2801
 
2501
2802
  // src/build/bundler.ts
@@ -2503,14 +2804,14 @@ import * as esbuild from "esbuild";
2503
2804
  import { existsSync as existsSync7 } from "fs";
2504
2805
  import { mkdir, readFile } from "fs/promises";
2505
2806
  import { createRequire } from "module";
2506
- import { join as join8, dirname as dirname4, isAbsolute as isAbsolute2, relative as relative2 } from "path";
2807
+ import { join as join9, dirname as dirname4, isAbsolute as isAbsolute2, relative as relative4 } from "path";
2507
2808
  import { fileURLToPath } from "url";
2508
2809
  var __dirname = dirname4(fileURLToPath(import.meta.url));
2509
2810
  async function bundleRuntime(options) {
2510
2811
  const entryContent = `
2511
2812
  export { hydrateApp, createApp } from '@constela/runtime';
2512
2813
  `;
2513
- const outFile = join8(options.outDir, "_constela", "runtime.js");
2814
+ const outFile = join9(options.outDir, "_constela", "runtime.js");
2514
2815
  await mkdir(dirname4(outFile), { recursive: true });
2515
2816
  try {
2516
2817
  await esbuild.build({
@@ -2535,10 +2836,10 @@ async function bundleRuntime(options) {
2535
2836
  }
2536
2837
  async function bundleCSS(options) {
2537
2838
  const cssFiles = Array.isArray(options.css) ? options.css : [options.css];
2538
- const outFile = join8(options.outDir, "_constela", "styles.css");
2839
+ const outFile = join9(options.outDir, "_constela", "styles.css");
2539
2840
  const shouldMinify = options.minify ?? true;
2540
2841
  await mkdir(dirname4(outFile), { recursive: true });
2541
- const resolvedCssFiles = cssFiles.map((f) => isAbsolute2(f) ? f : join8(process.cwd(), f));
2842
+ const resolvedCssFiles = cssFiles.map((f) => isAbsolute2(f) ? f : join9(process.cwd(), f));
2542
2843
  for (const fullPath of resolvedCssFiles) {
2543
2844
  if (!existsSync7(fullPath)) {
2544
2845
  throw new Error(`CSS file not found: ${fullPath}`);
@@ -2567,7 +2868,7 @@ async function bundleCSS(options) {
2567
2868
  if (options.content.length > 0) {
2568
2869
  const fg4 = (await import("fast-glob")).default;
2569
2870
  const resolvedContentPaths = options.content.map(
2570
- (p) => isAbsolute2(p) ? p : join8(process.cwd(), p)
2871
+ (p) => isAbsolute2(p) ? p : join9(process.cwd(), p)
2571
2872
  );
2572
2873
  const matchedFiles = await fg4(resolvedContentPaths, { onlyFiles: true });
2573
2874
  if (matchedFiles.length === 0) {
@@ -2578,9 +2879,9 @@ async function bundleCSS(options) {
2578
2879
  }
2579
2880
  const sourceDir = dirname4(firstCssFile);
2580
2881
  const sourceDirectives = options.content.map((contentPath) => {
2581
- const absolutePath = isAbsolute2(contentPath) ? contentPath : join8(process.cwd(), contentPath);
2882
+ const absolutePath = isAbsolute2(contentPath) ? contentPath : join9(process.cwd(), contentPath);
2582
2883
  const srcPath = absolutePath.includes("*") ? dirname4(absolutePath.split("*")[0] ?? absolutePath) : absolutePath;
2583
- const relativePath = relative2(sourceDir, srcPath);
2884
+ const relativePath = relative4(sourceDir, srcPath);
2584
2885
  return `@source "${relativePath}";`;
2585
2886
  }).join("\n");
2586
2887
  const processedCssInput = sourceDirectives + "\n" + cssContent;
@@ -2660,9 +2961,9 @@ function isDynamicRoute(pattern) {
2660
2961
  function getOutputPath(filePath, outDir) {
2661
2962
  const withoutExt = filePath.replace(/\.(json|ts|tsx|js|jsx)$/, "");
2662
2963
  if (withoutExt === "index" || withoutExt.endsWith("/index")) {
2663
- return join9(outDir, withoutExt + ".html");
2964
+ return join10(outDir, withoutExt + ".html");
2664
2965
  }
2665
- return join9(outDir, withoutExt, "index.html");
2966
+ return join10(outDir, withoutExt, "index.html");
2666
2967
  }
2667
2968
  function paramsToOutputPath(basePattern, params, outDir) {
2668
2969
  let path = basePattern;
@@ -2672,14 +2973,14 @@ function paramsToOutputPath(basePattern, params, outDir) {
2672
2973
  }
2673
2974
  const relativePath = path.startsWith("/") ? path.slice(1) : path;
2674
2975
  if (relativePath === "") {
2675
- return join9(outDir, "index.html");
2976
+ return join10(outDir, "index.html");
2676
2977
  }
2677
- return join9(outDir, relativePath, "index.html");
2978
+ return join10(outDir, relativePath, "index.html");
2678
2979
  }
2679
2980
  async function loadGetStaticPaths(pageFile) {
2680
2981
  const dir = dirname5(pageFile);
2681
2982
  const baseName = basename4(pageFile, ".json");
2682
- const pathsFile = join9(dir, `${baseName}.paths.ts`);
2983
+ const pathsFile = join10(dir, `${baseName}.paths.ts`);
2683
2984
  if (!existsSync8(pathsFile)) {
2684
2985
  return null;
2685
2986
  }
@@ -2720,7 +3021,7 @@ async function loadGetStaticPaths(pageFile) {
2720
3021
  }
2721
3022
  }
2722
3023
  async function loadLayout2(layoutName, layoutsDir) {
2723
- const layoutPath = join9(layoutsDir, `${layoutName}.json`);
3024
+ const layoutPath = join10(layoutsDir, `${layoutName}.json`);
2724
3025
  if (!existsSync8(layoutPath)) {
2725
3026
  throw new Error(`Layout "${layoutName}" not found at ${layoutPath}`);
2726
3027
  }
@@ -3121,8 +3422,8 @@ async function copyPublicDir(publicDir, outDir, generatedFiles) {
3121
3422
  async function copyDirRecursive(srcDir, destDir, skipFiles) {
3122
3423
  const entries = await readdir(srcDir, { withFileTypes: true });
3123
3424
  for (const entry of entries) {
3124
- const srcPath = join9(srcDir, entry.name);
3125
- const destPath = join9(destDir, entry.name);
3425
+ const srcPath = join10(srcDir, entry.name);
3426
+ const destPath = join10(destDir, entry.name);
3126
3427
  if (entry.isDirectory()) {
3127
3428
  await mkdir2(destPath, { recursive: true });
3128
3429
  await copyDirRecursive(srcPath, destPath, skipFiles);
@@ -3192,8 +3493,8 @@ async function build2(options) {
3192
3493
  const absoluteRoutesDir = isAbsolute3(routesDir) ? routesDir : resolve4(routesDir);
3193
3494
  const projectRoot = dirname5(dirname5(absoluteRoutesDir));
3194
3495
  for (const route of jsonPages) {
3195
- const relPathFromRoutesDir = relative3(absoluteRoutesDir, route.file);
3196
- const relPathFromProjectRoot = relative3(projectRoot, route.file);
3496
+ const relPathFromRoutesDir = relative5(absoluteRoutesDir, route.file);
3497
+ const relPathFromProjectRoot = relative5(projectRoot, route.file);
3197
3498
  const content = readFileSync5(route.file, "utf-8");
3198
3499
  const page = validateJsonPage(content, route.file);
3199
3500
  if (isDynamicRoute(route.pattern)) {
@@ -3243,7 +3544,7 @@ async function build2(options) {
3243
3544
  generatedFiles.push(outputPath);
3244
3545
  const slugValue = params["slug"];
3245
3546
  if (slugValue && (slugValue === "index" || slugValue.endsWith("/index"))) {
3246
- const parentOutputPath = join9(dirname5(dirname5(outputPath)), "index.html");
3547
+ const parentOutputPath = join10(dirname5(dirname5(outputPath)), "index.html");
3247
3548
  if (!generatedFiles.includes(parentOutputPath)) {
3248
3549
  await mkdir2(dirname5(parentOutputPath), { recursive: true });
3249
3550
  await writeFile(parentOutputPath, html, "utf-8");
@@ -3265,9 +3566,9 @@ async function build2(options) {
3265
3566
  const routePath = pageInfo.page.route.path;
3266
3567
  const relativePath = routePath.startsWith("/") ? routePath.slice(1) : routePath;
3267
3568
  if (relativePath === "" || relativePath === "/") {
3268
- outputPath = join9(outDir, "index.html");
3569
+ outputPath = join10(outDir, "index.html");
3269
3570
  } else {
3270
- outputPath = join9(outDir, relativePath, "index.html");
3571
+ outputPath = join10(outDir, relativePath, "index.html");
3271
3572
  }
3272
3573
  } else {
3273
3574
  outputPath = getOutputPath(relPathFromRoutesDir, outDir);
@@ -3295,10 +3596,10 @@ async function build2(options) {
3295
3596
 
3296
3597
  // src/config/config-loader.ts
3297
3598
  import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
3298
- import { join as join10 } from "path";
3599
+ import { join as join11 } from "path";
3299
3600
  var CONFIG_FILENAME = "constela.config.json";
3300
3601
  async function loadConfig(projectRoot) {
3301
- const configPath = join10(projectRoot, CONFIG_FILENAME);
3602
+ const configPath = join11(projectRoot, CONFIG_FILENAME);
3302
3603
  if (!existsSync9(configPath)) {
3303
3604
  return {};
3304
3605
  }
package/dist/cli/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  hyperlink,
5
5
  loadConfig,
6
6
  resolveConfig
7
- } from "../chunk-FKQKP2UH.js";
7
+ } from "../chunk-LKFIMB3Q.js";
8
8
  import "../chunk-PG3Y4EJN.js";
9
9
 
10
10
  // src/cli/index.ts
package/dist/index.d.ts CHANGED
@@ -140,6 +140,8 @@ interface DevServer {
140
140
  close(): Promise<void>;
141
141
  /** The port number the server is listening on */
142
142
  port: number;
143
+ /** The port number the HMR WebSocket server is listening on */
144
+ hmrPort: number;
143
145
  }
144
146
  /**
145
147
  * Creates a development server with HMR support.
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  transformCsv,
29
29
  transformMdx,
30
30
  transformYaml
31
- } from "./chunk-FKQKP2UH.js";
31
+ } from "./chunk-LKFIMB3Q.js";
32
32
  import {
33
33
  evaluateMetaExpression,
34
34
  generateHydrationScript,
@@ -46,5 +46,24 @@ interface InitClientOptions {
46
46
  * @returns AppInstance for controlling the application
47
47
  */
48
48
  declare function initClient(options: InitClientOptions): AppInstance;
49
+ /**
50
+ * Options for initializing the client application with HMR support
51
+ */
52
+ interface InitClientWithHMROptions extends InitClientOptions {
53
+ /** WebSocket URL for HMR server (e.g., "ws://localhost:3001") */
54
+ hmrUrl?: string;
55
+ }
56
+ /**
57
+ * Initialize the client application with HMR (Hot Module Replacement) support.
58
+ *
59
+ * This function extends initClient with automatic HMR setup:
60
+ * - Connects to HMR WebSocket server
61
+ * - Handles update messages by preserving state and re-hydrating
62
+ * - Shows error overlay on compilation errors
63
+ *
64
+ * @param options - Configuration options including optional HMR URL
65
+ * @returns AppInstance for controlling the application
66
+ */
67
+ declare function initClientWithHMR(options: InitClientWithHMROptions): AppInstance;
49
68
 
50
- export { type EscapeContext, type EscapeHandler, type InitClientOptions, type RouteContext, initClient };
69
+ export { type EscapeContext, type EscapeHandler, type InitClientOptions, type InitClientWithHMROptions, type RouteContext, initClient, initClientWithHMR };
@@ -1,5 +1,10 @@
1
1
  // src/runtime/entry-client.ts
2
- import { hydrateApp } from "@constela/runtime";
2
+ import {
3
+ hydrateApp,
4
+ createHMRClient,
5
+ createHMRHandler,
6
+ createErrorOverlay
7
+ } from "@constela/runtime";
3
8
  function initClient(options) {
4
9
  const { program, container, escapeHandlers = [], route } = options;
5
10
  const appInstance = hydrateApp({ program, container, ...route && { route } });
@@ -67,6 +72,59 @@ function initClient(options) {
67
72
  }
68
73
  };
69
74
  }
75
+ function initClientWithHMR(options) {
76
+ const { hmrUrl, ...clientOptions } = options;
77
+ const app = initClient(clientOptions);
78
+ if (hmrUrl) {
79
+ const overlay = createErrorOverlay();
80
+ const handlerOptions = {
81
+ container: options.container,
82
+ program: options.program,
83
+ ...options.route && { route: options.route }
84
+ };
85
+ const handler = createHMRHandler(handlerOptions);
86
+ const client = createHMRClient({
87
+ url: hmrUrl,
88
+ onUpdate: (_file, program) => {
89
+ overlay.hide();
90
+ handler.handleUpdate(program);
91
+ },
92
+ onError: (file, errors) => {
93
+ overlay.show({
94
+ file,
95
+ errors: errors.map((e) => {
96
+ if (typeof e === "object" && e !== null) {
97
+ const errObj = e;
98
+ const result = {
99
+ message: errObj.message ?? "Unknown error"
100
+ };
101
+ if (errObj.code !== void 0) result.code = errObj.code;
102
+ if (errObj.suggestion !== void 0) result.suggestion = errObj.suggestion;
103
+ return result;
104
+ }
105
+ return { message: String(e) };
106
+ })
107
+ });
108
+ },
109
+ onConnect: () => {
110
+ console.log("[HMR] Connected");
111
+ },
112
+ onDisconnect: () => {
113
+ console.log("[HMR] Disconnected");
114
+ }
115
+ });
116
+ client.connect();
117
+ const originalDestroy = app.destroy;
118
+ app.destroy = () => {
119
+ client.disconnect();
120
+ handler.destroy();
121
+ overlay.hide();
122
+ originalDestroy();
123
+ };
124
+ }
125
+ return app;
126
+ }
70
127
  export {
71
- initClient
128
+ initClient,
129
+ initClientWithHMR
72
130
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/start",
3
- "version": "1.5.4",
3
+ "version": "1.6.0",
4
4
  "description": "Meta-framework for Constela applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -43,13 +43,15 @@
43
43
  "postcss": "^8.5.0",
44
44
  "@tailwindcss/postcss": "^4.0.0",
45
45
  "tailwindcss": "^4.0.0",
46
- "@constela/core": "0.12.0",
47
- "@constela/runtime": "0.15.1",
46
+ "ws": "^8.18.0",
48
47
  "@constela/compiler": "0.11.1",
49
- "@constela/server": "8.0.0",
50
- "@constela/router": "13.0.0"
48
+ "@constela/core": "0.12.0",
49
+ "@constela/server": "8.0.1",
50
+ "@constela/runtime": "0.16.0",
51
+ "@constela/router": "14.0.0"
51
52
  },
52
53
  "devDependencies": {
54
+ "@types/ws": "^8.5.0",
53
55
  "@types/mdast": "^4.0.4",
54
56
  "tsup": "^8.0.0",
55
57
  "typescript": "^5.3.0",