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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Adrian Miller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,441 @@
1
+ # Clovie - Vintage Web Dev Tooling
2
+
3
+ A Node.js-based static site generator designed to be simple, fast, and highly modular. The "Hollow Knight of Web Dev" - simple but deep, easy to start but room to grow.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ packages/clovie/
9
+ ├── __tests__/ # Test files
10
+ │ └── index.test.js
11
+ ├── bin/ # CLI executable
12
+ │ └── cli.js
13
+ ├── config/ # Configuration files
14
+ │ └── default.config.js
15
+ ├── lib/ # Source code
16
+ │ ├── core/ # Core functionality
17
+ │ │ ├── index.js # Main Clovie class
18
+ │ │ ├── bundler.js # JavaScript bundling
19
+ │ │ ├── render.js # Template rendering
20
+ │ │ ├── write.js # File writing
21
+ │ │ ├── getViews.js # View processing
22
+ │ │ ├── getData.js # Data loading
23
+ │ │ ├── getStyles.js # SCSS compilation
24
+ │ │ └── getAssets.js # Asset processing
25
+ │ └── utils/ # Utility functions
26
+ │ ├── clean.js # Directory cleaning
27
+ │ └── create.js # Project creation
28
+ └── package.json
29
+ ```
30
+
31
+ ## Core Features
32
+
33
+ - **Template Engine Agnostic**: Support for Handlebars, Nunjucks, Pug, Mustache, or custom engines
34
+ - **Asset Processing**: JavaScript bundling with esbuild, SCSS compilation, static asset copying
35
+ - **Development Server**: Live reload with Browser-Sync and file watching
36
+ - **Data-Driven Pages**: Model system for dynamic page generation
37
+ - **Pagination Support**: Built-in pagination for data-driven content
38
+
39
+ ## Usage
40
+
41
+ ### Installation
42
+
43
+ #### Option 1: Local Installation (Recommended)
44
+ ```bash
45
+ # Install as dev dependency in your project
46
+ npm install --save-dev clovie
47
+
48
+ # Use via npm scripts
49
+ npm run build
50
+ npm run dev
51
+ ```
52
+
53
+ #### Option 2: Global Installation
54
+ ```bash
55
+ # Install globally
56
+ npm install -g clovie
57
+ ```
58
+
59
+ ### Creating New Projects
60
+
61
+ #### Using Clovie CLI (Recommended)
62
+ ```bash
63
+ # Create a new project
64
+ npx clovie create my-site
65
+
66
+ # Or with global install
67
+ clovie create my-site
68
+ ```
69
+
70
+
71
+
72
+ ### Building and Development
73
+
74
+ ```bash
75
+ # Build the site
76
+ clovie build
77
+ # or
78
+ npm run build
79
+
80
+ # Start development server with file watching
81
+ clovie watch
82
+ # or
83
+ npm run dev
84
+ ```
85
+
86
+ ## Configuration
87
+
88
+ ### Minimal Configuration (Recommended)
89
+
90
+ Clovie uses smart defaults and auto-detection, so you can start with just:
91
+
92
+ ```javascript
93
+ export default {
94
+ data: {
95
+ title: 'My Site'
96
+ }
97
+ };
98
+ ```
99
+
100
+ Clovie will automatically detect:
101
+ - `views/` directory for HTML templates
102
+ - `scripts/main.js` for JavaScript entry point
103
+ - `styles/main.scss` for SCSS entry point
104
+ - `assets/` directory for static files
105
+
106
+ ### Full Configuration
107
+
108
+ If you need custom paths or behavior:
109
+
110
+ ```javascript
111
+ export default {
112
+ // Custom paths (optional - Clovie will auto-detect if not specified)
113
+ scripts: './src/js/app.js',
114
+ styles: './src/css/main.scss',
115
+ views: './templates',
116
+ assets: './public',
117
+ outputDir: './build',
118
+
119
+ // Your data
120
+ data: {
121
+ title: 'My Site'
122
+ },
123
+
124
+ // Custom compiler (optional - Clovie has a good default)
125
+ compiler: (template, data) => {
126
+ return yourTemplateEngine(template, data);
127
+ }
128
+ };
129
+ ```
130
+
131
+ ## Advanced Features
132
+
133
+ ### Async Data Loading
134
+
135
+ Clovie supports asynchronous data loading for dynamic content:
136
+
137
+ ```javascript
138
+ // app.config.js
139
+ export default {
140
+ // ... other config
141
+ data: async () => {
142
+ // Fetch data from API
143
+ const response = await fetch('https://api.example.com/posts');
144
+ const posts = await response.json();
145
+
146
+ return {
147
+ title: 'My Blog',
148
+ posts: posts,
149
+ timestamp: new Date().toISOString()
150
+ };
151
+ }
152
+ };
153
+ ```
154
+
155
+ ### Data Models & Dynamic Pages
156
+
157
+ Create multiple pages from data arrays using the models system:
158
+
159
+ ```javascript
160
+ // app.config.js
161
+ export default {
162
+ // ... other config
163
+ data: {
164
+ title: 'My Blog',
165
+ posts: [
166
+ { id: 1, title: 'First Post', content: 'Hello World' },
167
+ { id: 2, title: 'Second Post', content: 'Another post' },
168
+ { id: 3, title: 'Third Post', content: 'Yet another' }
169
+ ]
170
+ },
171
+ models: {
172
+ posts: {
173
+ template: '_post.html', // Template to use
174
+ paginate: 2, // Posts per page (optional)
175
+ output: (post, index) => { // Custom output filename
176
+ return `post-${post.id}.html`;
177
+ },
178
+ transform: (post, index) => { // Transform data before rendering
179
+ return {
180
+ ...post,
181
+ excerpt: post.content.substring(0, 100) + '...',
182
+ date: new Date().toISOString()
183
+ };
184
+ }
185
+ }
186
+ }
187
+ };
188
+ ```
189
+
190
+ **Template (`_post.html`):**
191
+ ```html
192
+ <!DOCTYPE html>
193
+ <html>
194
+ <head>
195
+ <title>{{local.title}} - {{title}}</title>
196
+ </head>
197
+ <body>
198
+ <article>
199
+ <h1>{{local.title}}</h1>
200
+ <p>{{local.excerpt}}</p>
201
+ <div>{{local.content}}</div>
202
+ </article>
203
+ </body>
204
+ </html>
205
+ ```
206
+
207
+ **Output:**
208
+ - `post-1.html` - First post page
209
+ - `post-2.html` - Second post page
210
+ - `post-3.html` - Third post page
211
+
212
+ ### Custom Template Engines
213
+
214
+ Clovie is template-engine agnostic. Here are examples for popular engines:
215
+
216
+ #### Handlebars
217
+ ```javascript
218
+ import Handlebars from 'handlebars';
219
+
220
+ export default {
221
+ // ... other config
222
+ compiler: (template, data) => {
223
+ const compiled = Handlebars.compile(template);
224
+ return compiled(data);
225
+ }
226
+ };
227
+ ```
228
+
229
+ #### Nunjucks
230
+ ```javascript
231
+ import nunjucks from 'nunjucks';
232
+
233
+ export default {
234
+ // ... other config
235
+ compiler: (template, data) => {
236
+ return nunjucks.renderString(template, data);
237
+ }
238
+ };
239
+ ```
240
+
241
+ #### Pug
242
+ ```javascript
243
+ import pug from 'pug';
244
+
245
+ export default {
246
+ // ... other config
247
+ compiler: (template, data) => {
248
+ return pug.render(template, { ...data, pretty: true });
249
+ }
250
+ };
251
+ ```
252
+
253
+ #### Custom Engine
254
+ ```javascript
255
+ export default {
256
+ // ... other config
257
+ compiler: (template, data) => {
258
+ // Simple variable replacement
259
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
260
+ return data[key] || match;
261
+ });
262
+ }
263
+ };
264
+ ```
265
+
266
+ ### Pagination
267
+
268
+ The models system includes built-in pagination:
269
+
270
+ ```javascript
271
+ export default {
272
+ // ... other config
273
+ models: {
274
+ blog: {
275
+ template: '_blog.html',
276
+ paginate: 5, // 5 posts per page
277
+ output: (posts, pageNum) => {
278
+ return pageNum === 0 ? 'blog.html' : `blog-${pageNum + 1}.html`;
279
+ }
280
+ }
281
+ }
282
+ };
283
+ ```
284
+
285
+ **Output:**
286
+ - `blog.html` - First 5 posts
287
+ - `blog-2.html` - Next 5 posts
288
+ - `blog-3.html` - Remaining posts
289
+
290
+ ### Data Transformation
291
+
292
+ Transform data before rendering with custom functions:
293
+
294
+ ```javascript
295
+ export default {
296
+ // ... other config
297
+ models: {
298
+ products: {
299
+ template: '_product.html',
300
+ transform: (product, index) => {
301
+ return {
302
+ ...product,
303
+ price: `$${product.price.toFixed(2)}`,
304
+ slug: product.name.toLowerCase().replace(/\s+/g, '-'),
305
+ inStock: product.quantity > 0
306
+ };
307
+ }
308
+ }
309
+ }
310
+ };
311
+ ```
312
+
313
+ ## Error Handling & Best Practices
314
+
315
+ ### Error Handling
316
+
317
+ Clovie includes robust error handling for common issues:
318
+
319
+ - **Missing directories**: Gracefully handles missing views, scripts, or assets folders
320
+ - **File read errors**: Continues processing even if individual files fail
321
+ - **Template errors**: Provides clear error messages for compilation failures
322
+ - **Data validation**: Warns about invalid data structures
323
+
324
+ ### Progress Indicators
325
+
326
+ Clovie provides clear feedback during builds:
327
+
328
+ ```
329
+ 🚀 Starting build...
330
+ 🧹 Cleaning output directory...
331
+ 📊 Loading data...
332
+ Loaded 2 data sources
333
+ 📝 Processing views...
334
+ Processed 5 views
335
+ 🎨 Rendering templates...
336
+ Rendered 5 templates
337
+ ⚡ Bundling scripts...
338
+ Bundled 1 script files
339
+ 🎨 Compiling styles...
340
+ Compiled 1 style files
341
+ 📦 Processing assets...
342
+ Processed 3 asset files
343
+ 💾 Writing files...
344
+ ✅ Build completed in 45ms
345
+ ```
346
+
347
+ ### Auto-Discovery
348
+
349
+ Clovie automatically detects common project structures:
350
+
351
+ ```
352
+ 🔍 Auto-detected views directory: views
353
+ 🔍 Auto-detected scripts entry: scripts/main.js
354
+ 🔍 Auto-detected styles entry: styles/main.scss
355
+ 🔍 Auto-detected assets directory: assets
356
+ ```
357
+
358
+ ### Best Practices
359
+
360
+ 1. **Use partial templates** (files starting with `_`) for reusable components
361
+ 2. **Validate data structures** before passing to models
362
+ 3. **Handle async data** with proper error catching
363
+ 4. **Use meaningful output filenames** for SEO and organization
364
+ 5. **Transform data** in the model configuration, not in templates
365
+
366
+ ### Project Structure
367
+
368
+ When you create a new project with `clovie create`, you get this structure:
369
+
370
+ ```
371
+ my-site/
372
+ ├── app.config.js # Configuration
373
+ ├── package.json # Dependencies and scripts
374
+ ├── README.md # Project documentation
375
+ ├── views/ # HTML templates
376
+ │ └── index.html # Home page template
377
+ ├── scripts/ # JavaScript
378
+ │ └── main.js # Main script file
379
+ ├── styles/ # SCSS
380
+ │ └── main.scss # Main stylesheet
381
+ └── assets/ # Static files (images, etc.)
382
+ ```
383
+
384
+ #### Custom Project Structure
385
+ You can also create your own structure:
386
+
387
+ ```
388
+ my-site/
389
+ ├── app.config.js # Configuration
390
+ ├── views/ # Templates
391
+ │ ├── _base.html # Base template (partial)
392
+ │ ├── _header.html # Header partial
393
+ │ ├── index.html # Home page
394
+ │ └── _post.html # Post template (partial)
395
+ ├── scripts/ # JavaScript
396
+ │ └── main.js
397
+ ├── styles/ # SCSS
398
+ │ └── main.scss
399
+ ├── assets/ # Static files
400
+ │ └── images/
401
+ └── data/ # Data files (optional)
402
+ └── posts.json
403
+ ```
404
+
405
+ ## Development
406
+
407
+ ```bash
408
+ # Install dependencies
409
+ npm install
410
+
411
+ # Run tests
412
+ npm test
413
+
414
+ # Run tests in watch mode
415
+ npm run test:watch
416
+ ```
417
+
418
+ ## Troubleshooting
419
+
420
+ ### Common Issues
421
+
422
+ **"Views directory does not exist"**
423
+ - Ensure the `views` path in your config is correct
424
+ - Create the views directory if it doesn't exist
425
+
426
+ **"Data for model must be an array"**
427
+ - Check that your data structure matches the model configuration
428
+ - Ensure the referenced data key contains an array
429
+
430
+ **"Maximum directory depth exceeded"**
431
+ - Check for circular symlinks or extremely deep directory structures
432
+ - The limit is 50 levels deep (configurable in code)
433
+
434
+ **Build failures**
435
+ - Check console output for specific error messages
436
+ - Verify all referenced files exist
437
+ - Ensure template syntax matches your compiler
438
+
439
+ ## License
440
+
441
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ import path from "path";
3
+ import commandLineArgs from "command-line-args";
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
+ // Local
11
+ import Clovie from "../lib/main.js";
12
+
13
+ // Check for create command first (before any argument parsing)
14
+ if (process.argv.includes('create') && process.argv.length > 2) {
15
+ const projectName = process.argv[3]; // The name after 'create'
16
+
17
+ if (!projectName) {
18
+ console.error('Error: Please provide a project name');
19
+ console.error('Usage: clovie create <project-name>');
20
+ process.exit(1);
21
+ }
22
+
23
+ const fs = await import('fs');
24
+ const projectPath = path.resolve(process.cwd(), projectName);
25
+
26
+ if (fs.existsSync(projectPath)) {
27
+ console.error(`Error: Directory '${projectName}' already exists`);
28
+ process.exit(1);
29
+ }
30
+
31
+ // Copy template files
32
+ const templateDir = path.join(__dirname, '../templates/default');
33
+
34
+ // Create project directory first
35
+ fs.mkdirSync(projectPath, { recursive: true });
36
+
37
+ const copyDir = async (src, dest) => {
38
+ const entries = fs.readdirSync(src, { withFileTypes: true });
39
+
40
+ for (const entry of entries) {
41
+ const srcPath = path.join(src, entry.name);
42
+ const destPath = path.join(dest, entry.name);
43
+
44
+ if (entry.isDirectory()) {
45
+ fs.mkdirSync(destPath, { recursive: true });
46
+ await copyDir(srcPath, destPath);
47
+ } else {
48
+ let content = fs.readFileSync(srcPath, 'utf8');
49
+ // Replace template variables
50
+ content = content.replace(/\{\{projectName\}\}/g, projectName);
51
+ fs.writeFileSync(destPath, content);
52
+ }
53
+ }
54
+ };
55
+
56
+ try {
57
+ await copyDir(templateDir, projectPath);
58
+ console.log(`✅ Clovie project created successfully at ${projectPath}`);
59
+ console.log('\nNext steps:');
60
+ console.log(` cd ${projectName}`);
61
+ console.log(' npm install');
62
+ console.log(' npm run dev');
63
+ process.exit(0);
64
+ } catch (err) {
65
+ console.error('Error creating project:', err);
66
+ process.exit(1);
67
+ }
68
+ }
69
+
70
+ // Commandline options for other commands
71
+ const mainDefinitions = [
72
+ { name: 'command', defaultOption: true },
73
+ { name: 'watch', alias: 'w', type: Boolean }
74
+ ];
75
+ const mainOptions = commandLineArgs(mainDefinitions, { stopAtFirstUnknown: true });
76
+ const argv = mainOptions._unknown || [];
77
+
78
+ // Command-specific options
79
+ const optionDefinitions = [
80
+ { name: 'config', alias: 'c', type: String, defaultValue: 'app.config.js' },
81
+ { name: 'watch', alias: 'w', type: Boolean },
82
+ { name: 'template', alias: 't', type: String, defaultValue: 'default' }
83
+ ];
84
+
85
+ const options = commandLineArgs(optionDefinitions, { argv });
86
+
87
+
88
+
89
+ // Handle watch command
90
+ if (mainOptions.command === 'watch') {
91
+ options.watch = true;
92
+ }
93
+
94
+ // Config path
95
+ const configPath = path.resolve(process.cwd(), options.config);
96
+
97
+ // Main function
98
+ async function main() {
99
+ try {
100
+ // Config file
101
+ const configModule = await import(configPath);
102
+ const config = configModule.default || configModule;
103
+
104
+ // New Clovie instance
105
+ const site = new Clovie(config);
106
+
107
+ site.error(err => {
108
+ console.error(err);
109
+ process.exit(1);
110
+ });
111
+
112
+ if (options.watch) {
113
+ await site.startWatch();
114
+ } else {
115
+ await site.build();
116
+ console.log('Build complete');
117
+ process.exit(0);
118
+ }
119
+ } catch (err) {
120
+ console.error(err);
121
+ process.exit(1);
122
+ }
123
+ }
124
+
125
+ // Run main function
126
+ main();
@@ -0,0 +1,31 @@
1
+ import Handlebars from 'handlebars';
2
+ import path from 'path';
3
+
4
+ export default {
5
+ // Smart defaults - these paths are automatically detected
6
+ scripts: null, // Will auto-detect if not specified
7
+ styles: null, // Will auto-detect if not specified
8
+ views: null, // Will auto-detect if not specified
9
+ assets: null, // Will auto-detect if not specified
10
+ outputDir: path.resolve('./dist/'),
11
+
12
+ // Data and models
13
+ data: {},
14
+ models: {},
15
+
16
+ // Default compiler - Handlebars for powerful templating
17
+ compiler: (template, data) => {
18
+ try {
19
+ const compiled = Handlebars.compile(template);
20
+ return compiled(data);
21
+ } catch (err) {
22
+ console.warn(`⚠️ Template compilation error: ${err.message}`);
23
+ return template; // Fallback to raw template
24
+ }
25
+ },
26
+
27
+ // Development options
28
+ watch: false,
29
+ port: 3000,
30
+ open: false
31
+ }
@@ -0,0 +1,31 @@
1
+ import path from 'path';
2
+ import esbuild from 'esbuild';
3
+
4
+ export default function(file) {
5
+ const pathObj = path.parse(file);
6
+
7
+ return new Promise(async (resolve, reject) => {
8
+ try {
9
+ const result = await esbuild.build({
10
+ entryPoints: [file],
11
+ bundle: true,
12
+ write: false,
13
+ format: 'iife',
14
+ globalName: 'app',
15
+ platform: 'browser',
16
+ target: ['es2015'],
17
+ minify: false,
18
+ sourcemap: false,
19
+ // Performance optimizations
20
+ treeShaking: true,
21
+ metafile: false,
22
+ logLevel: 'silent'
23
+ });
24
+
25
+ const { text } = result.outputFiles[0];
26
+ resolve({[`${pathObj.name}.js`]: text});
27
+ } catch (err) {
28
+ reject(err);
29
+ }
30
+ });
31
+ };
@@ -0,0 +1,69 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+
5
+ export class BuildCache {
6
+ constructor(cacheDir) {
7
+ this.cacheDir = cacheDir;
8
+ this.cacheFile = path.join(cacheDir, '.clovie-cache.json');
9
+ this.cache = this.loadCache();
10
+ }
11
+
12
+ loadCache() {
13
+ try {
14
+ if (fs.existsSync(this.cacheFile)) {
15
+ return JSON.parse(fs.readFileSync(this.cacheFile, 'utf8'));
16
+ }
17
+ } catch (err) {
18
+ console.warn('⚠️ Could not load cache, starting fresh');
19
+ }
20
+ return { files: {}, lastBuild: null };
21
+ }
22
+
23
+ saveCache() {
24
+ try {
25
+ if (!fs.existsSync(this.cacheDir)) {
26
+ fs.mkdirSync(this.cacheDir, { recursive: true });
27
+ }
28
+ fs.writeFileSync(this.cacheFile, JSON.stringify(this.cache, null, 2));
29
+ } catch (err) {
30
+ console.warn('⚠️ Could not save cache');
31
+ }
32
+ }
33
+
34
+ getFileHash(filePath) {
35
+ try {
36
+ if (!fs.existsSync(filePath)) return null;
37
+ const content = fs.readFileSync(filePath);
38
+ return crypto.createHash('md5').update(content).digest('hex');
39
+ } catch (err) {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ hasChanged(filePath) {
45
+ const currentHash = this.getFileHash(filePath);
46
+ const cachedHash = this.cache.files[filePath];
47
+
48
+ if (currentHash !== cachedHash) {
49
+ this.cache.files[filePath] = currentHash;
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ getChangedFiles(files) {
56
+ return files.filter(file => this.hasChanged(file));
57
+ }
58
+
59
+ markBuilt() {
60
+ this.cache.lastBuild = Date.now();
61
+ this.saveCache();
62
+ }
63
+
64
+ getBuildStats() {
65
+ const totalFiles = Object.keys(this.cache.files).length;
66
+ const changedFiles = Object.values(this.cache.files).filter(hash => hash !== null).length;
67
+ return { totalFiles, changedFiles };
68
+ }
69
+ }