@zenithbuild/cli 0.5.0-beta.2.19 → 0.5.0-beta.2.20

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.
Files changed (2) hide show
  1. package/dist/dev-server.js +115 -55
  2. package/package.json +2 -2
@@ -5,16 +5,16 @@
5
5
  //
6
6
  // - Compiles pages on demand
7
7
  // - Rebuilds on file change
8
- // - Injects HMR client script
8
+ // - Exposes V1 HMR endpoints consumed by runtime dev client
9
9
  // - Server route resolution uses manifest matching
10
10
  //
11
11
  // V0: Uses Node.js http module + fs.watch. No external deps.
12
12
  // ---------------------------------------------------------------------------
13
13
 
14
14
  import { createServer } from 'node:http';
15
- import { watch } from 'node:fs';
15
+ import { existsSync, watch } from 'node:fs';
16
16
  import { readFile } from 'node:fs/promises';
17
- import { join, extname } from 'node:path';
17
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
18
18
  import { build } from './build.js';
19
19
  import {
20
20
  executeServerRoute,
@@ -52,9 +52,19 @@ export async function createDevServer(options) {
52
52
  config = {}
53
53
  } = options;
54
54
 
55
+ const resolvedPagesDir = resolve(pagesDir);
56
+ const resolvedOutDir = resolve(outDir);
57
+ const resolvedOutDirTmp = resolve(dirname(resolvedOutDir), `${basename(resolvedOutDir)}.tmp`);
58
+ const pagesParentDir = dirname(resolvedPagesDir);
59
+ const projectRoot = basename(pagesParentDir) === 'src'
60
+ ? dirname(pagesParentDir)
61
+ : pagesParentDir;
62
+ const watchRoots = new Set([pagesParentDir]);
63
+
55
64
  /** @type {import('http').ServerResponse[]} */
56
65
  const hmrClients = [];
57
- let _watcher = null;
66
+ /** @type {import('fs').FSWatcher[]} */
67
+ let _watchers = [];
58
68
 
59
69
  let buildId = 0;
60
70
  let buildStatus = 'ok'; // 'ok' | 'error' | 'building'
@@ -332,62 +342,108 @@ export async function createDevServer(options) {
332
342
  let _buildDebounce = null;
333
343
  let _queuedFiles = new Set();
334
344
 
345
+ function _isWithin(parent, child) {
346
+ const rel = relative(parent, child);
347
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
348
+ }
349
+
350
+ function _toDisplayPath(absPath) {
351
+ const rel = relative(projectRoot, absPath);
352
+ if (rel === '') return '.';
353
+ if (!rel.startsWith('..') && !isAbsolute(rel)) {
354
+ return rel;
355
+ }
356
+ return absPath;
357
+ }
358
+
359
+ function _shouldIgnoreChange(absPath) {
360
+ if (_isWithin(resolvedOutDir, absPath)) {
361
+ return true;
362
+ }
363
+ if (_isWithin(resolvedOutDirTmp, absPath)) {
364
+ return true;
365
+ }
366
+ const rel = relative(projectRoot, absPath);
367
+ if (rel.startsWith('..') || isAbsolute(rel)) {
368
+ return false;
369
+ }
370
+ const segments = rel.split(/[\\/]+/g);
371
+ return segments.includes('node_modules')
372
+ || segments.includes('.git')
373
+ || segments.includes('.zenith')
374
+ || segments.includes('target')
375
+ || segments.includes('.turbo');
376
+ }
377
+
335
378
  /**
336
- * Start watching the pages directory for changes.
379
+ * Start watching source roots for changes.
337
380
  */
338
381
  function _startWatcher() {
339
- try {
340
- _watcher = watch(pagesDir, { recursive: true }, (eventType, filename) => {
341
- if (!filename) return;
382
+ const queueRebuild = () => {
383
+ if (_buildDebounce !== null) {
384
+ clearTimeout(_buildDebounce);
385
+ }
386
+
387
+ _buildDebounce = setTimeout(async () => {
388
+ _buildDebounce = null;
389
+ const changed = Array.from(_queuedFiles).map(_toDisplayPath).sort();
390
+ _queuedFiles.clear();
342
391
 
343
- _queuedFiles.add(filename);
392
+ buildId++;
393
+ buildStatus = 'building';
394
+ _broadcastEvent('build_start', { changedFiles: changed });
344
395
 
345
- if (_buildDebounce !== null) {
346
- clearTimeout(_buildDebounce);
396
+ const startTime = Date.now();
397
+ try {
398
+ await build({ pagesDir, outDir, config });
399
+ buildStatus = 'ok';
400
+ buildError = null;
401
+ lastBuildMs = Date.now();
402
+ durationMs = lastBuildMs - startTime;
403
+
404
+ _broadcastEvent('build_complete', {
405
+ durationMs,
406
+ status: buildStatus
407
+ });
408
+
409
+ const onlyCss = changed.length > 0 && changed.every((f) => f.endsWith('.css'));
410
+ if (onlyCss) {
411
+ // Let the client fetch the updated CSS automatically
412
+ _broadcastEvent('css_update', {});
413
+ } else {
414
+ _broadcastEvent('reload', {});
415
+ }
416
+ } catch (err) {
417
+ const fullError = err instanceof Error ? err.message : String(err);
418
+ buildStatus = 'error';
419
+ buildError = { message: fullError.length > 10000 ? fullError.slice(0, 10000) + '... (truncated)' : fullError };
420
+ lastBuildMs = Date.now();
421
+ durationMs = lastBuildMs - startTime;
422
+
423
+ _broadcastEvent('build_error', buildError);
347
424
  }
425
+ }, 50);
426
+ };
348
427
 
349
- _buildDebounce = setTimeout(async () => {
350
- _buildDebounce = null;
351
- const changed = Array.from(_queuedFiles);
352
- _queuedFiles.clear();
353
-
354
- buildId++;
355
- buildStatus = 'building';
356
- _broadcastEvent('build_start', { changedFiles: changed });
357
-
358
- const startTime = Date.now();
359
- try {
360
- await build({ pagesDir, outDir, config });
361
- buildStatus = 'ok';
362
- buildError = null;
363
- lastBuildMs = Date.now();
364
- durationMs = lastBuildMs - startTime;
365
-
366
- _broadcastEvent('build_complete', {
367
- durationMs,
368
- status: buildStatus
369
- });
370
-
371
- const onlyCss = changed.every(f => f.endsWith('.css'));
372
- if (onlyCss) {
373
- // Let the client fetch the updated CSS automatically
374
- _broadcastEvent('css_update', {});
375
- } else {
376
- _broadcastEvent('reload', {});
377
- }
378
- } catch (err) {
379
- const fullError = err instanceof Error ? err.message : String(err);
380
- buildStatus = 'error';
381
- buildError = { message: fullError.length > 10000 ? fullError.slice(0, 10000) + '... (truncated)' : fullError };
382
- lastBuildMs = Date.now();
383
- durationMs = lastBuildMs - startTime;
384
-
385
- _broadcastEvent('build_error', buildError);
428
+ const roots = Array.from(watchRoots);
429
+ for (const root of roots) {
430
+ if (!existsSync(root)) continue;
431
+ try {
432
+ const watcher = watch(root, { recursive: true }, (_eventType, filename) => {
433
+ if (!filename) {
434
+ return;
386
435
  }
387
- }, 50);
388
- });
389
- } catch {
390
- // fs.watch may not support recursive on all platforms
436
+ const changedPath = resolve(root, String(filename));
437
+ if (_shouldIgnoreChange(changedPath)) {
438
+ return;
439
+ }
440
+ _queuedFiles.add(changedPath);
441
+ queueRebuild();
442
+ });
443
+ _watchers.push(watcher);
444
+ } catch {
445
+ // fs.watch recursive may not be supported on this platform/root
446
+ }
391
447
  }
392
448
  }
393
449
 
@@ -400,10 +456,14 @@ export async function createDevServer(options) {
400
456
  server,
401
457
  port: actualPort,
402
458
  close: () => {
403
- if (_watcher) {
404
- _watcher.close();
405
- _watcher = null;
459
+ for (const watcher of _watchers) {
460
+ try {
461
+ watcher.close();
462
+ } catch {
463
+ // ignore close errors
464
+ }
406
465
  }
466
+ _watchers = [];
407
467
  for (const client of hmrClients) {
408
468
  try { client.end(); } catch { }
409
469
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/cli",
3
- "version": "0.5.0-beta.2.19",
3
+ "version": "0.5.0-beta.2.20",
4
4
  "description": "Deterministic project orchestrator for Zenith framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  "prepublishOnly": "npm run build"
25
25
  },
26
26
  "dependencies": {
27
- "@zenithbuild/compiler": "0.5.0-beta.2.19"
27
+ "@zenithbuild/compiler": "0.5.0-beta.2.20"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@jest/globals": "^30.2.0",