clovie 0.1.0
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/LICENSE +21 -0
- package/README.md +441 -0
- package/bin/cli.js +126 -0
- package/config/default.config.js +31 -0
- package/lib/core/bundler.js +31 -0
- package/lib/core/cache.js +69 -0
- package/lib/core/discover.js +74 -0
- package/lib/core/getAssets.js +38 -0
- package/lib/core/getData.js +19 -0
- package/lib/core/getStyles.js +16 -0
- package/lib/core/getViews.js +187 -0
- package/lib/core/render.js +17 -0
- package/lib/core/server.js +104 -0
- package/lib/core/watcher.js +242 -0
- package/lib/core/write.js +24 -0
- package/lib/main.js +270 -0
- package/lib/utils/clean.js +21 -0
- package/lib/utils/create.js +31 -0
- package/lib/utils/readFilesToMap.js +24 -0
- package/package.json +73 -0
- package/templates/default/README.md +29 -0
- package/templates/default/app.config.js +27 -0
- package/templates/default/package.json +12 -0
- package/templates/default/scripts/main.js +1 -0
- package/templates/default/styles/main.scss +11 -0
- package/templates/default/views/index.html +12 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function discoverProjectStructure(config) {
|
|
5
|
+
const cwd = process.cwd();
|
|
6
|
+
const discovered = { ...config };
|
|
7
|
+
|
|
8
|
+
// Auto-detect views directory
|
|
9
|
+
if (!discovered.views) {
|
|
10
|
+
const viewDirs = ['views', 'templates', 'pages', 'src/views', 'src/templates'];
|
|
11
|
+
for (const dir of viewDirs) {
|
|
12
|
+
if (fs.existsSync(path.join(cwd, dir))) {
|
|
13
|
+
discovered.views = path.join('./', dir);
|
|
14
|
+
console.log(`🔍 Auto-detected views directory: ${dir}`);
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Auto-detect scripts directory
|
|
21
|
+
if (!discovered.scripts) {
|
|
22
|
+
const scriptDirs = ['scripts', 'js', 'src/scripts', 'src/js'];
|
|
23
|
+
for (const dir of scriptDirs) {
|
|
24
|
+
if (fs.existsSync(path.join(cwd, dir))) {
|
|
25
|
+
const mainFiles = ['main.js', 'index.js', 'app.js'];
|
|
26
|
+
for (const file of mainFiles) {
|
|
27
|
+
if (fs.existsSync(path.join(cwd, dir, file))) {
|
|
28
|
+
discovered.scripts = path.join('./', dir, file);
|
|
29
|
+
console.log(`🔍 Auto-detected scripts entry: ${dir}/${file}`);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (discovered.scripts) break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Auto-detect styles directory
|
|
39
|
+
if (!discovered.styles) {
|
|
40
|
+
const styleDirs = ['styles', 'css', 'scss', 'src/styles', 'src/css'];
|
|
41
|
+
for (const dir of styleDirs) {
|
|
42
|
+
if (fs.existsSync(path.join(cwd, dir))) {
|
|
43
|
+
const mainFiles = ['main.scss', 'main.css', 'style.scss', 'style.css'];
|
|
44
|
+
for (const file of mainFiles) {
|
|
45
|
+
if (fs.existsSync(path.join(cwd, dir, file))) {
|
|
46
|
+
discovered.styles = path.join('./', dir, file);
|
|
47
|
+
console.log(`🔍 Auto-detected styles entry: ${dir}/${file}`);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (discovered.styles) break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Auto-detect assets directory
|
|
57
|
+
if (!discovered.assets) {
|
|
58
|
+
const assetDirs = ['assets', 'public', 'static', 'src/assets', 'src/public'];
|
|
59
|
+
for (const dir of assetDirs) {
|
|
60
|
+
if (fs.existsSync(path.join(cwd, dir))) {
|
|
61
|
+
discovered.assets = path.join('./', dir);
|
|
62
|
+
console.log(`🔍 Auto-detected assets directory: ${dir}`);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Validate required directories
|
|
69
|
+
if (!discovered.views) {
|
|
70
|
+
console.warn('⚠️ No views directory found. Create a views/ folder with your HTML templates.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return discovered;
|
|
74
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { readFilesToMap } from '../utils/readFilesToMap.js';
|
|
4
|
+
|
|
5
|
+
export default function (src) {
|
|
6
|
+
try {
|
|
7
|
+
if (!src || !fs.existsSync(src)) {
|
|
8
|
+
console.warn(`Assets directory does not exist: ${src}`);
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let fileNames = getFileNames(src);
|
|
13
|
+
return fileNames ? readFilesToMap(fileNames, src) : {};
|
|
14
|
+
} catch (err) {
|
|
15
|
+
console.error(`Error processing assets from ${src}:`, err);
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getFileNames (PATH, files = fs.readdirSync(PATH, { withFileTypes: true }), accumulator = []) {
|
|
21
|
+
if (files.length === 0) return accumulator;
|
|
22
|
+
|
|
23
|
+
let value = files.shift();
|
|
24
|
+
if (!value) return accumulator;
|
|
25
|
+
|
|
26
|
+
let name = path.join(PATH, value.name);
|
|
27
|
+
let res = value.isDirectory() ? getFileNames(name) : name;
|
|
28
|
+
|
|
29
|
+
if (Array.isArray(res)) {
|
|
30
|
+
accumulator = accumulator.concat(res)
|
|
31
|
+
} else {
|
|
32
|
+
accumulator.push(res)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return files.length ? getFileNames(PATH, files, accumulator) : accumulator;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type from 'type-detect';
|
|
2
|
+
|
|
3
|
+
export default function getData (data) {
|
|
4
|
+
return new Promise ((resolve, reject) => {
|
|
5
|
+
switch (type(data)) {
|
|
6
|
+
case 'Object':
|
|
7
|
+
resolve(data);
|
|
8
|
+
break;
|
|
9
|
+
|
|
10
|
+
case 'function':
|
|
11
|
+
resolve(data())
|
|
12
|
+
break;
|
|
13
|
+
|
|
14
|
+
case 'Promise':
|
|
15
|
+
data.then(resolve)
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as sass from 'sass';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export default function (file) {
|
|
5
|
+
let pathObj = path.parse(file);
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
let res = sass.compile(file);
|
|
9
|
+
return {[`${pathObj.name}.css`]: res.css};
|
|
10
|
+
} catch (err) {
|
|
11
|
+
console.log('Sass Error in file: ' + err.file);
|
|
12
|
+
console.log('On line: ' + err.line);
|
|
13
|
+
console.log(err.formatted);
|
|
14
|
+
return {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { readFilesToMap } from '../utils/readFilesToMap.js';
|
|
4
|
+
|
|
5
|
+
export default function (src, models = {}, data) {
|
|
6
|
+
try {
|
|
7
|
+
if (!src || !fs.existsSync(src)) {
|
|
8
|
+
console.warn(`Views directory does not exist: ${src}`);
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let fileNames = getFileNames(src);
|
|
13
|
+
let templates = fileNames ? readFilesToMap(fileNames, src) : null;
|
|
14
|
+
let pages = templates ? getPages(templates, data) : {};
|
|
15
|
+
|
|
16
|
+
// Process models if any exist
|
|
17
|
+
if (Object.keys(models).length > 0) {
|
|
18
|
+
return processModels(templates, data, models, pages);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return pages;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error(`Error processing views from ${src}:`, err);
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getFileNames (PATH, files = fs.readdirSync(PATH, { withFileTypes: true }), accumulator = [], depth = 0) {
|
|
29
|
+
// Prevent infinite recursion and stack overflow
|
|
30
|
+
const MAX_DEPTH = 50;
|
|
31
|
+
if (depth > MAX_DEPTH) {
|
|
32
|
+
console.warn(`Maximum directory depth exceeded at: ${PATH}`);
|
|
33
|
+
return accumulator;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!files || files.length === 0) return accumulator;
|
|
37
|
+
|
|
38
|
+
let value = files.shift();
|
|
39
|
+
if (!value) return accumulator;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (value && !value.name.startsWith('.')) {
|
|
43
|
+
let name = path.join(PATH, value.name);
|
|
44
|
+
let res = value.isDirectory() ? getFileNames(name, [], [], depth + 1) : name;
|
|
45
|
+
|
|
46
|
+
if (Array.isArray(res)) {
|
|
47
|
+
accumulator = accumulator.concat(res)
|
|
48
|
+
} else {
|
|
49
|
+
accumulator.push(res)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(`Error processing file/directory: ${PATH}/${value?.name}:`, err);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return files.length ? getFileNames(PATH, files, accumulator, depth) : accumulator;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
function getPages (templates, data, keys = Object.keys(templates), accumulator = {}) {
|
|
62
|
+
if (!keys || keys.length === 0) return accumulator;
|
|
63
|
+
|
|
64
|
+
let key = keys.shift();
|
|
65
|
+
if (!key) return accumulator;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
if (!path.parse(key).name.startsWith('_')) {
|
|
69
|
+
let template = templates[key];
|
|
70
|
+
accumulator[key] = {
|
|
71
|
+
template,
|
|
72
|
+
data
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(`Error processing page: ${key}:`, err);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return keys.length ? getPages(templates, data, keys, accumulator) : accumulator;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function processModels(templates, data, models, existingPages = {}) {
|
|
83
|
+
const results = { ...existingPages };
|
|
84
|
+
|
|
85
|
+
for (const [modelName, config] of Object.entries(models)) {
|
|
86
|
+
try {
|
|
87
|
+
// Get the data array for this model
|
|
88
|
+
const dataKey = config.ref || modelName;
|
|
89
|
+
const items = data[dataKey];
|
|
90
|
+
|
|
91
|
+
if (!items) {
|
|
92
|
+
console.warn(`⚠️ No data found for model '${modelName}'. Expected '${dataKey}' in your data.`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!Array.isArray(items)) {
|
|
97
|
+
console.error(`❌ Data for model '${modelName}' must be an array, got: ${typeof items}`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Process each item
|
|
102
|
+
for (let i = 0; i < items.length; i++) {
|
|
103
|
+
const item = items[i];
|
|
104
|
+
const page = processModelItem(item, i, config, templates, data);
|
|
105
|
+
|
|
106
|
+
if (page) {
|
|
107
|
+
results[page.filename] = page;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`✅ Generated ${items.length} pages for model '${modelName}'`);
|
|
112
|
+
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(`❌ Error processing model '${modelName}':`, err);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function processModelItem(item, index, config, templates, globalData) {
|
|
122
|
+
try {
|
|
123
|
+
// Get template
|
|
124
|
+
const templateName = config.template;
|
|
125
|
+
if (!templateName) {
|
|
126
|
+
console.error(`❌ Model missing 'template' property`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const templatePath = path.normalize(`/${templateName}`);
|
|
131
|
+
const template = templates[templatePath];
|
|
132
|
+
|
|
133
|
+
if (!template) {
|
|
134
|
+
console.error(`❌ Template '${templateName}' not found. Available templates: ${Object.keys(templates).join(', ')}`);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Transform data if function provided
|
|
139
|
+
let processedData = item;
|
|
140
|
+
if (typeof config.transform === 'function') {
|
|
141
|
+
try {
|
|
142
|
+
processedData = config.transform(item, index);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error(`❌ Error in transform function for item ${index}:`, err);
|
|
145
|
+
processedData = item;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Generate filename
|
|
150
|
+
let filename = `${index}.html`;
|
|
151
|
+
if (typeof config.output === 'function') {
|
|
152
|
+
try {
|
|
153
|
+
filename = config.output(item, index);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error(`❌ Error in output function for item ${index}:`, err);
|
|
156
|
+
filename = `${index}.html`;
|
|
157
|
+
}
|
|
158
|
+
} else if (typeof config.output === 'string') {
|
|
159
|
+
// Simple string template replacement
|
|
160
|
+
filename = config.output
|
|
161
|
+
.replace('{index}', index)
|
|
162
|
+
.replace('{slug}', item.slug || item.id || index)
|
|
163
|
+
.replace('{title}', item.title || item.name || index);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Ensure .html extension
|
|
167
|
+
if (!filename.endsWith('.html')) {
|
|
168
|
+
filename += '.html';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
template,
|
|
173
|
+
data: {
|
|
174
|
+
local: processedData,
|
|
175
|
+
...globalData,
|
|
176
|
+
index,
|
|
177
|
+
isFirst: index === 0,
|
|
178
|
+
isLast: index === (globalData[config.ref || 'items']?.length - 1)
|
|
179
|
+
},
|
|
180
|
+
filename
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error(`❌ Error processing model item ${index}:`, err);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type from 'type-detect';
|
|
2
|
+
|
|
3
|
+
const render = async (views, compiler, fileNames = Object.keys(views), accumulator = {}) => {
|
|
4
|
+
if (!fileNames || fileNames.length === 0) return accumulator;
|
|
5
|
+
|
|
6
|
+
for (const fileName of fileNames) {
|
|
7
|
+
const view = views[fileName];
|
|
8
|
+
if (!view || !view.template) continue;
|
|
9
|
+
|
|
10
|
+
const res = compiler(view.template, {...view.data, fileName, fileNames});
|
|
11
|
+
accumulator[fileName] = type(res) === 'Promise' ? await res : res;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return accumulator;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default render;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createServer } from 'http';
|
|
4
|
+
import { Server } from 'socket.io';
|
|
5
|
+
|
|
6
|
+
export class DevServer {
|
|
7
|
+
constructor(clovieInstance, port = 3000) {
|
|
8
|
+
this.clovie = clovieInstance;
|
|
9
|
+
this.port = port;
|
|
10
|
+
this.app = express();
|
|
11
|
+
this.server = createServer(this.app);
|
|
12
|
+
this.io = new Server(this.server);
|
|
13
|
+
this.isRunning = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
start() {
|
|
17
|
+
if (this.isRunning) return;
|
|
18
|
+
|
|
19
|
+
// Serve static files from output directory
|
|
20
|
+
this.app.use(express.static(this.clovie.config.outputDir));
|
|
21
|
+
|
|
22
|
+
// Explicitly handle root path to serve index.html
|
|
23
|
+
this.app.get('/', (req, res) => {
|
|
24
|
+
res.sendFile(path.join(this.clovie.config.outputDir, 'index.html'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Handle paths without trailing slash by redirecting
|
|
28
|
+
this.app.get('*', (req, res, next) => {
|
|
29
|
+
if (!req.path.endsWith('/') && !req.path.includes('.')) {
|
|
30
|
+
// Redirect paths without trailing slash to include it
|
|
31
|
+
res.redirect(req.path + '/');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
next();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Serve source files for debugging
|
|
38
|
+
if (this.clovie.config.views) {
|
|
39
|
+
this.app.use('/source/views', express.static(this.clovie.config.views));
|
|
40
|
+
}
|
|
41
|
+
if (this.clovie.config.scriptsDir) {
|
|
42
|
+
this.app.use('/source/scripts', express.static(this.clovie.config.scriptsDir));
|
|
43
|
+
}
|
|
44
|
+
if (this.clovie.config.stylesDir) {
|
|
45
|
+
this.app.use('/source/styles', express.static(this.clovie.config.stylesDir));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// WebSocket for live reload
|
|
49
|
+
this.io.on('connection', (socket) => {
|
|
50
|
+
console.log('🔌 Client connected');
|
|
51
|
+
|
|
52
|
+
socket.on('disconnect', () => {
|
|
53
|
+
console.log('🔌 Client disconnected');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Add cache-busting headers for development
|
|
58
|
+
this.app.use((req, res, next) => {
|
|
59
|
+
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
60
|
+
res.set('Pragma', 'no-cache');
|
|
61
|
+
res.set('Expires', '0');
|
|
62
|
+
next();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Start server
|
|
66
|
+
this.server.listen(this.port, () => {
|
|
67
|
+
this.isRunning = true;
|
|
68
|
+
console.log(`🌐 Development server running at http://localhost:${this.port}`);
|
|
69
|
+
console.log(`📁 Serving files from: ${this.clovie.config.outputDir}`);
|
|
70
|
+
console.log(`🔌 Live reload enabled`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Handle server shutdown
|
|
74
|
+
process.on('SIGINT', () => {
|
|
75
|
+
this.stop();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
stop() {
|
|
80
|
+
if (!this.isRunning) return;
|
|
81
|
+
|
|
82
|
+
this.server.close(() => {
|
|
83
|
+
this.isRunning = false;
|
|
84
|
+
console.log('🛑 Development server stopped');
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Notify clients to reload
|
|
89
|
+
notifyReload() {
|
|
90
|
+
if (this.io) {
|
|
91
|
+
this.io.emit('reload');
|
|
92
|
+
console.log('🔄 Live reload triggered');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Get server info
|
|
97
|
+
getInfo() {
|
|
98
|
+
return {
|
|
99
|
+
port: this.port,
|
|
100
|
+
isRunning: this.isRunning,
|
|
101
|
+
outputDir: this.clovie.config.outputDir
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { BuildCache } from './cache.js';
|
|
5
|
+
|
|
6
|
+
export class SmartWatcher {
|
|
7
|
+
constructor(clovieInstance) {
|
|
8
|
+
this.clovie = clovieInstance;
|
|
9
|
+
this.watcher = null;
|
|
10
|
+
this.cache = new BuildCache(clovieInstance.config.outputDir);
|
|
11
|
+
this.debounceTimer = null;
|
|
12
|
+
this.isWatching = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
start() {
|
|
16
|
+
if (this.isWatching) return;
|
|
17
|
+
|
|
18
|
+
console.log('👀 Starting smart file watcher...');
|
|
19
|
+
this.isWatching = true;
|
|
20
|
+
|
|
21
|
+
// Watch views directory
|
|
22
|
+
if (this.clovie.config.views) {
|
|
23
|
+
this.watcher = chokidar.watch(this.clovie.config.views, {
|
|
24
|
+
ignored: /(^|[\/\\])\../, // Ignore hidden files
|
|
25
|
+
persistent: true
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.watcher.on('change', (filePath) => this.handleViewChange(filePath));
|
|
29
|
+
this.watcher.on('add', (filePath) => this.handleViewChange(filePath));
|
|
30
|
+
this.watcher.on('unlink', (filePath) => this.handleViewChange(filePath));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Watch scripts directory
|
|
34
|
+
if (this.clovie.config.scriptsDir) {
|
|
35
|
+
const scriptsWatcher = chokidar.watch(this.clovie.config.scriptsDir, {
|
|
36
|
+
ignored: /(^|[\/\\])\../,
|
|
37
|
+
persistent: true
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
scriptsWatcher.on('change', (filePath) => this.handleScriptChange(filePath));
|
|
41
|
+
scriptsWatcher.on('add', (filePath) => this.handleScriptChange(filePath));
|
|
42
|
+
scriptsWatcher.on('unlink', (filePath) => this.handleScriptChange(filePath));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Watch styles directory
|
|
46
|
+
if (this.clovie.config.stylesDir) {
|
|
47
|
+
const stylesWatcher = chokidar.watch(this.clovie.config.stylesDir, {
|
|
48
|
+
ignored: /(^|[\/\\])\../,
|
|
49
|
+
persistent: true
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
stylesWatcher.on('change', (filePath) => this.handleStyleChange(filePath));
|
|
53
|
+
stylesWatcher.on('add', (filePath) => this.handleStyleChange(filePath));
|
|
54
|
+
stylesWatcher.on('unlink', (filePath) => this.handleStyleChange(filePath));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Watch assets directory
|
|
58
|
+
if (this.clovie.config.assets) {
|
|
59
|
+
const assetsWatcher = chokidar.watch(this.clovie.config.assets, {
|
|
60
|
+
ignored: /(^|[\/\\])\../,
|
|
61
|
+
persistent: true
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
assetsWatcher.on('change', (filePath) => this.handleAssetChange(filePath));
|
|
65
|
+
assetsWatcher.on('add', (filePath) => this.handleAssetChange(filePath));
|
|
66
|
+
assetsWatcher.on('unlink', (filePath) => this.handleAssetChange(filePath));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log('✅ Smart watcher started');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
stop() {
|
|
73
|
+
if (this.watcher) {
|
|
74
|
+
this.watcher.close();
|
|
75
|
+
this.watcher = null;
|
|
76
|
+
}
|
|
77
|
+
this.isWatching = false;
|
|
78
|
+
console.log('🛑 Smart watcher stopped');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Debounced rebuild to avoid multiple rapid rebuilds
|
|
82
|
+
scheduleRebuild(type, filePath) {
|
|
83
|
+
clearTimeout(this.debounceTimer);
|
|
84
|
+
|
|
85
|
+
this.debounceTimer = setTimeout(() => {
|
|
86
|
+
this.rebuild(type, filePath);
|
|
87
|
+
}, 100); // 100ms debounce
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async rebuild(type, filePath) {
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
console.log(`🔄 Rebuilding due to ${type} change: ${path.basename(filePath)}`);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Only rebuild what's necessary based on what changed
|
|
96
|
+
switch (type) {
|
|
97
|
+
case 'view':
|
|
98
|
+
await this.rebuildViews();
|
|
99
|
+
break;
|
|
100
|
+
case 'script':
|
|
101
|
+
await this.rebuildScripts();
|
|
102
|
+
break;
|
|
103
|
+
case 'style':
|
|
104
|
+
await this.rebuildStyles();
|
|
105
|
+
break;
|
|
106
|
+
case 'asset':
|
|
107
|
+
await this.rebuildAssets();
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.cache.markBuilt();
|
|
112
|
+
const buildTime = Date.now() - startTime;
|
|
113
|
+
console.log(`✅ Incremental rebuild completed in ${buildTime}ms`);
|
|
114
|
+
|
|
115
|
+
// Trigger live reload if callback is set
|
|
116
|
+
if (this.onRebuild) {
|
|
117
|
+
this.onRebuild();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(`❌ Incremental rebuild failed:`, err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async rebuildViews() {
|
|
126
|
+
// For now, just trigger a full rebuild of views since incremental is complex
|
|
127
|
+
console.log(' Triggering full view rebuild...');
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Re-process all views
|
|
131
|
+
const getViews = await import('./getViews.js');
|
|
132
|
+
this.clovie.views = getViews.default(this.clovie.config.views, this.clovie.config.models, this.clovie.data);
|
|
133
|
+
|
|
134
|
+
// Re-render all views using the render function
|
|
135
|
+
const render = await import('./render.js');
|
|
136
|
+
this.clovie.rendered = await render.default(this.clovie.views, this.clovie.config.compiler, Object.keys(this.clovie.views));
|
|
137
|
+
|
|
138
|
+
// Write the updated views
|
|
139
|
+
const write = await import('./write.js');
|
|
140
|
+
write.default(this.clovie.rendered, this.clovie.config.outputDir);
|
|
141
|
+
|
|
142
|
+
console.log(' Views rebuilt successfully');
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error(' View rebuild failed:', err);
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async rebuildScripts() {
|
|
150
|
+
if (!this.clovie.config.scripts) return;
|
|
151
|
+
console.log(' Rebuilding scripts...');
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const bundler = await import('./bundler.js');
|
|
155
|
+
this.clovie.scripts = await bundler.default(this.clovie.config.scripts);
|
|
156
|
+
|
|
157
|
+
const write = await import('./write.js');
|
|
158
|
+
write.default(this.clovie.scripts, this.clovie.config.outputDir);
|
|
159
|
+
|
|
160
|
+
console.log(' Scripts rebuilt successfully');
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.error(' Script rebuild failed:', err);
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async rebuildStyles() {
|
|
168
|
+
if (!this.clovie.config.styles) return;
|
|
169
|
+
console.log(' Rebuilding styles...');
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const getStyles = await import('./getStyles.js');
|
|
173
|
+
this.clovie.styles = getStyles.default(this.clovie.config.styles);
|
|
174
|
+
|
|
175
|
+
const write = await import('./write.js');
|
|
176
|
+
write.default(this.clovie.styles, this.clovie.config.outputDir);
|
|
177
|
+
|
|
178
|
+
console.log(' Styles rebuilt successfully');
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.error(' Style rebuild failed:', err);
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async rebuildAssets() {
|
|
186
|
+
if (!this.clovie.config.assets) return;
|
|
187
|
+
console.log(' Rebuilding assets...');
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const getAssets = await import('./getAssets.js');
|
|
191
|
+
this.clovie.assets = getAssets.default(this.clovie.config.assets);
|
|
192
|
+
|
|
193
|
+
const write = await import('./write.js');
|
|
194
|
+
write.default(this.clovie.assets, this.clovie.config.outputDir);
|
|
195
|
+
|
|
196
|
+
console.log(' Assets rebuilt successfully');
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error(' Asset rebuild failed:', err);
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
getViewFiles(viewsDir) {
|
|
204
|
+
// Get all view files recursively
|
|
205
|
+
const files = [];
|
|
206
|
+
const scanDir = (dir) => {
|
|
207
|
+
try {
|
|
208
|
+
const items = fs.readdirSync(dir);
|
|
209
|
+
for (const item of items) {
|
|
210
|
+
const fullPath = path.join(dir, item);
|
|
211
|
+
const stat = fs.statSync(fullPath);
|
|
212
|
+
if (stat.isDirectory()) {
|
|
213
|
+
scanDir(fullPath);
|
|
214
|
+
} else if (path.extname(item) === '.html') {
|
|
215
|
+
files.push(fullPath);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (err) {
|
|
219
|
+
// Ignore errors
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
scanDir(viewsDir);
|
|
224
|
+
return files;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
handleViewChange(filePath) {
|
|
228
|
+
this.scheduleRebuild('view', filePath);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
handleScriptChange(filePath) {
|
|
232
|
+
this.scheduleRebuild('script', filePath);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
handleStyleChange(filePath) {
|
|
236
|
+
this.scheduleRebuild('style', filePath);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
handleAssetChange(filePath) {
|
|
240
|
+
this.scheduleRebuild('asset', filePath);
|
|
241
|
+
}
|
|
242
|
+
}
|