@stati/core 1.3.0 → 1.3.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.
Files changed (80) hide show
  1. package/dist/config/loader.d.ts +7 -1
  2. package/dist/config/loader.d.ts.map +1 -1
  3. package/dist/config/loader.js +17 -12
  4. package/dist/constants.d.ts +71 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +78 -0
  7. package/dist/core/build.d.ts +1 -1
  8. package/dist/core/build.d.ts.map +1 -1
  9. package/dist/core/build.js +94 -69
  10. package/dist/core/content.d.ts +1 -1
  11. package/dist/core/content.d.ts.map +1 -1
  12. package/dist/core/content.js +10 -5
  13. package/dist/core/dev.d.ts +1 -7
  14. package/dist/core/dev.d.ts.map +1 -1
  15. package/dist/core/dev.js +135 -126
  16. package/dist/core/invalidate.d.ts +1 -1
  17. package/dist/core/invalidate.d.ts.map +1 -1
  18. package/dist/core/invalidate.js +3 -3
  19. package/dist/core/isg/build-lock.d.ts.map +1 -1
  20. package/dist/core/isg/build-lock.js +4 -2
  21. package/dist/core/isg/builder.d.ts +1 -1
  22. package/dist/core/isg/builder.d.ts.map +1 -1
  23. package/dist/core/isg/deps.d.ts +1 -1
  24. package/dist/core/isg/deps.d.ts.map +1 -1
  25. package/dist/core/isg/deps.js +59 -78
  26. package/dist/core/isg/hash.d.ts.map +1 -1
  27. package/dist/core/isg/hash.js +26 -17
  28. package/dist/core/isg/manifest.d.ts +1 -1
  29. package/dist/core/isg/manifest.d.ts.map +1 -1
  30. package/dist/core/isg/manifest.js +21 -8
  31. package/dist/core/isg/ttl.d.ts +1 -1
  32. package/dist/core/isg/ttl.d.ts.map +1 -1
  33. package/dist/core/isg/ttl.js +6 -9
  34. package/dist/core/isg/validation.d.ts +1 -1
  35. package/dist/core/isg/validation.d.ts.map +1 -1
  36. package/dist/core/markdown.d.ts +1 -1
  37. package/dist/core/markdown.d.ts.map +1 -1
  38. package/dist/core/navigation.d.ts +1 -1
  39. package/dist/core/navigation.d.ts.map +1 -1
  40. package/dist/core/templates.d.ts +1 -1
  41. package/dist/core/templates.d.ts.map +1 -1
  42. package/dist/core/templates.js +25 -103
  43. package/dist/core/utils/fs.d.ts +37 -0
  44. package/dist/core/utils/fs.d.ts.map +1 -0
  45. package/dist/core/utils/fs.js +86 -0
  46. package/dist/core/utils/partials.d.ts +24 -0
  47. package/dist/core/utils/partials.d.ts.map +1 -0
  48. package/dist/core/utils/partials.js +85 -0
  49. package/dist/core/utils/paths.d.ts +67 -0
  50. package/dist/core/utils/paths.d.ts.map +1 -0
  51. package/dist/core/utils/paths.js +86 -0
  52. package/dist/core/utils/template-discovery.d.ts +34 -0
  53. package/dist/core/utils/template-discovery.d.ts.map +1 -0
  54. package/dist/core/utils/template-discovery.js +111 -0
  55. package/dist/index.d.ts +2 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/tests/utils/test-mocks.d.ts +69 -0
  58. package/dist/tests/utils/test-mocks.d.ts.map +1 -0
  59. package/dist/tests/utils/test-mocks.js +125 -0
  60. package/dist/types/config.d.ts +178 -0
  61. package/dist/types/config.d.ts.map +1 -0
  62. package/dist/types/config.js +1 -0
  63. package/dist/types/content.d.ts +124 -0
  64. package/dist/types/content.d.ts.map +1 -0
  65. package/dist/types/content.js +4 -0
  66. package/dist/types/index.d.ts +10 -0
  67. package/dist/types/index.d.ts.map +1 -0
  68. package/dist/types/index.js +5 -0
  69. package/dist/types/isg.d.ts +103 -0
  70. package/dist/types/isg.d.ts.map +1 -0
  71. package/dist/types/isg.js +4 -0
  72. package/dist/types/logging.d.ts +113 -0
  73. package/dist/types/logging.d.ts.map +1 -0
  74. package/dist/types/logging.js +4 -0
  75. package/dist/types/navigation.d.ts +43 -0
  76. package/dist/types/navigation.d.ts.map +1 -0
  77. package/dist/types/navigation.js +4 -0
  78. package/dist/types.d.ts +10 -10
  79. package/dist/types.d.ts.map +1 -1
  80. package/package.json +1 -1
package/dist/core/dev.js CHANGED
@@ -7,28 +7,12 @@ import chokidar from 'chokidar';
7
7
  import { build } from './build.js';
8
8
  import { loadConfig } from '../config/loader.js';
9
9
  import { loadCacheManifest, saveCacheManifest } from './isg/manifest.js';
10
+ import { resolveDevPaths, resolveCacheDir } from './utils/paths.js';
11
+ import { DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, TEMPLATE_EXTENSION } from '../constants.js';
10
12
  /**
11
- * Creates and configures a development server with live reload functionality.
12
- *
13
- * @param options - Development server configuration options
14
- * @returns Promise resolving to a DevServer instance
13
+ * Loads and validates configuration for the dev server.
15
14
  */
16
- export async function createDevServer(options = {}) {
17
- const { port = 3000, host = 'localhost', open = false, configPath, logger = {
18
- info: (msg) => console.log(msg),
19
- success: (msg) => console.log(msg),
20
- error: (msg) => console.error(msg),
21
- warning: (msg) => console.warn(msg),
22
- building: (msg) => console.log(msg),
23
- processing: (msg) => console.log(msg),
24
- stats: (msg) => console.log(msg),
25
- }, } = options;
26
- const url = `http://${host}:${port}`;
27
- let httpServer = null;
28
- let wsServer = null;
29
- let watcher = null;
30
- let config;
31
- let isBuilding = false;
15
+ async function loadDevConfig(configPath, logger) {
32
16
  // Load configuration
33
17
  try {
34
18
  if (configPath) {
@@ -36,124 +20,122 @@ export async function createDevServer(options = {}) {
36
20
  // This is a limitation of the current loadConfig implementation
37
21
  logger.info?.(`Loading config from: ${configPath}`);
38
22
  }
39
- config = await loadConfig(process.cwd());
23
+ const config = await loadConfig(process.cwd());
24
+ const { outDir, srcDir, staticDir } = resolveDevPaths(config);
25
+ return { config, outDir, srcDir, staticDir };
40
26
  }
41
27
  catch (error) {
42
28
  logger.error?.(`Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
43
29
  throw error;
44
30
  }
45
- const outDir = join(process.cwd(), config.outDir || 'dist');
46
- const srcDir = join(process.cwd(), config.srcDir || 'site');
47
- const staticDir = join(process.cwd(), config.staticDir || 'public');
48
- /**
49
- * Performs an initial build to ensure dist/ exists
50
- */
51
- async function initialBuild() {
52
- try {
31
+ }
32
+ /**
33
+ * Performs an initial build to ensure dist/ exists
34
+ */
35
+ async function performInitialBuild(configPath, logger) {
36
+ try {
37
+ await build({
38
+ logger,
39
+ force: false,
40
+ clean: false,
41
+ ...(configPath && { configPath }),
42
+ });
43
+ }
44
+ catch (error) {
45
+ logger.error?.(`Initial build failed: ${error instanceof Error ? error.message : String(error)}`);
46
+ throw error;
47
+ }
48
+ }
49
+ /**
50
+ * Performs incremental rebuild when files change, using ISG logic for smart rebuilds
51
+ */
52
+ async function performIncrementalRebuild(changedPath, configPath, logger, wsServer, isBuildingRef) {
53
+ if (isBuildingRef.value) {
54
+ logger.info?.('Build in progress, skipping...');
55
+ return;
56
+ }
57
+ isBuildingRef.value = true;
58
+ const startTime = Date.now();
59
+ // Create a quiet logger for dev builds that suppresses verbose output
60
+ const devLogger = {
61
+ info: () => { }, // Suppress info messages
62
+ success: () => { }, // Suppress success messages
63
+ error: logger.error || (() => { }),
64
+ warning: logger.warning || (() => { }),
65
+ building: () => { }, // Suppress building messages
66
+ processing: () => { }, // Suppress processing messages
67
+ stats: () => { }, // Suppress stats messages
68
+ };
69
+ try {
70
+ const relativePath = changedPath
71
+ .replace(process.cwd(), '')
72
+ .replace(/\\/g, '/')
73
+ .replace(/^\//, '');
74
+ // Check if the changed file is a template/partial
75
+ if (changedPath.endsWith(TEMPLATE_EXTENSION) || changedPath.includes('_partials')) {
76
+ await handleTemplateChange(changedPath, configPath, devLogger);
77
+ }
78
+ else {
79
+ // Content or static file changed - use normal rebuild
53
80
  await build({
54
- logger,
81
+ logger: devLogger,
55
82
  force: false,
56
83
  clean: false,
57
84
  ...(configPath && { configPath }),
58
85
  });
59
86
  }
60
- catch (error) {
61
- logger.error?.(`Initial build failed: ${error instanceof Error ? error.message : String(error)}`);
62
- throw error;
87
+ // Notify all connected clients to reload
88
+ if (wsServer) {
89
+ wsServer.clients.forEach((client) => {
90
+ const ws = client;
91
+ if (ws.readyState === 1) {
92
+ // WebSocket.OPEN
93
+ ws.send(JSON.stringify({ type: 'reload' }));
94
+ }
95
+ });
63
96
  }
97
+ const duration = Date.now() - startTime;
98
+ logger.info?.(`⚡ ${relativePath} rebuilt in ${duration}ms`);
64
99
  }
65
- /**
66
- * Performs incremental rebuild when files change, using ISG logic for smart rebuilds
67
- */
68
- async function incrementalRebuild(changedPath) {
69
- if (isBuilding) {
70
- logger.info?.('⏳ Build in progress, skipping...');
100
+ catch (error) {
101
+ const duration = Date.now() - startTime;
102
+ logger.error?.(`❌ Rebuild failed after ${duration}ms: ${error instanceof Error ? error.message : String(error)}`);
103
+ }
104
+ finally {
105
+ isBuildingRef.value = false;
106
+ }
107
+ }
108
+ /**
109
+ * Handles template/partial file changes by invalidating affected pages
110
+ */
111
+ async function handleTemplateChange(templatePath, configPath, logger) {
112
+ const cacheDir = resolveCacheDir();
113
+ try {
114
+ // Load existing cache manifest
115
+ const cacheManifest = await loadCacheManifest(cacheDir);
116
+ if (!cacheManifest) {
117
+ // No cache exists, perform full rebuild
118
+ await build({
119
+ logger,
120
+ force: false,
121
+ clean: false,
122
+ ...(configPath && { configPath }),
123
+ });
71
124
  return;
72
125
  }
73
- isBuilding = true;
74
- try {
75
- // Check if the changed file is a template/partial
76
- if (changedPath.endsWith('.eta') || changedPath.includes('_partials')) {
77
- logger.info?.(`🎨 Template changed: ${changedPath}`);
78
- await handleTemplateChange(changedPath);
79
- }
80
- else {
81
- // Content or static file changed - use normal rebuild
82
- logger.info?.(`📄 Content changed: ${changedPath}`);
83
- await build({
84
- logger,
85
- force: false,
86
- clean: false,
87
- ...(configPath && { configPath }),
88
- });
89
- }
90
- // Notify all connected clients to reload
91
- if (wsServer) {
92
- wsServer.clients.forEach((client) => {
93
- const ws = client;
94
- if (ws.readyState === 1) {
95
- // WebSocket.OPEN
96
- ws.send(JSON.stringify({ type: 'reload' }));
97
- }
98
- });
126
+ // Find pages that depend on this template
127
+ const affectedPages = [];
128
+ for (const [pagePath, entry] of Object.entries(cacheManifest.entries)) {
129
+ if (entry.deps.some((dep) => dep.includes(posix.normalize(templatePath.replace(/\\/g, '/'))))) {
130
+ affectedPages.push(pagePath);
131
+ // Remove from cache to force rebuild
132
+ delete cacheManifest.entries[pagePath];
99
133
  }
100
- logger.success?.('Rebuild complete');
101
- }
102
- catch (error) {
103
- logger.error?.(`Rebuild failed: ${error instanceof Error ? error.message : String(error)}`);
104
134
  }
105
- finally {
106
- isBuilding = false;
107
- }
108
- }
109
- /**
110
- * Handles template/partial file changes by invalidating affected pages
111
- */
112
- async function handleTemplateChange(templatePath) {
113
- const cacheDir = join(process.cwd(), '.stati');
114
- try {
115
- // Load existing cache manifest
116
- let cacheManifest = await loadCacheManifest(cacheDir);
117
- if (!cacheManifest) {
118
- // No cache exists, perform full rebuild
119
- logger.info?.('No cache found, performing full rebuild...');
120
- await build({
121
- logger,
122
- force: false,
123
- clean: false,
124
- ...(configPath && { configPath }),
125
- });
126
- return;
127
- }
128
- // Find pages that depend on this template
129
- const affectedPages = [];
130
- for (const [pagePath, entry] of Object.entries(cacheManifest.entries)) {
131
- if (entry.deps.some((dep) => dep.includes(posix.normalize(templatePath.replace(/\\/g, '/'))))) {
132
- affectedPages.push(pagePath);
133
- // Remove from cache to force rebuild
134
- delete cacheManifest.entries[pagePath];
135
- }
136
- }
137
- if (affectedPages.length > 0) {
138
- logger.info?.(`🎯 Invalidating ${affectedPages.length} affected pages:`);
139
- affectedPages.forEach((page) => logger.info?.(` 📄 ${page}`));
140
- // Save updated cache manifest
141
- await saveCacheManifest(cacheDir, cacheManifest);
142
- // Perform incremental rebuild (only affected pages will be rebuilt)
143
- await build({
144
- logger,
145
- force: false,
146
- clean: false,
147
- ...(configPath && { configPath }),
148
- });
149
- }
150
- else {
151
- logger.info?.('ℹ️ No pages affected by template change');
152
- }
153
- }
154
- catch (error) {
155
- logger.warning?.(`Template dependency analysis failed, performing full rebuild: ${error instanceof Error ? error.message : String(error)}`);
156
- // Fallback to full rebuild
135
+ if (affectedPages.length > 0) {
136
+ // Save updated cache manifest
137
+ await saveCacheManifest(cacheDir, cacheManifest);
138
+ // Perform incremental rebuild (only affected pages will be rebuilt)
157
139
  await build({
158
140
  logger,
159
141
  force: false,
@@ -162,6 +144,33 @@ export async function createDevServer(options = {}) {
162
144
  });
163
145
  }
164
146
  }
147
+ catch {
148
+ // Fallback to full rebuild
149
+ await build({
150
+ logger,
151
+ force: false,
152
+ clean: false,
153
+ ...(configPath && { configPath }),
154
+ });
155
+ }
156
+ }
157
+ export async function createDevServer(options = {}) {
158
+ const { port = DEFAULT_DEV_PORT, host = DEFAULT_DEV_HOST, open = false, configPath, logger = {
159
+ info: (msg) => console.log(msg),
160
+ success: (msg) => console.log(msg),
161
+ error: (msg) => console.error(msg),
162
+ warning: (msg) => console.warn(msg),
163
+ building: (msg) => console.log(msg),
164
+ processing: (msg) => console.log(msg),
165
+ stats: (msg) => console.log(msg),
166
+ }, } = options;
167
+ const url = `http://${host}:${port}`;
168
+ let httpServer = null;
169
+ let wsServer = null;
170
+ let watcher = null;
171
+ const isBuildingRef = { value: false };
172
+ // Load configuration
173
+ const { config: _config, outDir, srcDir, staticDir } = await loadDevConfig(configPath, logger);
165
174
  /**
166
175
  * Gets MIME type for a file based on its extension
167
176
  */
@@ -272,7 +281,7 @@ export async function createDevServer(options = {}) {
272
281
  url,
273
282
  async start() {
274
283
  // Perform initial build
275
- await initialBuild();
284
+ await performInitialBuild(configPath, logger);
276
285
  // Create HTTP server
277
286
  httpServer = createServer(async (req, res) => {
278
287
  const requestPath = req.url || '/';
@@ -302,7 +311,7 @@ export async function createDevServer(options = {}) {
302
311
  path: '/__ws',
303
312
  });
304
313
  wsServer.on('connection', (ws) => {
305
- logger.info?.('🔗 Browser connected for live reload');
314
+ logger.info?.('Browser connected for live reload');
306
315
  const websocket = ws;
307
316
  websocket.on('close', () => {
308
317
  logger.info?.('Browser disconnected from live reload');
@@ -325,13 +334,13 @@ export async function createDevServer(options = {}) {
325
334
  ignoreInitial: true,
326
335
  });
327
336
  watcher.on('change', (path) => {
328
- void incrementalRebuild(path);
337
+ void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef);
329
338
  });
330
339
  watcher.on('add', (path) => {
331
- void incrementalRebuild(path);
340
+ void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef);
332
341
  });
333
342
  watcher.on('unlink', (path) => {
334
- void incrementalRebuild(path);
343
+ void performIncrementalRebuild(path, configPath, logger, wsServer, isBuildingRef);
335
344
  });
336
345
  logger.success?.(`Dev server running at ${url}`);
337
346
  logger.info?.(`\nServing from:`);
@@ -1,4 +1,4 @@
1
- import type { CacheEntry } from '../types.js';
1
+ import type { CacheEntry } from '../types/index.js';
2
2
  /**
3
3
  * Invalidation result containing affected cache entries.
4
4
  */
@@ -1 +1 @@
1
- {"version":3,"file":"invalidate.d.ts","sourceRoot":"","sources":["../../src/core/invalidate.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0CAA0C;IAC1C,gBAAgB,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,2CAA2C;IAC3C,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAkC9D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAkC9F;AAiLD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoD5E"}
1
+ {"version":3,"file":"invalidate.d.ts","sourceRoot":"","sources":["../../src/core/invalidate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0CAA0C;IAC1C,gBAAgB,EAAE,MAAM,CAAC;IACzB,iCAAiC;IACjC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,2CAA2C;IAC3C,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAkC9D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAkC9F;AAiLD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoD5E"}
@@ -1,5 +1,5 @@
1
- import { join } from 'path';
2
1
  import { loadCacheManifest, saveCacheManifest } from './isg/manifest.js';
2
+ import { resolveCacheDir } from './utils/paths.js';
3
3
  /**
4
4
  * Parses an invalidation query string into individual query terms.
5
5
  * Supports space-separated values and quoted strings.
@@ -281,9 +281,9 @@ function matchesAge(entry, ageValue) {
281
281
  * ```
282
282
  */
283
283
  export async function invalidate(query) {
284
- const cacheDir = join(process.cwd(), '.stati');
284
+ const cacheDir = resolveCacheDir();
285
285
  // Load existing cache manifest
286
- let cacheManifest = await loadCacheManifest(cacheDir);
286
+ const cacheManifest = await loadCacheManifest(cacheDir);
287
287
  if (!cacheManifest) {
288
288
  // No cache to invalidate
289
289
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"build-lock.d.ts","sourceRoot":"","sources":["../../../src/core/isg/build-lock.ts"],"names":[],"mappings":"AAYA;;GAEG;AACH,UAAU,SAAS;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,EAAE,MAAM;IAI5B;;;;;;;;;;;;;;;;;;;OAmBG;IACG,WAAW,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAoDrF;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBlC;;;;;;;;;;;OAWG;IACG,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAiBpC;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAY9C;;;OAGG;YACW,eAAe;IAQ7B;;OAEG;YACW,cAAc;IAc5B;;OAEG;YACW,YAAY;IAS1B;;OAEG;YACW,gBAAgB;IAY9B;;OAEG;IACH,OAAO,CAAC,WAAW;IAQnB;;OAEG;IACH,OAAO,CAAC,KAAK;CAGd;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAClD,OAAO,CAAC,CAAC,CAAC,CASZ"}
1
+ {"version":3,"file":"build-lock.d.ts","sourceRoot":"","sources":["../../../src/core/isg/build-lock.ts"],"names":[],"mappings":"AAWA;;GAEG;AACH,UAAU,SAAS;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,EAAE,MAAM;IAI5B;;;;;;;;;;;;;;;;;;;OAmBG;IACG,WAAW,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAoDrF;;;;;;;OAOG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBlC;;;;;;;;;;;OAWG;IACG,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAiBpC;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAY9C;;;OAGG;YACW,eAAe;IAQ7B;;OAEG;YACW,cAAc;IAc5B;;OAEG;YACW,YAAY;IAY1B;;OAEG;YACW,gBAAgB;IAY9B;;OAEG;IACH,OAAO,CAAC,WAAW;IAQnB;;OAEG;IACH,OAAO,CAAC,KAAK;CAGd;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAClD,OAAO,CAAC,CAAC,CAAC,CASZ"}
@@ -1,5 +1,4 @@
1
- import fse from 'fs-extra';
2
- const { writeFile, readFile, pathExists, remove, ensureDir } = fse;
1
+ import { writeFile, readFile, pathExists, remove, ensureDir } from '../utils/fs.js';
3
2
  import { join, dirname } from 'path';
4
3
  import { hostname } from 'os';
5
4
  /**
@@ -178,6 +177,9 @@ export class BuildLockManager {
178
177
  async readLockFile() {
179
178
  try {
180
179
  const content = await readFile(this.lockPath, 'utf-8');
180
+ if (!content) {
181
+ return null;
182
+ }
181
183
  return JSON.parse(content);
182
184
  }
183
185
  catch {
@@ -1,4 +1,4 @@
1
- import type { PageModel, CacheEntry, StatiConfig } from '../../types.js';
1
+ import type { PageModel, CacheEntry, StatiConfig } from '../../types/index.js';
2
2
  /**
3
3
  * Determines if a page should be rebuilt based on ISG logic.
4
4
  * Checks content changes, dependency changes, TTL expiration, and freeze status.
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/core/isg/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAmEzE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,OAAO,CAAC,CAgKlB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,IAAI,GACf,OAAO,CAAC,UAAU,CAAC,CA2ErB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,IAAI,GACf,OAAO,CAAC,UAAU,CAAC,CAUrB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/core/isg/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAmE/E;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,OAAO,CAAC,CAgKlB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,IAAI,GACf,OAAO,CAAC,UAAU,CAAC,CA2ErB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,IAAI,GACf,OAAO,CAAC,UAAU,CAAC,CAUrB"}
@@ -1,4 +1,4 @@
1
- import type { PageModel, StatiConfig } from '../../types.js';
1
+ import type { PageModel, StatiConfig } from '../../types/index.js';
2
2
  /**
3
3
  * Error thrown when a circular dependency is detected in templates.
4
4
  */
@@ -1 +1 @@
1
- {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/core/isg/deps.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7D;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,eAAe,EAAE,MAAM,EAAE;gBAAzB,eAAe,EAAE,MAAM,EAAE,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CAoCnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CAiDnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../../src/core/isg/deps.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAKnE;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAE9B,eAAe,EAAE,MAAM,EAAE;gBAAzB,eAAe,EAAE,MAAM,EAAE,EACzC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CAoCnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CAiDnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB"}
@@ -1,7 +1,9 @@
1
1
  import { join, dirname, relative, posix } from 'path';
2
- import fse from 'fs-extra';
3
- const { pathExists, readFile } = fse;
2
+ import { pathExists, readFile } from '../utils/fs.js';
4
3
  import glob from 'fast-glob';
4
+ import { TEMPLATE_EXTENSION } from '../../constants.js';
5
+ import { isCollectionIndexPage, discoverLayout } from '../utils/template-discovery.js';
6
+ import { resolveSrcDir } from '../utils/paths.js';
5
7
  /**
6
8
  * Error thrown when a circular dependency is detected in templates.
7
9
  */
@@ -42,7 +44,7 @@ export async function trackTemplateDependencies(page, config) {
42
44
  return [];
43
45
  }
44
46
  const deps = [];
45
- const srcDir = join(process.cwd(), config.srcDir);
47
+ const srcDir = resolveSrcDir(config);
46
48
  const relativePath = relative(srcDir, page.sourcePath);
47
49
  // Track dependencies with circular detection
48
50
  const visited = new Set();
@@ -80,7 +82,7 @@ export async function findPartialDependencies(pagePath, config) {
80
82
  return [];
81
83
  }
82
84
  const deps = [];
83
- const srcDir = join(process.cwd(), config.srcDir);
85
+ const srcDir = resolveSrcDir(config);
84
86
  // Get the directory of the current page
85
87
  const pageDir = dirname(pagePath);
86
88
  const pathSegments = pageDir === '.' ? [] : pageDir.split(/[/\\]/);
@@ -135,71 +137,13 @@ export async function findPartialDependencies(pagePath, config) {
135
137
  * ```
136
138
  */
137
139
  export async function resolveTemplatePath(layout, config) {
138
- const srcDir = join(process.cwd(), config.srcDir);
140
+ const srcDir = resolveSrcDir(config);
139
141
  const layoutPath = join(srcDir, `${layout}.eta`);
140
142
  if (await pathExists(layoutPath)) {
141
143
  return layoutPath;
142
144
  }
143
145
  return null;
144
146
  }
145
- /**
146
- * Helper function to determine if a page is a collection index page.
147
- * Duplicated from templates.ts to avoid circular dependencies.
148
- */
149
- function isCollectionIndexPage(page) {
150
- // This is a simplified version - in a real implementation,
151
- // we'd need access to all pages to determine this properly.
152
- // For now, we'll assume any page ending in /index or at root is a collection page.
153
- return page.url === '/' || page.url.endsWith('/index') || page.slug === 'index';
154
- }
155
- /**
156
- * Helper function to discover layout files.
157
- * Duplicated from templates.ts to avoid circular dependencies.
158
- */
159
- async function discoverLayout(pagePath, config, explicitLayout, isIndexPage) {
160
- // Early return if required config values are missing
161
- if (!config.srcDir) {
162
- return null;
163
- }
164
- const srcDir = join(process.cwd(), config.srcDir);
165
- // If explicit layout is specified, use it
166
- if (explicitLayout) {
167
- const layoutPath = join(srcDir, `${explicitLayout}.eta`);
168
- if (await pathExists(layoutPath)) {
169
- return `${explicitLayout}.eta`;
170
- }
171
- }
172
- // Get the directory of the current page
173
- const pageDir = dirname(pagePath);
174
- const pathSegments = pageDir === '.' ? [] : pageDir.split(/[/\\]/);
175
- // Search for layout.eta from current directory up to root
176
- const dirsToSearch = [];
177
- // Add current directory if not root
178
- if (pathSegments.length > 0) {
179
- for (let i = pathSegments.length; i > 0; i--) {
180
- dirsToSearch.push(pathSegments.slice(0, i).join('/'));
181
- }
182
- }
183
- // Add root directory
184
- dirsToSearch.push('');
185
- for (const dir of dirsToSearch) {
186
- // For index pages, first check for index.eta in each directory
187
- if (isIndexPage) {
188
- const indexLayoutPath = dir ? join(srcDir, dir, 'index.eta') : join(srcDir, 'index.eta');
189
- if (await pathExists(indexLayoutPath)) {
190
- const relativePath = dir ? `${dir}/index.eta` : 'index.eta';
191
- return posix.normalize(relativePath);
192
- }
193
- }
194
- // Then check for layout.eta as fallback
195
- const layoutPath = dir ? join(srcDir, dir, 'layout.eta') : join(srcDir, 'layout.eta');
196
- if (await pathExists(layoutPath)) {
197
- const relativePath = dir ? `${dir}/layout.eta` : 'layout.eta';
198
- return posix.normalize(relativePath);
199
- }
200
- }
201
- return null;
202
- }
203
147
  /**
204
148
  * Detects circular dependencies in template includes/extends.
205
149
  * Uses DFS to traverse the dependency graph and detect cycles.
@@ -233,6 +177,9 @@ async function detectCircularDependencies(templatePath, srcDir, visited, current
233
177
  try {
234
178
  // Read template content to find includes/extends
235
179
  const content = await readFile(templatePath, 'utf-8');
180
+ if (!content) {
181
+ return; // Skip if file doesn't exist
182
+ }
236
183
  const dependencies = await parseTemplateDependencies(content, templatePath, srcDir);
237
184
  // Recursively check dependencies
238
185
  for (const depPath of dependencies) {
@@ -264,6 +211,7 @@ async function detectCircularDependencies(templatePath, srcDir, visited, current
264
211
  */
265
212
  async function parseTemplateDependencies(content, templatePath, srcDir) {
266
213
  const dependencies = [];
214
+ const templateDir = dirname(templatePath);
267
215
  // Look for Eta include patterns: <%~ include('template') %>
268
216
  const includePatterns = [
269
217
  /<%[~-]?\s*include\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
@@ -274,7 +222,7 @@ async function parseTemplateDependencies(content, templatePath, srcDir) {
274
222
  while ((match = pattern.exec(content)) !== null) {
275
223
  const includePath = match[1];
276
224
  if (includePath) {
277
- const resolvedPath = await resolveTemplatePathInternal(includePath, srcDir);
225
+ const resolvedPath = await resolveTemplatePathInternal(includePath, srcDir, templateDir);
278
226
  if (resolvedPath) {
279
227
  dependencies.push(resolvedPath);
280
228
  }
@@ -291,7 +239,7 @@ async function parseTemplateDependencies(content, templatePath, srcDir) {
291
239
  while ((match = pattern.exec(content)) !== null) {
292
240
  const layoutPath = match[1];
293
241
  if (layoutPath) {
294
- const resolvedPath = await resolveTemplatePathInternal(layoutPath, srcDir);
242
+ const resolvedPath = await resolveTemplatePathInternal(layoutPath, srcDir, templateDir);
295
243
  if (resolvedPath) {
296
244
  dependencies.push(resolvedPath);
297
245
  }
@@ -306,26 +254,59 @@ async function parseTemplateDependencies(content, templatePath, srcDir) {
306
254
  *
307
255
  * @param templateRef - Template reference from include/layout statement
308
256
  * @param srcDir - Source directory for resolving paths
257
+ * @param currentDir - Current directory for hierarchical search (optional)
309
258
  * @returns Absolute path to template file, or null if not found
310
259
  */
311
- async function resolveTemplatePathInternal(templateRef, srcDir) {
312
- // Add .eta extension if not present
313
- const templateName = templateRef.endsWith('.eta') ? templateRef : `${templateRef}.eta`;
260
+ async function resolveTemplatePathInternal(templateRef, srcDir, currentDir) {
261
+ const templateName = templateRef.endsWith(TEMPLATE_EXTENSION)
262
+ ? templateRef
263
+ : `${templateRef}${TEMPLATE_EXTENSION}`;
314
264
  // Try absolute path from srcDir
315
265
  const absolutePath = join(srcDir, templateName);
316
266
  if (await pathExists(absolutePath)) {
317
267
  return absolutePath;
318
268
  }
319
- // Try resolving relative to template directories
320
- // This is a simplified version - real implementation might need more sophisticated resolution
321
- const possiblePaths = [
322
- join(srcDir, '_templates', templateName),
323
- join(srcDir, '_partials', templateName),
324
- join(srcDir, '_layouts', templateName),
325
- ];
326
- for (const path of possiblePaths) {
327
- if (await pathExists(path)) {
328
- return path;
269
+ // Determine the starting directory for hierarchical search (relative to srcDir)
270
+ const startDir = currentDir ? relative(srcDir, currentDir) : '';
271
+ // Build list of directories to search (current dir up to srcDir root only)
272
+ // This ensures we never search outside the source directory boundary
273
+ const dirsToSearch = [];
274
+ // Safety check: ensure currentDir is within srcDir (relative path shouldn't start with '..')
275
+ if (startDir.startsWith('..')) {
276
+ // If currentDir is outside srcDir, only search at srcDir root
277
+ dirsToSearch.push('');
278
+ }
279
+ else {
280
+ const pathSegments = startDir === '' ? [] : startDir.split(/[/\\]/);
281
+ // Add directories from current up to srcDir root (don't go beyond srcDir)
282
+ if (pathSegments.length > 0) {
283
+ for (let i = pathSegments.length; i >= 0; i--) {
284
+ if (i === 0) {
285
+ dirsToSearch.push(''); // srcDir root directory
286
+ }
287
+ else {
288
+ dirsToSearch.push(pathSegments.slice(0, i).join('/'));
289
+ }
290
+ }
291
+ }
292
+ else {
293
+ dirsToSearch.push(''); // srcDir root directory only
294
+ }
295
+ }
296
+ // Search each directory level for the template in underscore directories
297
+ for (const dir of dirsToSearch) {
298
+ const searchDir = dir ? join(srcDir, dir) : srcDir;
299
+ try {
300
+ // Search for template in underscore directories at this level only
301
+ const pattern = posix.join(searchDir.replace(/\\/g, '/'), '_*', templateName);
302
+ const matches = await glob(pattern, { absolute: true });
303
+ if (matches.length > 0) {
304
+ return matches[0]; // Return first match
305
+ }
306
+ }
307
+ catch {
308
+ // Continue if directory doesn't exist or can't be read
309
+ continue;
329
310
  }
330
311
  }
331
312
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/isg/hash.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAWhG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiB9E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAanF"}
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/isg/hash.ts"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAKhG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiB9E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAKnF"}