@constela/start 1.5.5 → 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
|
|
1476
|
-
if (!
|
|
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
|
|
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 =
|
|
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 :
|
|
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 :
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 :
|
|
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 :
|
|
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 :
|
|
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 =
|
|
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
|
|
2964
|
+
return join10(outDir, withoutExt + ".html");
|
|
2664
2965
|
}
|
|
2665
|
-
return
|
|
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
|
|
2976
|
+
return join10(outDir, "index.html");
|
|
2676
2977
|
}
|
|
2677
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3125
|
-
const destPath =
|
|
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 =
|
|
3196
|
-
const relPathFromProjectRoot =
|
|
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 =
|
|
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 =
|
|
3569
|
+
outputPath = join10(outDir, "index.html");
|
|
3269
3570
|
} else {
|
|
3270
|
-
outputPath =
|
|
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
|
|
3599
|
+
import { join as join11 } from "path";
|
|
3299
3600
|
var CONFIG_FILENAME = "constela.config.json";
|
|
3300
3601
|
async function loadConfig(projectRoot) {
|
|
3301
|
-
const configPath =
|
|
3602
|
+
const configPath = join11(projectRoot, CONFIG_FILENAME);
|
|
3302
3603
|
if (!existsSync9(configPath)) {
|
|
3303
3604
|
return {};
|
|
3304
3605
|
}
|
package/dist/cli/index.js
CHANGED
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
|
@@ -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 {
|
|
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.
|
|
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
|
-
"
|
|
47
|
-
"@constela/runtime": "0.15.2",
|
|
46
|
+
"ws": "^8.18.0",
|
|
48
47
|
"@constela/compiler": "0.11.1",
|
|
49
|
-
"@constela/
|
|
50
|
-
"@constela/server": "8.0.1"
|
|
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",
|