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,24 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+
4
+ export default function write (pages, outputDir, keys = Object.keys(pages), accumulator = {}) {
5
+ const key = keys.shift();
6
+ const value = pages[key];
7
+
8
+ if (accumulator[key] != value) {
9
+ const dest = path.join(outputDir, key);
10
+ const dir = path.dirname(dest);
11
+
12
+ if (!fs.existsSync(dir)) {
13
+ fs.mkdirSync(dir, { recursive: true })
14
+ }
15
+
16
+ fs.writeFileSync(dest, value, err => {
17
+ if (err) {
18
+ console.log(err)
19
+ }
20
+ });
21
+ }
22
+
23
+ return keys.length ? write(pages, outputDir, keys) : 'success';
24
+ }
package/lib/main.js ADDED
@@ -0,0 +1,270 @@
1
+ import chokidar from 'chokidar';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ import bundler from './core/bundler.js';
10
+ import getAssets from './core/getAssets.js';
11
+ import getData from './core/getData.js';
12
+ import getStyles from './core/getStyles.js';
13
+ import getViews from './core/getViews.js';
14
+ import render from './core/render.js';
15
+ import write from './core/write.js';
16
+ import clean from './utils/clean.js';
17
+ import defaultConfig from '../config/default.config.js';
18
+ import { discoverProjectStructure } from './core/discover.js';
19
+ import { SmartWatcher } from './core/watcher.js';
20
+ import { DevServer } from './core/server.js';
21
+
22
+ export default class Clovie {
23
+ constructor (config) {
24
+ // Merge with defaults and auto-discover project structure
25
+ this.config = discoverProjectStructure(Object.assign(defaultConfig, config));
26
+
27
+ // Set derived paths
28
+ if (this.config.styles) {
29
+ this.config.stylesDir = path.resolve(path.dirname(this.config.styles));
30
+ }
31
+ if (this.config.scripts) {
32
+ this.config.scriptsDir = path.resolve(path.dirname(this.config.scripts));
33
+ }
34
+
35
+ this.errorCb = null;
36
+
37
+ // Initialize smart watcher
38
+ this.watcher = new SmartWatcher(this);
39
+
40
+ // Log configuration summary
41
+ console.log('šŸ“ Clovie Configuration:');
42
+ console.log(` Views: ${this.config.views || 'Not found'}`);
43
+ console.log(` Scripts: ${this.config.scripts || 'Not found'}`);
44
+ console.log(` Styles: ${this.config.styles || 'Not found'}`);
45
+ console.log(` Assets: ${this.config.assets || 'Not found'}`);
46
+ console.log(` Output: ${this.config.outputDir}`);
47
+ }
48
+
49
+ async startWatch() {
50
+ try {
51
+ console.log('šŸš€ Starting development server with smart watching...');
52
+
53
+ // Do initial build
54
+ console.log('šŸ“¦ Building initial site...');
55
+ await this.build();
56
+
57
+ // Start development server
58
+ console.log('🌐 Starting development server...');
59
+ this.devServer = new DevServer(this, this.config.port || 3000);
60
+ this.devServer.start();
61
+
62
+ // Start smart watcher
63
+ console.log('šŸ‘€ Starting smart watcher...');
64
+ this.watcher.start();
65
+
66
+ // Connect watcher to dev server for live reload
67
+ this.watcher.onRebuild = () => {
68
+ if (this.devServer) {
69
+ this.devServer.notifyReload();
70
+ }
71
+ };
72
+
73
+ // Keep process alive
74
+ process.on('SIGINT', () => {
75
+ console.log('\nšŸ›‘ Shutting down...');
76
+ this.watcher.stop();
77
+ if (this.devServer) {
78
+ this.devServer.stop();
79
+ }
80
+ process.exit(0);
81
+ });
82
+
83
+ console.log('āœ… Development server running. Press Ctrl+C to stop.');
84
+ } catch (err) {
85
+ console.error('āŒ Failed to start watch mode:', err);
86
+ throw err;
87
+ }
88
+ }
89
+
90
+ async build () {
91
+ const startTime = Date.now();
92
+
93
+ try {
94
+ console.log('šŸš€ Starting build...');
95
+
96
+ // Clean output directory
97
+ console.log('🧹 Cleaning output directory...');
98
+ clean(this.config.outputDir);
99
+
100
+ // Load data
101
+ console.log('šŸ“Š Loading data...');
102
+ this.data = this.config.data ? await getData(this.config.data) : {};
103
+ console.log(` Loaded ${Object.keys(this.data).length} data sources`);
104
+
105
+ // Process views
106
+ console.log('šŸ“ Processing views...');
107
+ this.views = getViews(this.config.views, this.config.models, this.data);
108
+
109
+ // Convert to the format expected by render
110
+ this.processedViews = {};
111
+ for (const [key, value] of Object.entries(this.views)) {
112
+ if (value && value.template) {
113
+ // Use filename from model processing, or generate default
114
+ const fileName = value.filename || key.replace(/\.[^/.]+$/, '.html');
115
+ this.processedViews[fileName] = value;
116
+ }
117
+ }
118
+ console.log(` Processed ${Object.keys(this.processedViews).length} views`);
119
+
120
+ // Render templates
121
+ console.log('šŸŽØ Rendering templates...');
122
+ this.rendered = await render(this.processedViews, this.config.compiler, Object.keys(this.processedViews));
123
+ console.log(` Rendered ${Object.keys(this.rendered).length} templates`);
124
+
125
+ // Process assets in parallel for speed
126
+ const assetPromises = [];
127
+
128
+ if (this.config.scripts) {
129
+ console.log('⚔ Bundling scripts...');
130
+ assetPromises.push(
131
+ bundler(this.config.scripts).then(scripts => {
132
+ this.scripts = scripts;
133
+ console.log(` Bundled ${Object.keys(scripts).length} script files`);
134
+ })
135
+ );
136
+ }
137
+
138
+ if (this.config.styles) {
139
+ console.log('šŸŽØ Compiling styles...');
140
+ assetPromises.push(
141
+ Promise.resolve().then(() => {
142
+ this.styles = getStyles(this.config.styles);
143
+ console.log(` Compiled ${Object.keys(this.styles).length} style files`);
144
+ })
145
+ );
146
+ }
147
+
148
+ if (this.config.assets) {
149
+ console.log('šŸ“¦ Processing assets...');
150
+ assetPromises.push(
151
+ Promise.resolve().then(() => {
152
+ this.assets = getAssets(this.config.assets);
153
+ console.log(` Processed ${Object.keys(this.assets).length} asset files`);
154
+ })
155
+ );
156
+ }
157
+
158
+ // Wait for all assets to complete
159
+ if (assetPromises.length > 0) {
160
+ await Promise.all(assetPromises);
161
+ }
162
+
163
+ // Write output
164
+ console.log('šŸ’¾ Writing files...');
165
+ this.cache = write(Object.assign(this.rendered, this.scripts, this.styles, this.assets), this.config.outputDir);
166
+
167
+ const buildTime = Date.now() - startTime;
168
+ console.log(`āœ… Build completed in ${buildTime}ms`);
169
+
170
+ return this.cache;
171
+ } catch (err) {
172
+ const buildTime = Date.now() - startTime;
173
+ console.error(`āŒ Build failed after ${buildTime}ms:`, err);
174
+
175
+ // Provide better error context
176
+ if (err.code === 'ENOENT') {
177
+ console.error('šŸ’” Tip: Check that all referenced files and directories exist');
178
+ } else if (err.message?.includes('template')) {
179
+ console.error('šŸ’” Tip: Verify your template syntax and data structure');
180
+ }
181
+
182
+ if (this.errorCb) {
183
+ this.errorCb(err);
184
+ } else {
185
+ throw err;
186
+ }
187
+ }
188
+ }
189
+
190
+ watch () {
191
+ try {
192
+ bs.init({
193
+ watch: true,
194
+ server: {
195
+ baseDir: this.config.outputDir,
196
+ serveStaticOptions: {
197
+ extensions: ["html", 'htm']
198
+ }
199
+ }
200
+ });
201
+
202
+ let options = {
203
+ ignored: /(^|[\/\\])\../,
204
+ persistent: true,
205
+ ignoreInitial: true
206
+ };
207
+
208
+ chokidar.watch(this.config.views, options).on('all', () => {
209
+ process.nextTick(async () => {
210
+ try {
211
+ console.log('Recompile Templates');
212
+ this.views = getViews(this.config.views, this.config.models, this.data);
213
+ this.urls = Object.keys(this.views);
214
+ this.rendered = await render(this.views, this.config.compiler, this.urls);
215
+ this.cache = write(this.rendered, this.config.outputDir, Object.keys(this.rendered), this.cache);
216
+ console.log('Templates Done');
217
+ } catch (err) {
218
+ console.error('Template recompilation failed:', err);
219
+ }
220
+ })
221
+ });
222
+
223
+ chokidar.watch(this.config.scriptsDir, options).on('all', () => {
224
+ process.nextTick(async () => {
225
+ try {
226
+ console.log('Recompile Scripts');
227
+ this.scripts = await bundler(this.config.scripts);
228
+ this.cache = write(this.scripts, this.config.outputDir, Object.keys(this.scripts), this.cache);
229
+ console.log('Scripts Done');
230
+ } catch (err) {
231
+ console.error('Script recompilation failed:', err);
232
+ }
233
+ })
234
+ });
235
+
236
+ chokidar.watch(this.config.stylesDir, options).on('all', () => {
237
+ process.nextTick(() => {
238
+ try {
239
+ console.log('Updates styles');
240
+ this.styles = getStyles(this.config.styles);
241
+ this.cache = write(this.styles, this.config.outputDir, Object.keys(this.styles), this.cache);
242
+ console.log('Styles Done');
243
+ } catch (err) {
244
+ console.error('Style update failed:', err);
245
+ }
246
+ });
247
+ });
248
+
249
+ chokidar.watch(this.config.assets, options).on('all', () => {
250
+ process.nextTick(() => {
251
+ try {
252
+ console.log('Updating assets');
253
+ this.assets = getAssets(this.config.assets);
254
+ this.cache = write(this.assets, this.config.outputDir, Object.keys(this.assets), this.cache);
255
+ console.log('Assets updated');
256
+ } catch (err) {
257
+ console.error('Asset update failed:', err);
258
+ }
259
+ });
260
+ });
261
+ } catch (err) {
262
+ this.error(err)
263
+ }
264
+ }
265
+
266
+ error(cb) {
267
+ this.errorCb = cb
268
+ }
269
+ }
270
+
@@ -0,0 +1,21 @@
1
+ import fs from 'fs';
2
+
3
+ export default function rmDir (dirPath) {
4
+ try {
5
+ var files = fs.readdirSync(dirPath)
6
+ }
7
+ catch(e) {
8
+ return;
9
+ }
10
+
11
+ if (files.length > 0)
12
+ for (var i = 0; i < files.length; i++) {
13
+ var filePath = dirPath + '/' + files[i];
14
+ if (fs.statSync(filePath).isFile())
15
+ fs.unlinkSync(filePath);
16
+ else
17
+ rmDir(filePath);
18
+ }
19
+ fs.rmdirSync(dirPath);
20
+ console.log(dirPath + ' cleaned')
21
+ };
@@ -0,0 +1,31 @@
1
+ import copydir from 'copy-dir';
2
+ import path from 'path';
3
+ import fs from "fs";
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname } from 'path';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ const boilerplate = path.resolve(__dirname, '../boilerplate');
11
+
12
+ export default (dest) => new Promise((res, rej) => {
13
+ try {
14
+ if (fs.existsSync(dest)) {
15
+ throw `Directory '${dest}'exists`
16
+ } else {
17
+ dest = path.resolve(dest)
18
+ copydir.sync(boilerplate, dest, {
19
+ filter: function(stat, _, filename){
20
+ if (stat === 'directory' && filename === 'dist') {
21
+ return false;
22
+ }
23
+ return true; // remind to return a true value when file check passed.
24
+ }
25
+ });
26
+ res(`New Attics project created at ${dest}`)
27
+ }
28
+ } catch(e) {
29
+ rej(e)
30
+ }
31
+ });
@@ -0,0 +1,24 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Reads multiple files and returns a map of file paths to file contents
6
+ * @param {string[]} files - Array of file paths
7
+ * @param {string} src - Source directory path
8
+ * @param {Object} accumulator - Accumulator object for results
9
+ * @returns {Object} Map of file paths to file contents
10
+ */
11
+ export function readFilesToMap(files, src, accumulator = {}) {
12
+ if (files.length === 0) return accumulator;
13
+
14
+ let file = files.shift();
15
+ if (!file) return accumulator;
16
+
17
+ let key = file.substring(path.join(src).length);
18
+ accumulator[key] = fs.readFileSync(file).toString('utf8');
19
+
20
+ return files.length ? readFilesToMap(files, src, accumulator) : accumulator;
21
+ }
22
+
23
+ // Keep the old name as an alias for backward compatibility
24
+ export const getTemplates = readFilesToMap;
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "clovie",
3
+ "version": "0.1.0",
4
+ "description": "Vintage web dev tooling with modern quality of life",
5
+ "keywords": [
6
+ "static-site-generator",
7
+ "ssg",
8
+ "templating",
9
+ "handlebars",
10
+ "nunjucks",
11
+ "pug",
12
+ "mustache",
13
+ "scss",
14
+ "esbuild",
15
+ "live-reload",
16
+ "development-server",
17
+ "web-development",
18
+ "nodejs"
19
+ ],
20
+ "license": "MIT",
21
+ "author": {
22
+ "name": "Adrian Miller",
23
+ "email": "code.mill@fastmail.com"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/adrianjonmiller/clovie.git"
28
+ },
29
+ "homepage": "https://github.com/adrianjonmiller/clovie#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/adrianjonmiller/clovie/issues"
32
+ },
33
+ "main": "lib/main.js",
34
+ "type": "module",
35
+ "bin": {
36
+ "clovie": "bin/cli.js"
37
+ },
38
+ "files": [
39
+ "lib/**/*",
40
+ "bin/**/*",
41
+ "config/**/*",
42
+ "templates/**/*",
43
+ "README.md",
44
+ "LICENSE"
45
+ ],
46
+ "scripts": {
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "test:run": "vitest run",
50
+ "docs:build": "cd docs && node ../bin/cli.js build",
51
+ "docs:dev": "cd docs && node ../bin/cli.js watch",
52
+ "prepublishOnly": "npm test"
53
+ },
54
+ "dependencies": {
55
+ "@babel/core": "^7.23.7",
56
+ "@babel/preset-env": "^7.23.7",
57
+ "esbuild": "^0.19.11",
58
+ "chokidar": "^3.5.3",
59
+ "command-line-args": "^5.2.1",
60
+ "express": "^4.18.2",
61
+ "handlebars": "^4.7.8",
62
+ "sass": "^1.69.5",
63
+ "socket.io": "^4.7.4",
64
+ "type-detect": "^4.0.8",
65
+ "babelify": "^10.0.0"
66
+ },
67
+ "devDependencies": {
68
+ "vitest": "^1.0.4"
69
+ },
70
+ "engines": {
71
+ "node": ">=18.0.0"
72
+ }
73
+ }
@@ -0,0 +1,29 @@
1
+ # {{projectName}}
2
+
3
+ A static site built with Clovie.
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ # Install dependencies
9
+ npm install
10
+
11
+ # Start development server
12
+ npm run dev
13
+
14
+ # Build for production
15
+ npm run build
16
+ ```
17
+
18
+ ## Project Structure
19
+
20
+ - `views/` - HTML templates
21
+ - `scripts/` - JavaScript files
22
+ - `styles/` - SCSS files
23
+ - `assets/` - Static assets
24
+ - `dist/` - Build output
25
+
26
+ ## Learn More
27
+
28
+ - [Clovie Documentation](https://github.com/your-org/clovie)
29
+ - [Examples](https://github.com/your-org/clovie/tree/main/examples)
@@ -0,0 +1,27 @@
1
+ export default {
2
+ // Clovie will auto-detect these paths!
3
+ // Just add your data and models below
4
+
5
+ data: {
6
+ title: '{{projectName}}'
7
+ },
8
+
9
+ // Example models (uncomment to use):
10
+ // models: {
11
+ // posts: {
12
+ // template: '_post.html',
13
+ // output: 'post-{slug}.html',
14
+ // transform: (post) => ({
15
+ // ...post,
16
+ // excerpt: post.content.substring(0, 100) + '...'
17
+ // })
18
+ // }
19
+ // }
20
+
21
+ // Custom compiler (optional - Clovie has a good default)
22
+ // compiler: (template, data) => {
23
+ // return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
24
+ // return data[key] || match;
25
+ // });
26
+ // }
27
+ };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "1.0.0",
4
+ "description": "Clovie static site",
5
+ "scripts": {
6
+ "build": "clovie build",
7
+ "dev": "clovie watch"
8
+ },
9
+ "devDependencies": {
10
+ "clovie": "^0.0.23"
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ console.log("Hello from Clovie!");
@@ -0,0 +1,11 @@
1
+ body {
2
+ font-family: Arial, sans-serif;
3
+ margin: 0;
4
+ padding: 20px;
5
+ background: #f5f5f5;
6
+ }
7
+
8
+ h1 {
9
+ color: #333;
10
+ text-align: center;
11
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>{{title}}</title>
5
+ <link rel="stylesheet" href="main.css">
6
+ </head>
7
+ <body>
8
+ <h1>{{title}}</h1>
9
+ <p>Welcome to your new Clovie site!</p>
10
+ <script src="main.js"></script>
11
+ </body>
12
+ </html>