clovie 0.1.32 → 0.1.36

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 (67) hide show
  1. package/README.md +3 -34
  2. package/bin/cli.js +1 -12
  3. package/lib/Apps.js +66 -0
  4. package/lib/Cache.js +94 -0
  5. package/lib/Compile.js +432 -0
  6. package/lib/Configurator.js +101 -0
  7. package/lib/File.js +276 -0
  8. package/lib/LiveReload.js +145 -0
  9. package/lib/Run.js +182 -0
  10. package/lib/Server/Kernel.js +155 -0
  11. package/lib/Server/Router.js +189 -0
  12. package/lib/Server/Server.js +260 -0
  13. package/lib/Server/adapters/AdapterInterface.js +340 -0
  14. package/lib/Server/adapters/ExpressAdapter.js +133 -0
  15. package/lib/Server/adapters/HttpAdapter.js +117 -0
  16. package/lib/Server/adapters/index.js +15 -0
  17. package/lib/Server/main.js +1 -0
  18. package/lib/Server/utils/httpParsing.js +97 -0
  19. package/lib/Server/utils/routeMatch.js +98 -0
  20. package/lib/Server/utils/serverReady.js +83 -0
  21. package/lib/createClovie.js +47 -0
  22. package/lib/createViteApp.js +45 -0
  23. package/lib/examples/kernel-adapters.js +100 -0
  24. package/lib/examples/kernel-usage.js +177 -0
  25. package/lib/types/kernel.js +120 -0
  26. package/lib/utils/clean.js +21 -0
  27. package/lib/utils/liveReloadScript.js +48 -0
  28. package/lib/utils/loadRenderEngine.js +135 -0
  29. package/lib/utils/outputPath.js +28 -0
  30. package/lib/utils/progress.js +108 -0
  31. package/lib/utils/tasks.js +57 -0
  32. package/lib/utils/transformConfig.js +349 -0
  33. package/package.json +11 -19
  34. package/templates/server/AI_DEVELOPMENT_GUIDE.md +59 -439
  35. package/templates/server/README.md +21 -116
  36. package/templates/server/clovie.config.js +7 -98
  37. package/templates/server/views/about.html +2 -12
  38. package/templates/server/views/index.html +2 -2
  39. package/templates/static/AI_DEVELOPMENT_GUIDE.md +20 -77
  40. package/dist/ExpressAdapter-BsYIpF7A.js +0 -109
  41. package/dist/ExpressAdapter-BsYIpF7A.js.map +0 -1
  42. package/dist/LiveReload-ClrG9hAe.js +0 -101
  43. package/dist/LiveReload-ClrG9hAe.js.map +0 -1
  44. package/dist/Server-GcEIC8Hj.js +0 -554
  45. package/dist/Server-GcEIC8Hj.js.map +0 -1
  46. package/dist/cjs/ExpressAdapter--oUcRq-k.cjs +0 -81
  47. package/dist/cjs/ExpressAdapter--oUcRq-k.cjs.map +0 -1
  48. package/dist/cjs/LiveReload-BvFz-bG3.cjs +0 -76
  49. package/dist/cjs/LiveReload-BvFz-bG3.cjs.map +0 -1
  50. package/dist/cjs/Server-DkYaMJak.cjs +0 -554
  51. package/dist/cjs/Server-DkYaMJak.cjs.map +0 -1
  52. package/dist/cjs/createClovie-CJ1h3Pfo.cjs +0 -6430
  53. package/dist/cjs/createClovie-CJ1h3Pfo.cjs.map +0 -1
  54. package/dist/cjs/index-DU3_mYKB.cjs +0 -461
  55. package/dist/cjs/index-DU3_mYKB.cjs.map +0 -1
  56. package/dist/cjs/index.cjs +0 -9
  57. package/dist/cjs/index.cjs.map +0 -1
  58. package/dist/createClovie-Dh7RbB5L.js +0 -6448
  59. package/dist/createClovie-Dh7RbB5L.js.map +0 -1
  60. package/dist/index-D8ThZ_1b.js +0 -484
  61. package/dist/index-D8ThZ_1b.js.map +0 -1
  62. package/dist/index.d.ts +0 -229
  63. package/dist/index.js +0 -32
  64. package/dist/index.js.map +0 -1
  65. package/scripts/build-types.js +0 -247
  66. package/scripts/killPort.js +0 -138
  67. package/scripts/publish.js +0 -180
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > *"The Hollow Knight of Web Dev"* - Simple but deep, easy to start but room to grow.
4
4
 
5
- A powerful Node.js-based **static site generator** and **full-stack web framework** that bridges the gap between simple static sites and complex web applications. Built on the **@brickworks/engine** service architecture for maximum flexibility and maintainability.
5
+ A powerful Node.js-based **static site generator** and **full-stack web framework** that bridges the gap between simple static sites and complex web applications. Built on the **@jucie.io/engine** service architecture for maximum flexibility and maintainability.
6
6
 
7
7
  ## 🚀 Quick Start
8
8
 
@@ -24,14 +24,13 @@ npm run dev
24
24
  - **🎨 Template Agnostic**: Handlebars, Nunjucks, Pug, Mustache, or custom engines
25
25
  - **📦 Asset Pipeline**: SCSS compilation, JavaScript bundling with esbuild
26
26
  - **🔄 Live Reload**: WebSocket-based live reload during development
27
- - **🗄️ Database Ready**: SQLite integration for server mode applications
28
27
  - **🛣️ Dynamic Routing**: Data-driven page generation and API endpoints
29
28
  - **🧩 App Orchestration**: Build and serve external Vite/Webpack/Rollup/esbuild apps via kernel handlers
30
29
  - **🔧 Service Architecture**: Modular, extensible service-oriented design
31
30
 
32
31
  ## 🏗️ Architecture Overview
33
32
 
34
- Clovie uses a **service-oriented architecture** built on `@brickworks/engine`. All functionality is provided by services that extend `ServiceProvider`, orchestrated through dependency injection with reactive state management.
33
+ Clovie uses a **service-oriented architecture** built on `@jucie.io/engine`. All functionality is provided by services that extend `ServiceProvider`, orchestrated through dependency injection with reactive state management.
35
34
 
36
35
  ### Core Services
37
36
 
@@ -277,9 +276,6 @@ export default {
277
276
  }
278
277
  },
279
278
 
280
- // Database configuration (optional)
281
- dbPath: './data/app.db',
282
-
283
279
  // Express middleware (auto-selects Express adapter)
284
280
  middleware: [
285
281
  express.json(),
@@ -433,28 +429,6 @@ export default {
433
429
 
434
430
  See [Apps Integration](docs/CONFIGURATION.md#apps-integration) for full examples covering Webpack, Rollup, and esbuild setups.
435
431
 
436
- ### 📊 Database Integration (Server Mode)
437
-
438
- Clovie includes built-in SQLite database support for server applications:
439
-
440
- ```javascript
441
- export default {
442
- type: 'server',
443
- dbPath: './data/app.db', // SQLite database path
444
-
445
- // Database is available in API actions and routes
446
- api: [{
447
- path: '/api/posts',
448
- method: 'GET',
449
- action: async (state, event) => {
450
- // Access database through state.db
451
- const posts = await state.db.query('SELECT * FROM posts ORDER BY created_at DESC');
452
- return { posts };
453
- }
454
- }]
455
- };
456
- ```
457
-
458
432
  ### 🔄 Async Data Loading
459
433
 
460
434
  Load data dynamically at build time or runtime:
@@ -628,7 +602,7 @@ my-site/
628
602
  - Validate input data in API actions
629
603
  - Use middleware for authentication and request parsing
630
604
  - Handle errors gracefully in API endpoints
631
- - Implement proper database migrations and seeding
605
+ - Implement proper data migrations and seeding
632
606
 
633
607
  ### 🔧 Configuration Tips
634
608
 
@@ -715,11 +689,6 @@ clovie kill --check # Check port status
715
689
  - Check that template files have correct extensions
716
690
  - Ensure partial files are in the `partials` directory
717
691
 
718
- **Database Connection Issues (Server Mode)**
719
- - Check that `dbPath` in config points to a writable directory
720
- - Ensure SQLite is properly installed
721
- - Verify database initialization in your API actions
722
-
723
692
  **Live Reload Not Working**
724
693
  - Check that you're in development mode (`npm run dev`)
725
694
  - Verify WebSocket connection in browser dev tools
package/bin/cli.js CHANGED
@@ -13,8 +13,7 @@ const packageJson = JSON.parse(readFileSync(path.join(__dirname, '../package.jso
13
13
  const CLOVIE_VERSION = packageJson.version;
14
14
 
15
15
  // Local - import from compiled dist for published package
16
- // import { createClovie } from "../lib/createClovie.js";
17
- import { createClovie } from "../dist/index.js";
16
+ import { createClovie } from "../lib/createClovie.js";
18
17
  import { killPort, checkPorts, killCommonPorts } from "../scripts/killPort.js";
19
18
 
20
19
  // Check for kill command first (before any argument parsing)
@@ -361,16 +360,6 @@ function setupGracefulShutdown(clovie) {
361
360
  }, 5000); // 5 second timeout
362
361
 
363
362
  try {
364
- // Close database if it exists and is initialized
365
- const database = clovie.database;
366
- if (database && database.isInitialized && database.isInitialized()) {
367
- console.log('💾 Closing database...');
368
- if (typeof database.checkpoint === 'function') {
369
- await database.checkpoint();
370
- console.log('✅ Database written successfully');
371
- }
372
- }
373
-
374
363
  // Stop server if it's running
375
364
  const server = clovie.server;
376
365
  if (server && server.isRunning && server.isRunning()) {
package/lib/Apps.js ADDED
@@ -0,0 +1,66 @@
1
+ import { ServiceProvider } from '@jucie.io/engine';
2
+ import path from 'path';
3
+ import fs from 'fs/promises';
4
+ import { spawn } from 'child_process';
5
+ import { pathToFileURL } from 'url';
6
+ import { PageCache } from './Cache.js';
7
+ import { File } from './File.js';
8
+ import { createProgressTracker } from './utils/progress.js';
9
+
10
+ export class Apps extends ServiceProvider {
11
+ static manifest = {
12
+ name: 'Clovie Apps',
13
+ namespace: 'apps',
14
+ version: '1.0.0',
15
+ dependencies: [PageCache, File],
16
+ defaults: {
17
+ apps: []
18
+ }
19
+ };
20
+
21
+ #progressTracker = null;
22
+ #buildProcesses = new Map();
23
+ #buildResults = new Map();
24
+ #handlers = new Map();
25
+ #apps = [];
26
+
27
+ actions(useContext) {
28
+ const log = useContext('log');
29
+
30
+
31
+ return {
32
+ build: async (opts = {}) => {
33
+ this.#apps = opts.apps || [];
34
+ try {
35
+
36
+ for (const app of this.#apps) {
37
+ const { default: config } = await import(app.config);
38
+ await this.createViteApp(opts, config);
39
+ }
40
+
41
+ return true
42
+
43
+ } catch (error) {
44
+ console.error('Error building apps', error);
45
+ }
46
+ return true
47
+ }
48
+ };
49
+ }
50
+
51
+ async createViteApp(opts = {}, config = {}) {
52
+ const [server, log] = this.useContext('server', 'log');
53
+ const { createServer } = await import('vite');
54
+ const vite = await createServer({
55
+ httpServer: server.getHttpServer(),
56
+ root: opts.root,
57
+ configFile: opts.configFile
58
+ });
59
+
60
+ // server.add('GET', '/vite', async (ctx) => {
61
+ // const url = ctx.req.originalUrl;
62
+ // const template = await vite.transformIndexHtml(url, fs.readFileSync(join(opts.root, 'index.html'), 'utf-8'));
63
+ // return ctx.respond.html(template);
64
+ // })
65
+ }
66
+ }
package/lib/Cache.js ADDED
@@ -0,0 +1,94 @@
1
+ import { ServiceProvider } from '@jucie.io/engine';
2
+ import crypto from 'crypto';
3
+
4
+ export class PageCache extends ServiceProvider {
5
+ static manifest = {
6
+ name: 'Clovie Page Cache',
7
+ namespace: 'pageCache',
8
+ version: '1.0.0',
9
+ }
10
+
11
+ actions(useContext) {
12
+ const { cache } = useContext();
13
+ return {
14
+ /**
15
+ * Set cached content with hash for comparison
16
+ */
17
+ set: (filePath, content) => {
18
+ const hash = this.#createHash(content);
19
+ const cacheEntry = {
20
+ filePath,
21
+ hash,
22
+ timestamp: Date.now()
23
+ };
24
+ cache.set(['.cache', filePath], cacheEntry);
25
+ return hash;
26
+ },
27
+
28
+ /**
29
+ * Get cached content
30
+ */
31
+ get: (filePath) => {
32
+ const entry = cache.get(['.cache', filePath]);
33
+ return entry ? entry.content : null;
34
+ },
35
+
36
+ /**
37
+ * Check if content has changed by comparing hashes
38
+ */
39
+ hasChanged: (filePath, newContent) => {
40
+ return this.#hasChanged(filePath, newContent);
41
+ },
42
+
43
+ /**
44
+ * Get hash of cached content
45
+ */
46
+ getHash: (filePath) => {
47
+ const entry = cache.get(['.cache', filePath]);
48
+ return entry ? entry.hash : null;
49
+ },
50
+
51
+ /**
52
+ * Get cache entry metadata
53
+ */
54
+ getEntry: (filePath) => {
55
+ return cache.get(['.cache', filePath]);
56
+ },
57
+
58
+ /**
59
+ * Clear cache entry
60
+ */
61
+ clear: (filePath) => {
62
+ cache.delete(['.cache', filePath]);
63
+ },
64
+
65
+ /**
66
+ * Clear all cache
67
+ */
68
+ clearAll: () => {
69
+ cache.delete(['.cache']);
70
+ }
71
+ }
72
+ }
73
+
74
+ #hasChanged(filePath, newContent) {
75
+ const cache = this.useContext('cache');
76
+ const entry = cache.get(['.cache', filePath]);
77
+
78
+ if (!entry) {
79
+ return true; // No cache entry means it's new/changed
80
+ }
81
+
82
+ const newHash = this.#createHash(newContent);
83
+ return entry.hash !== newHash;
84
+ }
85
+
86
+ /**
87
+ * Create consistent hash for content
88
+ */
89
+ #createHash(content) {
90
+ // Handle different content types
91
+ const contentString = typeof content === 'string' ? content : JSON.stringify(content);
92
+ return crypto.createHash('sha256').update(contentString).digest('hex');
93
+ }
94
+ }
package/lib/Compile.js ADDED
@@ -0,0 +1,432 @@
1
+ import { ServiceProvider } from '@jucie.io/engine';
2
+ import path from 'path';
3
+ import { PageCache } from './Cache.js';
4
+ import { File } from './File.js';
5
+ import { createProgressTracker } from './utils/progress.js';
6
+
7
+ import { replaceRouteParams } from './Server/utils/routeMatch.js';
8
+ import { formatOutputPath } from './utils/outputPath.js';
9
+
10
+ export class Compile extends ServiceProvider {
11
+
12
+ static manifest = {
13
+ name: 'Clovie Compile',
14
+ namespace: 'compile',
15
+ version: '1.0.0',
16
+ dependencies: [PageCache, File]
17
+ };
18
+
19
+ #progressTracker = null;
20
+ #pathMap = new Map();
21
+
22
+ actions(useContext) {
23
+ const [file, log] = useContext('file', 'log');
24
+
25
+ // Initialize progress tracker if not already done
26
+ if (!this.#progressTracker) {
27
+ this.#progressTracker = createProgressTracker(log);
28
+ }
29
+
30
+ return {
31
+ assets: async (opts) => {
32
+ if (!opts.assets) {
33
+ log.debug('No assets directory to compile');
34
+ return [];
35
+ }
36
+ const filePaths = file.getFilePaths(opts.assets);
37
+
38
+ if (filePaths.length === 0) {
39
+ log.debug('No assets to compile');
40
+ return [];
41
+ }
42
+
43
+ const progressId = this.#progressTracker.createProgressBar('Compiling Assets', filePaths.length, 'files');
44
+ const promises = [];
45
+
46
+ for (let i = 0; i < filePaths.length; i++) {
47
+ const filePath = filePaths[i];
48
+ promises.push(
49
+ this.#asset(filePath, opts).then(() => {
50
+ this.#progressTracker.updateProgress(progressId, i + 1, {
51
+ current: path.basename(filePath)
52
+ });
53
+ })
54
+ );
55
+ }
56
+
57
+ await Promise.all(promises);
58
+ this.#progressTracker.completeProgress(progressId, `✅ Compiled ${filePaths.length} assets`);
59
+
60
+ return promises;
61
+ },
62
+ scripts: async (opts) => {
63
+ if (!opts.scripts) {
64
+ log.debug('No scripts directory to compile');
65
+ return [];
66
+ }
67
+ const filePaths = file.getFilePaths(opts.scripts);
68
+
69
+ if (filePaths.length === 0) {
70
+ log.debug('No scripts to compile');
71
+ return [];
72
+ }
73
+
74
+ const progressId = this.#progressTracker.createProgressBar('Compiling Scripts', filePaths.length, 'files');
75
+ const promises = [];
76
+
77
+ for (let i = 0; i < filePaths.length; i++) {
78
+ const filePath = filePaths[i];
79
+ promises.push(
80
+ this.#script(filePath, opts).then(() => {
81
+ this.#progressTracker.updateProgress(progressId, i + 1, {
82
+ current: path.basename(filePath)
83
+ });
84
+ })
85
+ );
86
+ }
87
+
88
+ await Promise.all(promises);
89
+ this.#progressTracker.completeProgress(progressId, `✅ Compiled ${filePaths.length} scripts`);
90
+
91
+ return promises;
92
+ },
93
+ styles: async (opts) => {
94
+ if (!opts.styles) {
95
+ log.debug('No styles directory to compile');
96
+ return [];
97
+ }
98
+ const filePaths = file.getFilePaths(opts.styles);
99
+
100
+ if (filePaths.length === 0) {
101
+ log.debug('No styles to compile');
102
+ return [];
103
+ }
104
+
105
+ const progressId = this.#progressTracker.createProgressBar('Compiling Styles', filePaths.length, 'files');
106
+ const promises = [];
107
+
108
+ for (let i = 0; i < filePaths.length; i++) {
109
+ const filePath = filePaths[i];
110
+ promises.push(
111
+ this.#style(filePath, opts).then(() => {
112
+ this.#progressTracker.updateProgress(progressId, i + 1, {
113
+ current: path.basename(filePath)
114
+ });
115
+ })
116
+ );
117
+ }
118
+
119
+ await Promise.all(promises);
120
+ this.#progressTracker.completeProgress(progressId, `✅ Compiled ${filePaths.length} styles`);
121
+
122
+ return promises;
123
+ },
124
+ partials: async (opts) => {
125
+ if (!opts.partials) {
126
+ log.debug('No partials directory to compile');
127
+ return [];
128
+ }
129
+ const filePaths = file.getFilePaths(opts.partials);
130
+
131
+ if (filePaths.length === 0) {
132
+ log.debug('No partials to load');
133
+ return [];
134
+ }
135
+
136
+ const progressId = this.#progressTracker.createProgressBar('Loading Partials', filePaths.length, 'files');
137
+ const promises = [];
138
+
139
+ for (let i = 0; i < filePaths.length; i++) {
140
+ const filePath = filePaths[i];
141
+ promises.push(
142
+ Promise.resolve(this.#loadPartial(filePath, opts)).then(() => {
143
+ this.#progressTracker.updateProgress(progressId, i + 1, {
144
+ current: path.basename(filePath)
145
+ });
146
+ })
147
+ );
148
+ }
149
+
150
+ await Promise.all(promises);
151
+ this.#progressTracker.completeProgress(progressId, `✅ Loaded ${filePaths.length} partials`);
152
+
153
+ return promises;
154
+ },
155
+ views: async (opts) => {
156
+ if (!opts.views) {
157
+ log.debug('No views directory to compile');
158
+ return [];
159
+ }
160
+ const filePaths = file.getFilePaths(opts.views);
161
+
162
+ if (filePaths.length === 0) {
163
+ log.debug('No views to compile');
164
+ return [];
165
+ }
166
+
167
+ const progressId = this.#progressTracker.createProgressBar('Compiling Views', filePaths.length, 'files');
168
+ const promises = [];
169
+
170
+ for (let i = 0; i < filePaths.length; i++) {
171
+ const filePath = filePaths[i];
172
+ console.log('filePath', filePath);
173
+ promises.push(
174
+ this.#view(filePath, opts).then(() => {
175
+ this.#progressTracker.updateProgress(progressId, i + 1, {
176
+ current: path.basename(filePath)
177
+ });
178
+ })
179
+ );
180
+ }
181
+
182
+ await Promise.all(promises);
183
+ this.#progressTracker.completeProgress(progressId, `✅ Compiled ${filePaths.length} views`);
184
+
185
+ return promises;
186
+ },
187
+
188
+ routes: async (opts) => {
189
+ if (!opts.routes || opts.routes.length === 0) {
190
+ log.debug('No routes to compile');
191
+ return [];
192
+ }
193
+
194
+ const progressId = this.#progressTracker.createProgressBar('Compiling Routes', opts.routes.length, 'routes');
195
+ const promises = [];
196
+ const routesLength = opts.routes.length;
197
+
198
+ for (let i = 0; i < routesLength; i++) {
199
+ const route = opts.routes[i];
200
+ if (route.repeat) {
201
+ const data = route.repeat(opts.data);
202
+ const entries = Array.isArray(data) ? data.entries() : Object.entries(data);
203
+ for (const [key, value] of entries) {
204
+ promises.push(
205
+ this.#route(route, opts, value, key).then(() => {
206
+ this.#progressTracker.updateProgress(progressId, i + 1, {
207
+ current: route.path || 'route'
208
+ });
209
+ })
210
+ );
211
+ }
212
+ continue;
213
+ }
214
+ promises.push(
215
+ this.#route(route, opts, null, null).then(() => {
216
+ this.#progressTracker.updateProgress(progressId, i + 1, {
217
+ current: route.path || 'route'
218
+ });
219
+ })
220
+ );
221
+ }
222
+
223
+ await Promise.all(promises);
224
+ this.#progressTracker.completeProgress(progressId, `✅ Compiled ${opts.routes.length} routes`);
225
+
226
+ return promises;
227
+ },
228
+
229
+ asset: (path, opts) => this.#asset(path, opts),
230
+ script: (path, opts) => this.#script(path, opts),
231
+ style: (path, opts) => this.#style(path, opts),
232
+ partial: (path, opts) => this.#loadPartial(path, opts),
233
+ view: (path, opts) => this.#view(path, opts),
234
+ remove: (path, opts) => this.#remove(path, opts),
235
+ route: (route, opts, item, key) => this.#route(route, opts, item, key),
236
+ };
237
+ }
238
+
239
+ #remove(sourcePath) {
240
+ const file = this.useContext('file');
241
+ const outputPaths = this.#pathMap.get(sourcePath);
242
+ if (outputPaths) {
243
+ outputPaths.forEach(outputPath => {
244
+ file.delete(outputPath);
245
+ });
246
+ }
247
+ }
248
+
249
+ async #asset(filePath, opts) {
250
+ const [file, log] = this.useContext('file', 'log');
251
+ const content = file.read(filePath);
252
+
253
+ if (content) {
254
+ const assetsDir = opts.assets;
255
+ const absoluteAssetsDir = this.#toAbsolutePath(assetsDir);
256
+ const absoluteFilePath = this.#toAbsolutePath(filePath);
257
+ const relativePath = path.relative(absoluteAssetsDir, absoluteFilePath);
258
+
259
+ log.debug(`Processing asset: ${filePath}`);
260
+ log.debug(`Asset processed: ${relativePath}`);
261
+ this.#registerOutputPath(filePath, relativePath, opts.outputDir);
262
+ return this.#write(relativePath, content, opts.outputDir);
263
+ }
264
+ }
265
+
266
+ async #script(filePath, opts) {
267
+ const [file, log] = this.useContext('file', 'log');
268
+ const content = file.read(filePath);
269
+
270
+ if (content && opts.scriptCompiler && typeof opts.scriptCompiler === 'function') {
271
+ const scriptsDir = opts.scripts;
272
+ const fileName = path.basename(filePath);
273
+
274
+ // Ensure both paths are absolute for proper relative calculation
275
+ const absoluteScriptsDir = this.#toAbsolutePath(scriptsDir);
276
+ const absoluteFilePath = this.#toAbsolutePath(filePath);
277
+ const relativePath = path.relative(absoluteScriptsDir, absoluteFilePath);
278
+
279
+ try {
280
+ log.debug(`Using configured script compiler for: ${filePath}`);
281
+ const compiled = await opts.scriptCompiler(content, filePath);
282
+ log.debug(`Script compiled successfully: ${fileName}`);
283
+ this.#registerOutputPath(filePath, relativePath, opts.outputDir);
284
+ return this.#write(relativePath, compiled, opts.outputDir);
285
+ } catch (err) {
286
+ log.warn(`Script compilation failed for ${filePath}: ${err.message}`);
287
+ }
288
+ }
289
+ }
290
+
291
+ async #style(filePath, opts) {
292
+ const [file, log] = this.useContext('file', 'log');
293
+ const content = file.read(filePath);
294
+
295
+ if (content && opts.styleCompiler && typeof opts.styleCompiler === 'function') {
296
+ const stylesDir = opts.styles;
297
+
298
+ // Ensure both paths are absolute for proper relative calculation
299
+ const absoluteStylesDir = this.#toAbsolutePath(stylesDir);
300
+ const absoluteFilePath = this.#toAbsolutePath(filePath);
301
+ const relativePath = path.relative(absoluteStylesDir, absoluteFilePath).replace(/\.(scss|sass|less|styl)$/, '.css');
302
+ const fileName = path.basename(relativePath);
303
+
304
+ try {
305
+ log.debug(`Using configured style compiler for: ${filePath}`);
306
+ const compiled = await opts.styleCompiler(content, filePath);
307
+ log.debug(`Style compiled successfully: ${fileName}`);
308
+ this.#registerOutputPath(filePath, relativePath, opts.outputDir);
309
+ return this.#write(relativePath, compiled, opts.outputDir);
310
+ } catch (err) {
311
+ log.warn(`Style compilation failed for ${filePath}: ${err.message}`);
312
+ }
313
+ }
314
+ }
315
+
316
+ #loadPartial(filePath, opts) {
317
+ const [file, log] = this.useContext('file', 'log');
318
+
319
+ if (file.exists(filePath) && opts.renderEngine) {
320
+ try {
321
+ const partialsDir = opts.partials;
322
+ const relativePath = path.relative(partialsDir, filePath);
323
+
324
+ // Get the partial name without extension (e.g., 'header.html' -> 'header')
325
+ const partialName = path.parse(relativePath).name;
326
+
327
+ // Read the partial content
328
+ const content = file.read(filePath);
329
+
330
+ log.debug(`Registering partial: ${partialName}`);
331
+ opts.renderEngine.register(partialName, content);
332
+ log.debug(`Registered partial: ${partialName}`);
333
+ } catch (err) {
334
+ log.warn('Could not register partials:', err.message);
335
+ }
336
+ }
337
+ }
338
+
339
+ async #view(filePath, opts) {
340
+ const [file, liveReload, log] = this.useContext('file', 'liveReload', 'log');
341
+
342
+ if (file.exists(filePath) && opts.renderEngine) {
343
+ const fileName = path.basename(filePath);
344
+ const viewsDir = opts.views;
345
+
346
+ // Ensure both paths are absolute for proper relative calculation
347
+ const absoluteViewsDir = this.#toAbsolutePath(viewsDir);
348
+ const absoluteFilePath = this.#toAbsolutePath(filePath);
349
+ const relativePath = path.relative(absoluteViewsDir, absoluteFilePath);
350
+ const renderedContent = await this.#template(filePath, opts.data, opts);
351
+ this.#registerOutputPath(relativePath, relativePath, opts.outputDir);
352
+ return this.#write(relativePath, renderedContent, opts.outputDir);
353
+ }
354
+ }
355
+
356
+ async #route(route, opts, value, key) {
357
+ const [file, log] = this.useContext('file', 'log');
358
+ const data = route.data(opts.data, value, key);
359
+ const relativePath = replaceRouteParams(route.path, data);
360
+ const outputPath = formatOutputPath(relativePath);
361
+
362
+ if (file.exists(route.template) && opts.renderEngine) {
363
+ const renderedContent = await this.#template(route.template, data, opts);
364
+ this.#registerOutputPath(relativePath, outputPath, opts.outputDir);
365
+ return this.#write(outputPath, renderedContent, opts.outputDir);
366
+ } else {
367
+ log.warn(`No template compiler configured for route: ${route.path}`);
368
+ }
369
+ }
370
+
371
+ async #template(templatePath, data, opts) {
372
+ const [file, log] = this.useContext('file', 'log');
373
+ try {
374
+ const template = file.read(templatePath);
375
+ let renderedContent = await opts.renderEngine.render(template, data);
376
+ renderedContent = await this.#injectLiveReloadScript(renderedContent, opts);
377
+ return renderedContent;
378
+ } catch (err) {
379
+ log.debug('Error compiling', err)
380
+ }
381
+ }
382
+
383
+ async #injectLiveReloadScript(renderedContent, opts) {
384
+ const [liveReload, log] = this.useContext('liveReload', 'log');
385
+ try {
386
+ if (liveReload && renderedContent.includes('</body>')) {
387
+ log.debug('Injecting live reload script');
388
+ renderedContent = await liveReload.injectLiveReloadScript(renderedContent, opts);
389
+ }
390
+ return renderedContent;
391
+ } catch (err) {
392
+ log.debug('Error injecting live reload script', err);
393
+ return renderedContent;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Convert a path to an absolute path
399
+ * @param {string} inputPath - The path to normalize
400
+ * @returns {string} - Absolute path
401
+ */
402
+ #toAbsolutePath(inputPath) {
403
+ return path.isAbsolute(inputPath) ? inputPath : path.resolve(process.cwd(), inputPath);
404
+ }
405
+
406
+ #registerOutputPath(sourcePath, relativePath, outputDir) {
407
+ const outputPath = path.join(outputDir, relativePath);
408
+ if (!this.#pathMap.has(sourcePath)) {
409
+ this.#pathMap.set(sourcePath, new Set());
410
+ }
411
+ this.#pathMap.get(sourcePath).add(outputPath);
412
+ }
413
+
414
+ #write(filePath, content, outputDir) {
415
+ const [file, pageCache, log, liveReload] = this.useContext('file', 'pageCache', 'log', 'liveReload');
416
+ const outputPath = path.join(outputDir, filePath);
417
+
418
+ // Cache based on OUTPUT content (handles both template and data changes)
419
+ if (pageCache.hasChanged(outputPath, content)) {
420
+ file.write(outputPath, content);
421
+ pageCache.set(outputPath, content);
422
+ log.debug(`Written: ${filePath}`);
423
+
424
+ if (liveReload) {
425
+ liveReload.notifyReload();
426
+ }
427
+ return outputPath;
428
+ } else {
429
+ log.debug(`Skipped (no changes): ${filePath}`);
430
+ }
431
+ }
432
+ }