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.
@@ -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
+ }