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.
- package/README.md +3 -34
- package/bin/cli.js +1 -12
- package/lib/Apps.js +66 -0
- package/lib/Cache.js +94 -0
- package/lib/Compile.js +432 -0
- package/lib/Configurator.js +101 -0
- package/lib/File.js +276 -0
- package/lib/LiveReload.js +145 -0
- package/lib/Run.js +182 -0
- package/lib/Server/Kernel.js +155 -0
- package/lib/Server/Router.js +189 -0
- package/lib/Server/Server.js +260 -0
- package/lib/Server/adapters/AdapterInterface.js +340 -0
- package/lib/Server/adapters/ExpressAdapter.js +133 -0
- package/lib/Server/adapters/HttpAdapter.js +117 -0
- package/lib/Server/adapters/index.js +15 -0
- package/lib/Server/main.js +1 -0
- package/lib/Server/utils/httpParsing.js +97 -0
- package/lib/Server/utils/routeMatch.js +98 -0
- package/lib/Server/utils/serverReady.js +83 -0
- package/lib/createClovie.js +47 -0
- package/lib/createViteApp.js +45 -0
- package/lib/examples/kernel-adapters.js +100 -0
- package/lib/examples/kernel-usage.js +177 -0
- package/lib/types/kernel.js +120 -0
- package/lib/utils/clean.js +21 -0
- package/lib/utils/liveReloadScript.js +48 -0
- package/lib/utils/loadRenderEngine.js +135 -0
- package/lib/utils/outputPath.js +28 -0
- package/lib/utils/progress.js +108 -0
- package/lib/utils/tasks.js +57 -0
- package/lib/utils/transformConfig.js +349 -0
- package/package.json +11 -19
- package/templates/server/AI_DEVELOPMENT_GUIDE.md +59 -439
- package/templates/server/README.md +21 -116
- package/templates/server/clovie.config.js +7 -98
- package/templates/server/views/about.html +2 -12
- package/templates/server/views/index.html +2 -2
- package/templates/static/AI_DEVELOPMENT_GUIDE.md +20 -77
- package/dist/ExpressAdapter-BsYIpF7A.js +0 -109
- package/dist/ExpressAdapter-BsYIpF7A.js.map +0 -1
- package/dist/LiveReload-ClrG9hAe.js +0 -101
- package/dist/LiveReload-ClrG9hAe.js.map +0 -1
- package/dist/Server-GcEIC8Hj.js +0 -554
- package/dist/Server-GcEIC8Hj.js.map +0 -1
- package/dist/cjs/ExpressAdapter--oUcRq-k.cjs +0 -81
- package/dist/cjs/ExpressAdapter--oUcRq-k.cjs.map +0 -1
- package/dist/cjs/LiveReload-BvFz-bG3.cjs +0 -76
- package/dist/cjs/LiveReload-BvFz-bG3.cjs.map +0 -1
- package/dist/cjs/Server-DkYaMJak.cjs +0 -554
- package/dist/cjs/Server-DkYaMJak.cjs.map +0 -1
- package/dist/cjs/createClovie-CJ1h3Pfo.cjs +0 -6430
- package/dist/cjs/createClovie-CJ1h3Pfo.cjs.map +0 -1
- package/dist/cjs/index-DU3_mYKB.cjs +0 -461
- package/dist/cjs/index-DU3_mYKB.cjs.map +0 -1
- package/dist/cjs/index.cjs +0 -9
- package/dist/cjs/index.cjs.map +0 -1
- package/dist/createClovie-Dh7RbB5L.js +0 -6448
- package/dist/createClovie-Dh7RbB5L.js.map +0 -1
- package/dist/index-D8ThZ_1b.js +0 -484
- package/dist/index-D8ThZ_1b.js.map +0 -1
- package/dist/index.d.ts +0 -229
- package/dist/index.js +0 -32
- package/dist/index.js.map +0 -1
- package/scripts/build-types.js +0 -247
- package/scripts/killPort.js +0 -138
- 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 **@
|
|
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 `@
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|