@stati/core 1.15.2 → 1.16.1

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.
@@ -58,7 +58,7 @@ async function copyStaticAssetsWithLogging(sourceDir, destDir, logger, basePath
58
58
  if (item.isDirectory()) {
59
59
  // Recursively copy directories
60
60
  await ensureDir(destPath);
61
- filesCopied += await copyStaticAssetsWithLogging(sourcePath, destPath, logger, relativePath);
61
+ filesCopied += await copyStaticAssetsWithLogging(sourcePath, destPath, logger);
62
62
  }
63
63
  else {
64
64
  // Copy individual files
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AA2B7D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AA4SD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CA+axF"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/core/dev.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAe,MAAM,EAAE,MAAM,mBAAmB,CAAC;AA2B7D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAiXD,wBAAsB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAudxF"}
package/dist/core/dev.js CHANGED
@@ -61,15 +61,32 @@ async function performInitialBuild(configPath, logger, onError) {
61
61
  }
62
62
  }
63
63
  /**
64
- * Performs incremental rebuild when files change, using ISG logic for smart rebuilds
64
+ * Performs incremental rebuild when files change, using ISG logic for smart rebuilds.
65
+ * Uses a pending changes queue to ensure no file changes are lost when builds are in progress.
66
+ *
67
+ * @param changedPath - The path of the file that changed
68
+ * @param eventType - The type of change: 'add', 'change', or 'unlink'
69
+ * @param configPath - Optional path to the stati config file
70
+ * @param staticDir - The static assets directory path
71
+ * @param logger - Logger instance for output
72
+ * @param wsServer - WebSocket server for live reload notifications
73
+ * @param isBuildingRef - Mutable reference tracking whether a build is currently in progress
74
+ * @param pendingChangesRef - Mutable reference holding queued file changes that occurred during an
75
+ * active build. The `changes` Map stores file paths as keys and their event types as values,
76
+ * ensuring each file is only processed once with its most recent event type.
77
+ * @param onError - Optional callback invoked when build errors occur
78
+ * @param batchedChanges - Array of changes to process in a single batch (used for pending queue processing)
65
79
  */
66
- async function performIncrementalRebuild(changedPath, configPath, logger, wsServer, isBuildingRef, onError) {
80
+ async function performIncrementalRebuild(changedPath, eventType, configPath, staticDir, logger, wsServer, isBuildingRef, pendingChangesRef, onError, batchedChanges = []) {
81
+ // If a build is in progress, queue this change for processing after the current build
67
82
  if (isBuildingRef.value) {
68
- logger.info?.('Build in progress, skipping...');
83
+ pendingChangesRef.changes.set(changedPath, eventType);
69
84
  return;
70
85
  }
71
86
  isBuildingRef.value = true;
72
87
  const startTime = Date.now();
88
+ // All changes being processed in this build (primary + batched)
89
+ const allChanges = [{ path: changedPath, eventType }, ...batchedChanges];
73
90
  // Create a quiet logger for dev builds that suppresses verbose output
74
91
  const devLogger = {
75
92
  info: () => { }, // Suppress info messages
@@ -80,11 +97,15 @@ async function performIncrementalRebuild(changedPath, configPath, logger, wsServ
80
97
  processing: () => { }, // Suppress processing messages
81
98
  stats: () => { }, // Suppress stats messages
82
99
  };
100
+ // Helper to check if a path is a static asset
101
+ const normalizedStaticDir = staticDir.replace(/\\/g, '/');
102
+ const isStaticAsset = (path) => {
103
+ const normalizedPath = path.replace(/\\/g, '/');
104
+ return normalizedPath.startsWith(normalizedStaticDir);
105
+ };
106
+ // Helper to get relative path
107
+ const getRelativePath = (path) => path.replace(process.cwd(), '').replace(/\\/g, '/').replace(/^\//, '');
83
108
  try {
84
- const relativePath = changedPath
85
- .replace(process.cwd(), '')
86
- .replace(/\\/g, '/')
87
- .replace(/^\//, '');
88
109
  // Check if the changed file is a template/partial
89
110
  if (changedPath.endsWith(TEMPLATE_EXTENSION) || changedPath.includes('_partials')) {
90
111
  await handleTemplateChange(changedPath, configPath, devLogger);
@@ -116,7 +137,22 @@ async function performIncrementalRebuild(changedPath, configPath, logger, wsServ
116
137
  });
117
138
  }
118
139
  const duration = Date.now() - startTime;
119
- logger.info?.(`⚡ ${relativePath} rebuilt in ${duration}ms`);
140
+ // Log all files that were processed in this batch
141
+ for (const change of allChanges) {
142
+ const relativePath = getRelativePath(change.path);
143
+ let action;
144
+ if (change.eventType === 'unlink') {
145
+ action = 'deleted';
146
+ }
147
+ else if (isStaticAsset(change.path)) {
148
+ action = 'copied';
149
+ }
150
+ else {
151
+ action = 'rebuilt';
152
+ }
153
+ logger.info?.(`⚡ ${relativePath} ${action}`);
154
+ }
155
+ logger.info?.(` Done in ${duration}ms`);
120
156
  }
121
157
  catch (error) {
122
158
  const buildError = error instanceof Error ? error : new Error(String(error));
@@ -129,6 +165,18 @@ async function performIncrementalRebuild(changedPath, configPath, logger, wsServ
129
165
  }
130
166
  finally {
131
167
  isBuildingRef.value = false;
168
+ // Check if there are pending changes that occurred during this build
169
+ if (pendingChangesRef.changes.size > 0) {
170
+ // Get all pending changes - the build will process all of them at once
171
+ const pendingChanges = Array.from(pendingChangesRef.changes.entries()).map(([path, evtType]) => ({ path, eventType: evtType }));
172
+ pendingChangesRef.changes.clear();
173
+ // Trigger another rebuild with all batched changes
174
+ // The build() call will copy all static assets and rebuild all affected pages
175
+ const [first, ...remaining] = pendingChanges;
176
+ if (first) {
177
+ void performIncrementalRebuild(first.path, first.eventType, configPath, staticDir, logger, wsServer, isBuildingRef, pendingChangesRef, onError, remaining);
178
+ }
179
+ }
132
180
  }
133
181
  }
134
182
  /**
@@ -291,8 +339,10 @@ export async function createDevServer(options = {}) {
291
339
  lastBuildError = error;
292
340
  };
293
341
  let watcher = null;
342
+ let cssWatcher = null;
294
343
  let tsWatchers = [];
295
344
  const isBuildingRef = { value: false };
345
+ const pendingChangesRef = { changes: new Map() };
296
346
  let isStopping = false;
297
347
  /**
298
348
  * Gets MIME type for a file based on its extension
@@ -564,14 +614,34 @@ export async function createDevServer(options = {}) {
564
614
  persistent: true,
565
615
  ignoreInitial: true,
566
616
  });
617
+ // Set up separate watcher for CSS files in output directory
618
+ // This enables live reload when external tools (like Tailwind CLI) update CSS
619
+ cssWatcher = chokidar.watch([join(outDir, '**/*.css')], {
620
+ ignored: /(^|[/\\])\../, // ignore dotfiles
621
+ persistent: true,
622
+ ignoreInitial: true,
623
+ });
624
+ cssWatcher.on('change', (path) => {
625
+ const relativePath = path.replace(process.cwd(), '').replace(/\\/g, '/').replace(/^\//, '');
626
+ logger.info?.(`⚡ ${relativePath} updated`);
627
+ // Just notify clients to reload - no rebuild needed since CSS was already compiled
628
+ if (wsServer) {
629
+ wsServer.clients.forEach((client) => {
630
+ const ws = client;
631
+ if (ws.readyState === 1) {
632
+ ws.send(JSON.stringify({ type: 'reload' }));
633
+ }
634
+ });
635
+ }
636
+ });
567
637
  watcher.on('change', (path) => {
568
- void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef, setLastBuildError);
638
+ void performIncrementalRebuild(path, 'change', configPath, staticDir, logger, wsServer, isBuildingRef, pendingChangesRef, setLastBuildError);
569
639
  });
570
640
  watcher.on('add', (path) => {
571
- void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef, setLastBuildError);
641
+ void performIncrementalRebuild(path, 'add', configPath, staticDir, logger, wsServer, isBuildingRef, pendingChangesRef, setLastBuildError);
572
642
  });
573
643
  watcher.on('unlink', (path) => {
574
- void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef, setLastBuildError);
644
+ void performIncrementalRebuild(path, 'unlink', configPath, staticDir, logger, wsServer, isBuildingRef, pendingChangesRef, setLastBuildError);
575
645
  });
576
646
  logger.success?.(`Dev server running at ${url}`);
577
647
  logger.info?.(`\nServing:`);
@@ -598,6 +668,11 @@ export async function createDevServer(options = {}) {
598
668
  await watcher.close();
599
669
  watcher = null;
600
670
  }
671
+ // Clean up CSS watcher
672
+ if (cssWatcher) {
673
+ await cssWatcher.close();
674
+ cssWatcher = null;
675
+ }
601
676
  // Clean up TypeScript watchers
602
677
  if (tsWatchers.length > 0) {
603
678
  await Promise.all(tsWatchers.map(async (w) => {
@@ -1 +1 @@
1
- {"version":3,"file":"callable-partials.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/callable-partials.utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;IAC1C,QAAQ,IAAI,MAAM,CAAC;IACnB,OAAO,IAAI,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,GAAG,EACR,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,eAAe,EAAE,MAAM,GACtB,eAAe,CAqDjB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAcjC"}
1
+ {"version":3,"file":"callable-partials.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/callable-partials.utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;IAC1C,QAAQ,IAAI,MAAM,CAAC;IACnB,OAAO,IAAI,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,GAAG,EACR,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,eAAe,EAAE,MAAM,GACtB,eAAe,CAqDjB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CA8BjC"}
@@ -96,13 +96,23 @@ export function makeCallablePartial(eta, partialPath, baseContext, renderedConte
96
96
  */
97
97
  export function wrapPartialsAsCallable(eta, partials, partialPaths, baseContext) {
98
98
  const callablePartials = {};
99
+ // Create a context that will have partials updated to the callable versions
100
+ // This enables nested callable partial calls
101
+ const contextWithCallablePartials = {
102
+ ...baseContext,
103
+ // The partials property will be updated after all callables are created
104
+ get partials() {
105
+ return callablePartials;
106
+ },
107
+ };
99
108
  for (const [name, renderedContent] of Object.entries(partials)) {
100
109
  const partialPath = partialPaths[name];
101
110
  if (!partialPath) {
102
111
  console.warn(`No path found for partial "${name}", skipping callable wrapper`);
103
112
  continue;
104
113
  }
105
- callablePartials[name] = makeCallablePartial(eta, partialPath, baseContext, renderedContent);
114
+ // Pass the context that has dynamic access to all callable partials
115
+ callablePartials[name] = makeCallablePartial(eta, partialPath, contextWithCallablePartials, renderedContent);
106
116
  }
107
117
  return callablePartials;
108
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stati/core",
3
- "version": "1.15.2",
3
+ "version": "1.16.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",