clovie 0.1.4 → 0.1.6

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 CHANGED
@@ -1,40 +1,105 @@
1
1
  # Clovie - Vintage Web Dev Tooling
2
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.
3
+ A Node.js-based static site generator and web framework designed to be simple, fast, and highly modular. Built on the **brickworks/engine** pattern for maximum flexibility and maintainability.
4
+
5
+ ## Architecture Overview
6
+
7
+ Clovie uses a **service-oriented architecture** where all functionality is provided by services that extend `ServiceProvider` from the brickworks/engine framework. The engine orchestrates these services through dependency injection and provides stable state management.
8
+
9
+ ### Core Services
10
+
11
+ - **File** - File system operations (reading, writing, watching)
12
+ - **Compiler** - Template compilation with live reload injection
13
+ - **Views** - View discovery and processing
14
+ - **Routes** - Route generation and processing (static & dynamic)
15
+ - **Build** - Static site generation orchestration
16
+ - **Server** - Express server management (conditionally loaded)
17
+ - **Cache** - Build caching and incremental builds
18
+
19
+ ### Two Operating Modes
20
+
21
+ **Static Mode (`type: 'static'`)**:
22
+ - Generates static HTML files to output directory
23
+ - Uses Express server only for development (live reload, file serving)
24
+ - Traditional static site generator behavior
25
+
26
+ **Server Mode (`type: 'server'`)**:
27
+ - Builds a real Express application
28
+ - Serves static files + handles dynamic routes/API endpoints
29
+ - Full web application server with server-side rendering
4
30
 
5
31
  ## Project Structure
6
32
 
7
33
  ```
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
34
+ clovie/
35
+ ├── __tests__/ # Test files
36
+ ├── bin/ # CLI executable
37
+ │ └── cli.js # Command line interface
38
+ ├── config/ # Configuration files
39
+ │ └── clovie.config.js # Default configuration
40
+ ├── lib/ # Source code - Service-based architecture
41
+ ├── createClovie.js # Engine factory function
42
+ │ ├── Build.js # Build service
43
+ │ ├── Cache.js # Caching service
44
+ │ ├── Compiler.js # Template compilation service
45
+ │ ├── File.js # File system service
46
+ │ ├── Routes.js # Routing service
47
+ │ ├── Server.js # Express server service
48
+ │ ├── Views.js # View processing service
49
+ └── utils/ # Utility functions
50
+ ├── clean.js # Directory cleaning
51
+ ├── discover.js # Auto-discovery
52
+ └── liveReloadScript.js # Live reload
53
+ ├── templates/ # Project templates
54
+ └── examples/ # Configuration examples
55
+ ```
56
+
57
+ ## Service Architecture
58
+
59
+ Each service in Clovie extends `ServiceProvider` and defines:
60
+
61
+ - **Static manifest**: Name, namespace, version, and dependencies
62
+ - **initialize()**: Setup phase with access to config and context
63
+ - **actions()**: Methods exposed through the engine context
64
+
65
+ ### Service Dependencies
66
+
67
+ Services declare their dependencies in their manifest:
68
+
69
+ ```javascript
70
+ static manifest = {
71
+ name: 'Clovie Build',
72
+ namespace: 'build',
73
+ version: '1.0.0',
74
+ dependencies: [Cache, Routes] // Initialized first
75
+ };
76
+ ```
77
+
78
+ ### State Management
79
+
80
+ The engine provides two state stores:
81
+
82
+ - **`state`**: Reactive store for build-time data (from config.data)
83
+ - **`stable`**: Persistent storage (cache, build stats, etc.)
84
+
85
+ Services access these via `useContext()`:
86
+
87
+ ```javascript
88
+ actions(useContext) {
89
+ const { state, stable, file, compiler } = useContext();
90
+ // Service methods can access other services and state
91
+ }
29
92
  ```
30
93
 
31
94
  ## Core Features
32
95
 
33
96
  - **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
97
+ - **Asset Processing**: JavaScript bundling with esbuild, SCSS compilation, static asset copying
98
+ - **Development Server**: Live reload with Express and file watching
99
+ - **Dynamic Routing**: Powerful route system for both static and server-side page generation
100
+ - **Server-Side Rendering**: Full Express applications with dynamic routes and API endpoints
101
+ - **Incremental Builds**: Smart caching for faster rebuilds
102
+ - **Auto-Discovery**: Intelligent project structure detection
38
103
 
39
104
  ## Usage
40
105
 
@@ -71,18 +136,27 @@ clovie create my-site
71
136
 
72
137
  ### Building and Development
73
138
 
139
+ #### Static Mode (Default)
74
140
  ```bash
75
- # Build the site
141
+ # Build static files
76
142
  clovie build
77
143
  # or
78
144
  npm run build
79
145
 
80
- # Start development server with file watching
146
+ # Development server with live reload
81
147
  clovie watch
82
148
  # or
83
149
  npm run dev
84
150
  ```
85
151
 
152
+ #### Server Mode
153
+ ```bash
154
+ # Run as Express server application
155
+ clovie server
156
+ # or add to package.json:
157
+ # "scripts": { "serve": "clovie server" }
158
+ ```
159
+
86
160
  ## Configuration
87
161
 
88
162
  ### Minimal Configuration (Recommended)
@@ -103,28 +177,87 @@ Clovie will automatically detect:
103
177
  - `styles/main.scss` for SCSS entry point
104
178
  - `assets/` directory for static files
105
179
 
106
- ### Full Configuration
107
-
108
- If you need custom paths or behavior:
180
+ ### Static Site Configuration
109
181
 
110
182
  ```javascript
111
183
  export default {
112
- // Custom paths (optional - Clovie will auto-detect if not specified)
184
+ type: 'static', // Default - generates static files
185
+
186
+ // Auto-detected paths (override if needed)
187
+ views: './src/views',
113
188
  scripts: './src/js/app.js',
114
- styles: './src/css/main.scss',
115
- views: './templates',
189
+ styles: './src/scss/main.scss',
116
190
  assets: './public',
117
- outputDir: './build',
191
+ outputDir: './dist',
118
192
 
119
- // Your data
120
- data: {
121
- title: 'My Site'
193
+ // Template compilation
194
+ templateCompiler: (template, data) => {
195
+ return yourTemplateEngine(template, data);
122
196
  },
123
197
 
124
- // Custom compiler (optional - Clovie has a good default)
125
- compiler: (template, data) => {
126
- return yourTemplateEngine(template, data);
127
- }
198
+ // Routes for dynamic pages from data
199
+ routes: [{
200
+ name: 'Blog Posts',
201
+ path: '/posts/:slug',
202
+ template: 'post.html',
203
+ repeat: (state) => state.get(['posts']),
204
+ data: (state, post) => ({
205
+ ...post,
206
+ title: post.title,
207
+ slug: post.slug
208
+ })
209
+ }]
210
+ };
211
+ ```
212
+
213
+ ### Server Application Configuration
214
+
215
+ ```javascript
216
+ export default {
217
+ type: 'server', // Express application mode
218
+
219
+ port: 3000,
220
+ outputDir: './dist', // Serve static files from here
221
+
222
+ // Same view/asset processing as static mode
223
+ views: './src/views',
224
+ scripts: './src/js',
225
+ styles: './src/scss',
226
+
227
+ // Server-specific routes
228
+ routes: [{
229
+ name: 'Posts API',
230
+ path: '/api/posts',
231
+ method: 'GET',
232
+ handler: async (req, res) => {
233
+ const posts = await getPosts();
234
+ res.json(posts);
235
+ }
236
+ }, {
237
+ name: 'Post Pages',
238
+ path: '/posts/:slug',
239
+ template: 'post.html',
240
+ data: async (state, params) => {
241
+ const post = await getPost(params.slug);
242
+ return { post };
243
+ }
244
+ }],
245
+
246
+ // API routes
247
+ api: [{
248
+ path: '/api/users',
249
+ method: 'POST',
250
+ handler: async (req, res) => {
251
+ const user = await createUser(req.body);
252
+ res.json(user);
253
+ }
254
+ }],
255
+
256
+ // Express middleware
257
+ middleware: [
258
+ express.json(),
259
+ cors()
260
+ ]
128
261
  };
129
262
  ```
130
263
 
@@ -152,9 +285,9 @@ export default {
152
285
  };
153
286
  ```
154
287
 
155
- ### Data Models & Dynamic Pages
288
+ ### Dynamic Routes & Data-Driven Pages
156
289
 
157
- Create multiple pages from data arrays using the models system:
290
+ Create multiple pages from data arrays using the routes system:
158
291
 
159
292
  ```javascript
160
293
  // clovie.config.js
@@ -163,51 +296,46 @@ export default {
163
296
  data: {
164
297
  title: 'My Blog',
165
298
  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' }
299
+ { id: 1, title: 'First Post', content: 'Hello World', slug: 'first-post' },
300
+ { id: 2, title: 'Second Post', content: 'Another post', slug: 'second-post' },
301
+ { id: 3, title: 'Third Post', content: 'Yet another', slug: 'third-post' }
169
302
  ]
170
303
  },
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
- }
304
+ routes: [{
305
+ name: 'Blog Posts',
306
+ path: '/posts/:slug',
307
+ template: 'post.html',
308
+ repeat: (state) => state.get(['posts']),
309
+ data: (state, post) => ({
310
+ ...post,
311
+ excerpt: post.content.substring(0, 100) + '...',
312
+ date: new Date().toISOString()
313
+ })
314
+ }]
187
315
  };
188
316
  ```
189
317
 
190
- **Template (`_post.html`):**
318
+ **Template (`post.html`):**
191
319
  ```html
192
320
  <!DOCTYPE html>
193
321
  <html>
194
322
  <head>
195
- <title>{{local.title}} - {{title}}</title>
323
+ <title>{{title}} - {{../title}}</title>
196
324
  </head>
197
325
  <body>
198
326
  <article>
199
- <h1>{{local.title}}</h1>
200
- <p>{{local.excerpt}}</p>
201
- <div>{{local.content}}</div>
327
+ <h1>{{title}}</h1>
328
+ <p>{{excerpt}}</p>
329
+ <div>{{content}}</div>
202
330
  </article>
203
331
  </body>
204
332
  </html>
205
333
  ```
206
334
 
207
335
  **Output:**
208
- - `post-1.html` - First post page
209
- - `post-2.html` - Second post page
210
- - `post-3.html` - Third post page
336
+ - `posts/first-post.html` - First post page
337
+ - `posts/second-post.html` - Second post page
338
+ - `posts/third-post.html` - Third post page
211
339
 
212
340
  ### Custom Template Engines
213
341
 
@@ -263,50 +391,55 @@ export default {
263
391
  };
264
392
  ```
265
393
 
266
- ### Pagination
394
+ ### Route Pagination
267
395
 
268
- The models system includes built-in pagination:
396
+ Routes support built-in pagination for large datasets:
269
397
 
270
398
  ```javascript
271
399
  export default {
272
400
  // ... 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
- }
401
+ routes: [{
402
+ name: 'Blog Pagination',
403
+ path: '/blog/:page?',
404
+ template: 'blog.html',
405
+ paginate: 5, // 5 posts per page
406
+ repeat: (state) => state.get(['posts']),
407
+ data: (state, posts, pageInfo) => ({
408
+ posts,
409
+ pagination: pageInfo,
410
+ title: `Blog - Page ${pageInfo.current}`
411
+ })
412
+ }]
282
413
  };
283
414
  ```
284
415
 
285
416
  **Output:**
286
- - `blog.html` - First 5 posts
287
- - `blog-2.html` - Next 5 posts
288
- - `blog-3.html` - Remaining posts
417
+ - `blog.html` - First 5 posts (page 1)
418
+ - `blog/2.html` - Next 5 posts (page 2)
419
+ - `blog/3.html` - Remaining posts (page 3)
289
420
 
290
- ### Data Transformation
421
+ ### Data Transformation in Routes
291
422
 
292
- Transform data before rendering with custom functions:
423
+ Transform data before rendering using the `data` function:
293
424
 
294
425
  ```javascript
295
426
  export default {
296
427
  // ... 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
- }
428
+ routes: [{
429
+ name: 'Products',
430
+ path: '/products/:slug',
431
+ template: 'product.html',
432
+ repeat: (state) => state.get(['products']),
433
+ data: (state, product) => ({
434
+ ...product,
435
+ price: `$${product.price.toFixed(2)}`,
436
+ slug: product.name.toLowerCase().replace(/\s+/g, '-'),
437
+ inStock: product.quantity > 0,
438
+ relatedProducts: state.get(['products']).filter(p =>
439
+ p.category === product.category && p.id !== product.id
440
+ )
441
+ })
442
+ }]
310
443
  };
311
444
  ```
312
445
 
@@ -358,10 +491,11 @@ Clovie automatically detects common project structures:
358
491
  ### Best Practices
359
492
 
360
493
  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
494
+ 2. **Validate data structures** before passing to routes
495
+ 3. **Handle async data** with proper error catching in route data functions
496
+ 4. **Use meaningful route paths** for SEO and organization
497
+ 5. **Transform data** in route data functions, not in templates
498
+ 6. **Separate static and dynamic routes** for better performance
365
499
 
366
500
  ### Project Structure
367
501
 
@@ -423,9 +557,9 @@ npm run test:watch
423
557
  - Ensure the `views` path in your config is correct
424
558
  - Create the views directory if it doesn't exist
425
559
 
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
560
+ **"Route repeat function must return an array"**
561
+ - Check that your route's repeat function returns an array
562
+ - Ensure the data structure matches the route configuration
429
563
 
430
564
  **"Maximum directory depth exceeded"**
431
565
  - Check for circular symlinks or extremely deep directory structures
package/bin/cli.js CHANGED
@@ -8,7 +8,7 @@ const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = dirname(__filename);
9
9
 
10
10
  // Local
11
- import Clovie from "../lib/main.js";
11
+ import { createClovie } from "../lib/createClovie.js";
12
12
 
13
13
  // Check for create command first (before any argument parsing)
14
14
  if (process.argv.includes('create') && process.argv.length > 2) {
@@ -91,6 +91,11 @@ if (mainOptions.command === 'watch') {
91
91
  options.watch = true;
92
92
  }
93
93
 
94
+ // Handle server command
95
+ if (mainOptions.command === 'server') {
96
+ options.server = true;
97
+ }
98
+
94
99
  // Config path
95
100
  const configPath = path.resolve(process.cwd(), options.config);
96
101
 
@@ -113,19 +118,103 @@ async function main() {
113
118
  }
114
119
  }
115
120
 
116
- // New Clovie instance
117
- const site = new Clovie(config);
121
+ // Override config type if server command is used
122
+ if (options.server) {
123
+ config.type = 'server';
124
+ }
118
125
 
119
- site.error(err => {
120
- console.error(err);
121
- process.exit(1);
122
- });
126
+ // New Clovie instance
127
+ const clovie = await createClovie(config);
123
128
 
124
- if (options.watch) {
125
- await site.startWatch();
129
+ if (options.server) {
130
+ // Server mode - run as Express server
131
+ console.log('🌐 Starting server mode...');
132
+
133
+ // Load data into state first
134
+ if (config.data) {
135
+ console.log('📊 Loading data into state...');
136
+ let loadedData = {};
137
+ if (typeof config.data === 'function') {
138
+ loadedData = await config.data();
139
+ } else if (typeof config.data === 'object') {
140
+ loadedData = config.data;
141
+ }
142
+ clovie.state.load(loadedData);
143
+ console.log(` Loaded ${Object.keys(loadedData).length} data sources into state`);
144
+ }
145
+
146
+ // Start server
147
+ clovie.server.start();
148
+
149
+ // Keep the process running
150
+ process.on('SIGINT', () => {
151
+ console.log('\n🛑 Stopping server...');
152
+ clovie.server.stop();
153
+ process.exit(0);
154
+ });
155
+
156
+ } else if (options.watch) {
157
+ // Development mode with file watching
158
+ console.log('🏗️ Initial build...');
159
+ await clovie.build.static();
160
+ console.log('✅ Initial build completed\n');
161
+
162
+ // Start development server
163
+ clovie.server.start();
164
+
165
+ // Set up file watching
166
+ console.log('👀 Setting up file watching...');
167
+ const watchPaths = [
168
+ config.views,
169
+ config.partials,
170
+ config.styles,
171
+ config.scripts,
172
+ ].filter(Boolean); // Remove undefined paths
173
+
174
+ const watchers = clovie.file.watch(watchPaths);
175
+
176
+ // Set up event handlers for each watcher
177
+ watchers.forEach(watcher => {
178
+ watcher.on('change', async (filePath) => {
179
+ console.log(`🔄 File changed: ${filePath}`);
180
+ console.log('🔄 Triggering rebuild...');
181
+
182
+ try {
183
+ const result = await clovie.build.static();
184
+ console.log(`✅ Rebuild completed in ${result.buildTime}ms`);
185
+
186
+ // Notify live reload
187
+ if (clovie.server && clovie.server.notifyReload) {
188
+ clovie.server.notifyReload();
189
+ }
190
+ } catch (error) {
191
+ console.error('❌ Rebuild failed:', error.message);
192
+ }
193
+ });
194
+
195
+ watcher.on('error', (error) => {
196
+ console.error('❌ File watcher error:', error);
197
+ });
198
+ });
199
+
200
+ console.log(`🌐 Development server running at http://localhost:${config.port || 3000}`);
201
+ console.log('👀 Watching for file changes...');
202
+ console.log('Press Ctrl+C to stop the server\n');
203
+
204
+ // Keep the process running
205
+ process.on('SIGINT', () => {
206
+ console.log('\n🛑 Stopping development server...');
207
+ if (clovie.file.isWatching()) {
208
+ clovie.file.stopWatching();
209
+ }
210
+ process.exit(0);
211
+ });
212
+
126
213
  } else {
127
- await site.build();
128
- console.log('Build complete');
214
+ // Build mode
215
+ const result = await clovie.build.static();
216
+ console.log(`✅ Build completed in ${result.buildTime}ms`);
217
+ console.log(`📁 Generated ${result.filesGenerated} files`);
129
218
  process.exit(0);
130
219
  }
131
220
  } catch (err) {
@@ -31,5 +31,22 @@ export default {
31
31
  // Development options
32
32
  watch: false,
33
33
  port: 3000,
34
- open: false
34
+ open: false,
35
+ routes: [
36
+ {
37
+ name: 'Products',
38
+ path: '/products/:slug',
39
+ template: 'index.html',
40
+ repeat: (state) => {
41
+ return state.get(['products'])
42
+ },
43
+ data: (state, item) => {
44
+ return {
45
+ ...item,
46
+ slug: item.name,
47
+ title: item.name
48
+ }
49
+ }
50
+ }
51
+ ]
35
52
  }