@rettangoli/sites 0.2.0-rc3 → 0.2.0-rc5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/sites",
3
- "version": "0.2.0-rc3",
3
+ "version": "0.2.0-rc5",
4
4
  "description": "Generate static sites using Markdown and YAML. Straightforward, zero-complexity. Complete toolkit for landing pages, blogs, documentation, admin dashboards, and more.git remote add origin git@github.com:yuusoft-org/sitic.git",
5
5
  "author": {
6
6
  "name": "Luciano Hanyon Wu",
@@ -17,7 +17,7 @@
17
17
  "templates"
18
18
  ],
19
19
  "dependencies": {
20
- "jempl": "^0.2.0-rc1",
20
+ "jempl": "^0.3.1-rc1",
21
21
  "js-yaml": "^4.1.0",
22
22
  "markdown-it": "^14.1.0",
23
23
  "playwright": "^1.55.0",
package/src/cli/build.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
- import path from 'path';
3
2
  import { createSiteBuilder } from '../createSiteBuilder.js';
3
+ import { loadSiteConfig } from '../utils/loadSiteConfig.js';
4
4
 
5
5
  /**
6
6
  * Build the static site
@@ -10,27 +10,21 @@ import { createSiteBuilder } from '../createSiteBuilder.js';
10
10
  * @param {boolean} options.quiet - Suppress build output logs
11
11
  */
12
12
  export const buildSite = async (options = {}) => {
13
- const { rootDir = process.cwd(), mdRender, quiet = false } = options;
14
-
15
- // Try to load config file if it exists
13
+ const { rootDir = process.cwd(), mdRender, functions, quiet = false } = options;
14
+
15
+ // Load config file if needed
16
16
  let config = {};
17
- if (!mdRender) {
18
- try {
19
- const configPath = path.join(rootDir, 'sites.config.js');
20
- const configModule = await import(configPath);
21
- config = configModule.default || {};
22
- } catch (e) {
23
- // Config file is optional, continue without it
24
- }
17
+ if (!mdRender || !functions) {
18
+ config = await loadSiteConfig(rootDir);
25
19
  }
26
20
 
27
- const build = createSiteBuilder({
28
- fs,
21
+ const build = createSiteBuilder({
22
+ fs,
29
23
  rootDir,
30
24
  mdRender: mdRender || config.mdRender,
31
- functions: config.functions || {},
25
+ functions: functions || config.functions || {},
32
26
  quiet
33
27
  });
34
-
28
+
35
29
  build();
36
30
  };
package/src/cli/watch.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import fs, { watch, existsSync } from 'node:fs';
2
2
  import http from 'node:http';
3
3
  import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
4
5
  import { WebSocketServer } from 'ws';
5
6
  import { buildSite } from './build.js';
6
7
  import ScreenshotCapture from './screenshot.js';
8
+ import { loadSiteConfig } from '../utils/loadSiteConfig.js';
7
9
 
8
10
  // Client script to inject into HTML pages
9
11
  const CLIENT_SCRIPT = `
@@ -74,16 +76,16 @@ class DevServer {
74
76
  // Create WebSocket server
75
77
  this.wss = new WebSocketServer({ server: this.httpServer });
76
78
  console.log('WebSocket server created');
77
-
79
+
78
80
  this.wss.on('connection', (ws) => {
79
81
  console.log('✅ Client connected. Total clients:', this.clients.size + 1);
80
82
  this.clients.add(ws);
81
-
83
+
82
84
  ws.on('close', () => {
83
85
  console.log('❌ Client disconnected. Remaining clients:', this.clients.size - 1);
84
86
  this.clients.delete(ws);
85
87
  });
86
-
88
+
87
89
  ws.on('error', (err) => {
88
90
  console.error('❌ WebSocket error:', err);
89
91
  this.clients.delete(ws);
@@ -103,21 +105,21 @@ class DevServer {
103
105
  const urlParts = req.url.split('?');
104
106
  let urlPath = urlParts[0];
105
107
  const queryString = urlParts[1] || '';
106
-
108
+
107
109
  // Check if this is a screenshot request
108
110
  const isScreenshotRequest = queryString.includes('screenshot=true');
109
-
111
+
110
112
  // Default to index.html for root
111
113
  if (urlPath === '/') {
112
114
  urlPath = '/index.html';
113
115
  }
114
-
116
+
115
117
  // Handle trailing slash - remove it for processing
116
118
  const hasTrailingSlash = urlPath.endsWith('/') && urlPath !== '/';
117
119
  if (hasTrailingSlash) {
118
120
  urlPath = urlPath.slice(0, -1);
119
121
  }
120
-
122
+
121
123
  // Handle paths without extensions
122
124
  if (!path.extname(urlPath)) {
123
125
  // First try as .html file
@@ -132,16 +134,16 @@ class DevServer {
132
134
  }
133
135
  }
134
136
  }
135
-
137
+
136
138
  const filePath = path.join(this.siteDir, urlPath);
137
-
139
+
138
140
  // Check if file exists
139
141
  if (!existsSync(filePath)) {
140
142
  res.writeHead(404);
141
143
  res.end('404 Not Found');
142
144
  return;
143
145
  }
144
-
146
+
145
147
  // Check if it's a directory
146
148
  const stats = fs.statSync(filePath);
147
149
  if (stats.isDirectory()) {
@@ -155,18 +157,18 @@ class DevServer {
155
157
  return;
156
158
  }
157
159
  }
158
-
160
+
159
161
  // Serve the file
160
162
  this.serveFile(filePath, res, isScreenshotRequest);
161
163
  }
162
-
164
+
163
165
  serveFile(filePath, res, skipWebSocket = false) {
164
166
  const ext = path.extname(filePath);
165
167
  const contentType = this.getContentType(ext);
166
-
168
+
167
169
  try {
168
170
  let content = fs.readFileSync(filePath);
169
-
171
+
170
172
  // Inject client script into HTML files (unless it's a screenshot request)
171
173
  if (ext === '.html' && !skipWebSocket) {
172
174
  content = content.toString();
@@ -179,7 +181,7 @@ class DevServer {
179
181
  content = content + CLIENT_SCRIPT;
180
182
  }
181
183
  }
182
-
184
+
183
185
  res.writeHead(200, { 'Content-Type': contentType });
184
186
  res.end(content);
185
187
  } catch (err) {
@@ -206,12 +208,12 @@ class DevServer {
206
208
 
207
209
  reloadAll() {
208
210
  console.log('📤 Sending reload message to', this.clients.size, 'clients');
209
-
211
+
210
212
  // Send a simple reload command to all clients
211
213
  const message = JSON.stringify({
212
214
  type: 'reload-current'
213
215
  });
214
-
216
+
215
217
  let sentCount = 0;
216
218
  this.clients.forEach(client => {
217
219
  if (client.readyState === 1) { // WebSocket.OPEN
@@ -219,7 +221,7 @@ class DevServer {
219
221
  sentCount++;
220
222
  }
221
223
  });
222
-
224
+
223
225
  console.log('✅ Reload message sent to', sentCount, 'clients');
224
226
  }
225
227
 
@@ -233,29 +235,38 @@ class DevServer {
233
235
  const setupWatcher = (directory, options, server, screenshotCapture) => {
234
236
  let debounceTimer = null;
235
237
  let pendingFiles = new Set();
236
-
238
+
237
239
  const processChanges = async () => {
238
240
  const files = [...pendingFiles];
239
241
  pendingFiles.clear();
240
-
242
+
241
243
  console.log('Rebuilding site...');
242
244
  try {
243
- await buildSite({ ...options, quiet: true });
244
- console.log('Rebuild complete');
245
+ // Always reload config on rebuild to pick up function changes
246
+ const config = await loadSiteConfig(options.rootDir, true, true);
245
247
 
248
+ const currentOptions = {
249
+ ...options,
250
+ mdRender: config.mdRender || options.mdRender,
251
+ functions: config.functions || options.functions || {}
252
+ };
253
+
254
+ await buildSite({ ...currentOptions, quiet: true });
255
+ console.log('Rebuild complete');
256
+
246
257
  // Just reload all clients - they'll reload their current page
247
258
  console.log('🔄 Reloading all connected clients');
248
259
  server.reloadAll();
249
-
260
+
250
261
  // If screenshots are enabled and pages were changed, capture screenshots
251
- const pageFiles = files.filter(file =>
262
+ const pageFiles = files.filter(file =>
252
263
  (file.includes('pages/') || file.startsWith('pages/')) &&
253
264
  (file.endsWith('.md') || file.endsWith('.yaml') || file.endsWith('.yml'))
254
265
  );
255
266
  if (screenshotCapture && pageFiles.length > 0) {
256
267
  // Wait a bit for the server to be ready with new content
257
268
  await new Promise(resolve => setTimeout(resolve, 1000));
258
-
269
+
259
270
  console.log('📸 Capturing screenshots for changed pages...');
260
271
  // Capture screenshots for changed pages
261
272
  for (const file of pageFiles) {
@@ -264,12 +275,12 @@ const setupWatcher = (directory, options, server, screenshotCapture) => {
264
275
  await screenshotCapture.capturePageScreenshot(file);
265
276
  }
266
277
  }
267
-
278
+
268
279
  } catch (error) {
269
280
  console.error('Error during rebuild:', error);
270
281
  }
271
282
  };
272
-
283
+
273
284
  watch(
274
285
  directory,
275
286
  { recursive: true },
@@ -279,35 +290,35 @@ const setupWatcher = (directory, options, server, screenshotCapture) => {
279
290
  if (filename.endsWith('~') || filename.startsWith('.') || filename.includes('/.')) {
280
291
  return;
281
292
  }
282
-
293
+
283
294
  // Skip _site directory if it somehow gets included
284
295
  if (filename.includes('_site/')) {
285
296
  return;
286
297
  }
287
-
298
+
288
299
  // For static directory, only rebuild for content files, not binary files
289
300
  const isStaticDir = directory.endsWith('/static') || directory.includes('/static/');
290
301
  if (isStaticDir) {
291
302
  const ext = path.extname(filename).toLowerCase();
292
303
  const binaryExts = ['.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.zip', '.woff', '.woff2', '.ttf', '.eot'];
293
-
304
+
294
305
  if (binaryExts.includes(ext)) {
295
306
  // For binary files in static, just copy them without full rebuild
296
307
  console.log(`📁 Static file changed: ${directory}/${filename} (skipping rebuild)`);
297
308
  return;
298
309
  }
299
310
  }
300
-
311
+
301
312
  console.log(`Detected ${event} in ${filename}`);
302
-
313
+
303
314
  // Add to pending files for rebuild
304
315
  const fullPath = path.join(directory, filename);
305
316
  pendingFiles.add(fullPath);
306
-
317
+
307
318
  if (debounceTimer) {
308
319
  clearTimeout(debounceTimer);
309
320
  }
310
-
321
+
311
322
  debounceTimer = setTimeout(processChanges, 10);
312
323
  }
313
324
  },
@@ -316,15 +327,38 @@ const setupWatcher = (directory, options, server, screenshotCapture) => {
316
327
 
317
328
  // Main watch function
318
329
  const watchSite = async (options = {}) => {
319
- const {
330
+ const {
320
331
  port = 3001,
321
- rootDir = '.',
332
+ rootDir = process.cwd(),
322
333
  screenshots = false
323
334
  } = options;
324
335
 
325
- // Do initial build
336
+ // Load config file
337
+ console.log(`📁 Current working directory: ${process.cwd()}`);
338
+ console.log(`📁 rootDir parameter: ${rootDir}`);
339
+ const config = await loadSiteConfig(rootDir, false);
340
+
341
+ if (Object.keys(config).length > 0) {
342
+ console.log('✅ Loaded sites.config.js');
343
+ if (config.mdRender) {
344
+ console.log('✅ Custom mdRender function found');
345
+ } else {
346
+ console.log('ℹ️ No custom mdRender function in config');
347
+ }
348
+ if (config.functions) {
349
+ console.log(`✅ Found ${Object.keys(config.functions).length} custom function(s)`);
350
+ }
351
+ } else {
352
+ console.log('ℹ️ No sites.config.js found, using defaults');
353
+ }
354
+
355
+ // Do initial build with config
326
356
  console.log('Starting initial build...');
327
- await buildSite({ rootDir });
357
+ await buildSite({
358
+ rootDir,
359
+ mdRender: config.mdRender,
360
+ functions: config.functions || {}
361
+ });
328
362
  console.log('Initial build complete');
329
363
 
330
364
  // Start custom dev server
@@ -341,12 +375,16 @@ const watchSite = async (options = {}) => {
341
375
 
342
376
  // Watch all relevant directories
343
377
  const dirsToWatch = ['data', 'templates', 'partials', 'pages'];
344
-
378
+
345
379
  dirsToWatch.forEach(dir => {
346
380
  const dirPath = path.join(rootDir, dir);
347
381
  if (existsSync(dirPath)) {
348
382
  console.log(`👁️ Watching: ${dir}/`);
349
- setupWatcher(dirPath, { rootDir }, server, screenshotCapture);
383
+ setupWatcher(dirPath, {
384
+ rootDir,
385
+ mdRender: config.mdRender,
386
+ functions: config.functions || {}
387
+ }, server, screenshotCapture);
350
388
  }
351
389
  });
352
390
 
@@ -359,7 +397,7 @@ const watchSite = async (options = {}) => {
359
397
  server.close();
360
398
  process.exit();
361
399
  });
362
-
400
+
363
401
  process.on('SIGTERM', async () => {
364
402
  if (screenshotCapture) {
365
403
  await screenshotCapture.close();
@@ -369,4 +407,4 @@ const watchSite = async (options = {}) => {
369
407
  });
370
408
  };
371
409
 
372
- export default watchSite;
410
+ export default watchSite;
@@ -120,7 +120,7 @@ export function createSiteBuilder({ fs, rootDir = '.', mdRender, functions = {},
120
120
  } else if (item.isFile() && (item.name.endsWith('.yaml') || item.name.endsWith('.md'))) {
121
121
  // Extract frontmatter
122
122
  const frontmatter = extractFrontmatter(itemPath);
123
-
123
+
124
124
  // Calculate URL
125
125
  const outputFileName = item.name.replace(/\.(yaml|md)$/, '.html');
126
126
  const outputRelativePath = basePath ? path.join(basePath, outputFileName) : outputFileName;
@@ -130,7 +130,7 @@ export function createSiteBuilder({ fs, rootDir = '.', mdRender, functions = {},
130
130
  if (frontmatter.tags) {
131
131
  // Normalize tags to array
132
132
  const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : [frontmatter.tags];
133
-
133
+
134
134
  // Add to collections
135
135
  tags.forEach(tag => {
136
136
  if (typeof tag === 'string' && tag.trim()) {
@@ -296,25 +296,25 @@ export function createSiteBuilder({ fs, rootDir = '.', mdRender, functions = {},
296
296
  function copyStaticFiles() {
297
297
  const staticDir = path.join(rootDir, 'static');
298
298
  const outputDir = path.join(rootDir, '_site');
299
-
299
+
300
300
  if (!fs.existsSync(staticDir)) {
301
301
  return;
302
302
  }
303
-
303
+
304
304
  // Ensure output directory exists
305
305
  if (!fs.existsSync(outputDir)) {
306
306
  fs.mkdirSync(outputDir, { recursive: true });
307
307
  }
308
-
308
+
309
309
  function copyRecursive(src, dest) {
310
310
  const stats = fs.statSync(src);
311
-
311
+
312
312
  if (stats.isDirectory()) {
313
313
  // Create directory if it doesn't exist
314
314
  if (!fs.existsSync(dest)) {
315
315
  fs.mkdirSync(dest, { recursive: true });
316
316
  }
317
-
317
+
318
318
  // Copy all items in directory
319
319
  const items = fs.readdirSync(src);
320
320
  items.forEach(item => {
@@ -326,7 +326,7 @@ export function createSiteBuilder({ fs, rootDir = '.', mdRender, functions = {},
326
326
  if (!quiet) console.log(` -> Copied ${src} to ${dest}`);
327
327
  }
328
328
  }
329
-
329
+
330
330
  if (!quiet) console.log('Copying static files...');
331
331
  const items = fs.readdirSync(staticDir);
332
332
  items.forEach(item => {
@@ -338,13 +338,13 @@ export function createSiteBuilder({ fs, rootDir = '.', mdRender, functions = {},
338
338
 
339
339
  // Start build process
340
340
  if (!quiet) console.log('Starting build process...');
341
-
341
+
342
342
  // Copy static files first (they can be overwritten by pages)
343
343
  copyStaticFiles();
344
-
344
+
345
345
  // Process all pages (can overwrite static files)
346
346
  processAllPages('');
347
-
347
+
348
348
  if (!quiet) console.log('Build complete!');
349
349
  };
350
350
  }
@@ -0,0 +1,36 @@
1
+ import path from 'path';
2
+ import { pathToFileURL } from 'url';
3
+
4
+ /**
5
+ * Load the sites.config.js file from a given directory
6
+ * @param {string} rootDir - The root directory to look for sites.config.js
7
+ * @param {boolean} throwOnError - Whether to throw on errors other than file not found
8
+ * @param {boolean} bustCache - Whether to bypass module cache (for reloading)
9
+ * @returns {Promise<Object>} The loaded config object or empty object if not found
10
+ */
11
+ export async function loadSiteConfig(rootDir, throwOnError = true, bustCache = false) {
12
+ try {
13
+ const configPath = path.join(rootDir, 'sites.config.js');
14
+ let importUrl = pathToFileURL(configPath).href;
15
+
16
+ // Add timestamp to force reload if cache busting is requested
17
+ if (bustCache) {
18
+ importUrl += `?t=${Date.now()}`;
19
+ }
20
+
21
+ const configModule = await import(importUrl);
22
+ return configModule.default || {};
23
+ } catch (e) {
24
+ // Only ignore file not found errors
25
+ if (e.code === 'ENOENT') {
26
+ // Config file is optional, return empty config
27
+ return {};
28
+ } else if (throwOnError) {
29
+ // Re-throw any other errors (syntax errors, module not found, etc.)
30
+ throw e;
31
+ } else {
32
+ console.error('Error loading sites.config.js:', e);
33
+ return {};
34
+ }
35
+ }
36
+ }