@terrymooreii/sia 1.0.1 ā 2.0.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/_config.yml +33 -0
- package/bin/cli.js +51 -0
- package/defaults/includes/footer.njk +14 -0
- package/defaults/includes/header.njk +71 -0
- package/defaults/includes/pagination.njk +26 -0
- package/defaults/includes/tag-list.njk +11 -0
- package/defaults/layouts/base.njk +41 -0
- package/defaults/layouts/note.njk +25 -0
- package/defaults/layouts/page.njk +14 -0
- package/defaults/layouts/post.njk +43 -0
- package/defaults/pages/blog.njk +36 -0
- package/defaults/pages/feed.njk +28 -0
- package/defaults/pages/index.njk +60 -0
- package/defaults/pages/notes.njk +34 -0
- package/defaults/pages/tag.njk +41 -0
- package/defaults/pages/tags.njk +39 -0
- package/defaults/styles/main.css +1074 -0
- package/lib/assets.js +234 -0
- package/lib/build.js +260 -19
- package/lib/collections.js +191 -0
- package/lib/config.js +114 -0
- package/lib/content.js +323 -0
- package/lib/index.js +53 -18
- package/lib/init.js +555 -6
- package/lib/new.js +379 -41
- package/lib/server.js +257 -0
- package/lib/templates.js +249 -0
- package/package.json +30 -15
- package/readme.md +216 -52
- package/src/images/.gitkeep +3 -0
- package/src/notes/2024-12-17-first-note.md +6 -0
- package/src/pages/about.md +29 -0
- package/src/posts/2024-12-16-markdown-features.md +76 -0
- package/src/posts/2024-12-17-welcome-to-sia.md +78 -0
- package/src/posts/2024-12-17-welcome-to-static-forge.md +78 -0
- package/.github/workflows/main.yml +0 -33
- package/.prettierignore +0 -3
- package/.prettierrc +0 -8
- package/lib/helpers.js +0 -37
- package/lib/markdown.js +0 -33
- package/lib/parse.js +0 -94
- package/lib/readconfig.js +0 -16
- package/lib/rss.js +0 -63
- package/templates/siarc-template.js +0 -53
- package/templates/src/_partials/_footer.njk +0 -1
- package/templates/src/_partials/_head.njk +0 -35
- package/templates/src/_partials/_header.njk +0 -1
- package/templates/src/_partials/_layout.njk +0 -12
- package/templates/src/_partials/_nav.njk +0 -12
- package/templates/src/_partials/page.njk +0 -5
- package/templates/src/_partials/post.njk +0 -13
- package/templates/src/_partials/posts.njk +0 -19
- package/templates/src/assets/android-chrome-192x192.png +0 -0
- package/templates/src/assets/android-chrome-512x512.png +0 -0
- package/templates/src/assets/apple-touch-icon.png +0 -0
- package/templates/src/assets/favicon-16x16.png +0 -0
- package/templates/src/assets/favicon-32x32.png +0 -0
- package/templates/src/assets/favicon.ico +0 -0
- package/templates/src/assets/site.webmanifest +0 -19
- package/templates/src/content/index.md +0 -7
- package/templates/src/css/markdown.css +0 -1210
- package/templates/src/css/theme.css +0 -120
package/lib/new.js
CHANGED
|
@@ -1,46 +1,384 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import slugify from '
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
import { slugify } from './content.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get current date in YYYY-MM-DD format
|
|
9
|
+
*/
|
|
10
|
+
function getDateString() {
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const year = now.getFullYear();
|
|
13
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
14
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
15
|
+
return `${year}-${month}-${day}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get current ISO date string
|
|
20
|
+
*/
|
|
21
|
+
function getISODate() {
|
|
22
|
+
return new Date().toISOString();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse tags string into array
|
|
27
|
+
*/
|
|
28
|
+
function parseTags(tagsInput) {
|
|
29
|
+
if (!tagsInput || tagsInput.trim() === '') {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
return tagsInput
|
|
33
|
+
.split(',')
|
|
34
|
+
.map(tag => tag.trim())
|
|
35
|
+
.filter(tag => tag.length > 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format tags for YAML
|
|
40
|
+
*/
|
|
41
|
+
function formatTags(tags) {
|
|
42
|
+
if (!tags || tags.length === 0) {
|
|
43
|
+
return '[]';
|
|
44
|
+
}
|
|
45
|
+
return `[${tags.join(', ')}]`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a new post
|
|
50
|
+
*/
|
|
51
|
+
function createPost(config, options) {
|
|
52
|
+
const slug = slugify(options.title);
|
|
53
|
+
const date = getDateString();
|
|
54
|
+
const filename = `${date}-${slug}.md`;
|
|
55
|
+
const postsDir = join(config.inputDir, config.collections.posts?.path || 'posts');
|
|
56
|
+
const filePath = join(postsDir, filename);
|
|
57
|
+
|
|
58
|
+
// Ensure directory exists
|
|
59
|
+
if (!existsSync(postsDir)) {
|
|
60
|
+
mkdirSync(postsDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let frontMatter = `---
|
|
64
|
+
title: "${options.title}"
|
|
65
|
+
date: ${getISODate()}
|
|
66
|
+
tags: ${formatTags(options.tags)}`;
|
|
67
|
+
|
|
68
|
+
if (options.excerpt) {
|
|
69
|
+
frontMatter += `\nexcerpt: "${options.excerpt}"`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options.draft) {
|
|
73
|
+
frontMatter += `\ndraft: true`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
frontMatter += `\n---
|
|
77
|
+
|
|
78
|
+
${options.content || 'Write your post content here...'}
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
writeFileSync(filePath, frontMatter, 'utf-8');
|
|
82
|
+
return filePath;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a new page
|
|
87
|
+
*/
|
|
88
|
+
function createPage(config, options) {
|
|
89
|
+
const slug = slugify(options.title);
|
|
90
|
+
const filename = `${slug}.md`;
|
|
91
|
+
const pagesDir = join(config.inputDir, config.collections.pages?.path || 'pages');
|
|
92
|
+
const filePath = join(pagesDir, filename);
|
|
93
|
+
|
|
94
|
+
// Ensure directory exists
|
|
95
|
+
if (!existsSync(pagesDir)) {
|
|
96
|
+
mkdirSync(pagesDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let frontMatter = `---
|
|
100
|
+
title: "${options.title}"
|
|
101
|
+
layout: page`;
|
|
102
|
+
|
|
103
|
+
if (options.permalink) {
|
|
104
|
+
frontMatter += `\npermalink: ${options.permalink}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
frontMatter += `\n---
|
|
108
|
+
|
|
109
|
+
${options.content || 'Write your page content here...'}
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
writeFileSync(filePath, frontMatter, 'utf-8');
|
|
113
|
+
return filePath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create a new note (short post)
|
|
118
|
+
*/
|
|
119
|
+
function createNote(config, options) {
|
|
120
|
+
const slug = options.title ? slugify(options.title) : 'note';
|
|
121
|
+
const date = getDateString();
|
|
122
|
+
const timestamp = Date.now();
|
|
123
|
+
const filename = `${date}-${slug}-${timestamp}.md`;
|
|
124
|
+
const notesDir = join(config.inputDir, config.collections.notes?.path || 'notes');
|
|
125
|
+
const filePath = join(notesDir, filename);
|
|
126
|
+
|
|
127
|
+
// Ensure directory exists
|
|
128
|
+
if (!existsSync(notesDir)) {
|
|
129
|
+
mkdirSync(notesDir, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const content = `---
|
|
133
|
+
date: ${getISODate()}
|
|
134
|
+
tags: ${formatTags(options.tags)}
|
|
37
135
|
---
|
|
38
136
|
|
|
39
|
-
|
|
137
|
+
${options.content || options.title || 'Write your note here...'}
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
141
|
+
return filePath;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Prompt for post options
|
|
146
|
+
*/
|
|
147
|
+
async function promptForPost(initialTitle) {
|
|
148
|
+
const answers = await prompts([
|
|
149
|
+
{
|
|
150
|
+
type: 'text',
|
|
151
|
+
name: 'title',
|
|
152
|
+
message: 'Post title:',
|
|
153
|
+
initial: initialTitle || '',
|
|
154
|
+
validate: value => value.length > 0 ? true : 'Title is required'
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: 'text',
|
|
158
|
+
name: 'tags',
|
|
159
|
+
message: 'Tags (comma-separated):',
|
|
160
|
+
initial: ''
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: 'text',
|
|
164
|
+
name: 'excerpt',
|
|
165
|
+
message: 'Excerpt (optional):',
|
|
166
|
+
initial: ''
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
type: 'confirm',
|
|
170
|
+
name: 'draft',
|
|
171
|
+
message: 'Save as draft?',
|
|
172
|
+
initial: true
|
|
173
|
+
}
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
if (!answers.title) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
title: answers.title,
|
|
182
|
+
tags: parseTags(answers.tags),
|
|
183
|
+
excerpt: answers.excerpt || null,
|
|
184
|
+
draft: answers.draft
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Prompt for page options
|
|
190
|
+
*/
|
|
191
|
+
async function promptForPage(initialTitle) {
|
|
192
|
+
const answers = await prompts([
|
|
193
|
+
{
|
|
194
|
+
type: 'text',
|
|
195
|
+
name: 'title',
|
|
196
|
+
message: 'Page title:',
|
|
197
|
+
initial: initialTitle || '',
|
|
198
|
+
validate: value => value.length > 0 ? true : 'Title is required'
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
type: 'text',
|
|
202
|
+
name: 'permalink',
|
|
203
|
+
message: 'Custom permalink (optional, e.g., /about/):',
|
|
204
|
+
initial: ''
|
|
205
|
+
}
|
|
206
|
+
]);
|
|
40
207
|
|
|
41
|
-
|
|
208
|
+
if (!answers.title) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
42
211
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
212
|
+
return {
|
|
213
|
+
title: answers.title,
|
|
214
|
+
permalink: answers.permalink || null
|
|
215
|
+
};
|
|
46
216
|
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Prompt for note options
|
|
220
|
+
*/
|
|
221
|
+
async function promptForNote(initialContent) {
|
|
222
|
+
const answers = await prompts([
|
|
223
|
+
{
|
|
224
|
+
type: 'text',
|
|
225
|
+
name: 'content',
|
|
226
|
+
message: 'Note content:',
|
|
227
|
+
initial: initialContent || '',
|
|
228
|
+
validate: value => value.length > 0 ? true : 'Content is required'
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
type: 'text',
|
|
232
|
+
name: 'tags',
|
|
233
|
+
message: 'Tags (comma-separated):',
|
|
234
|
+
initial: ''
|
|
235
|
+
}
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
if (!answers.content) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
title: answers.content.substring(0, 50),
|
|
244
|
+
content: answers.content,
|
|
245
|
+
tags: parseTags(answers.tags)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Prompt for content type
|
|
251
|
+
*/
|
|
252
|
+
async function promptForType() {
|
|
253
|
+
const answer = await prompts({
|
|
254
|
+
type: 'select',
|
|
255
|
+
name: 'type',
|
|
256
|
+
message: 'What would you like to create?',
|
|
257
|
+
choices: [
|
|
258
|
+
{ title: 'Post', description: 'A blog post with title, date, and tags', value: 'post' },
|
|
259
|
+
{ title: 'Page', description: 'A static page (e.g., About, Contact)', value: 'page' },
|
|
260
|
+
{ title: 'Note', description: 'A short note or quick thought', value: 'note' }
|
|
261
|
+
],
|
|
262
|
+
initial: 0
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return answer.type;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* New command handler for CLI
|
|
270
|
+
*/
|
|
271
|
+
export async function newCommand(type, title, cmdOptions = {}) {
|
|
272
|
+
const config = loadConfig(process.cwd());
|
|
273
|
+
|
|
274
|
+
// Quick mode - skip prompts if type and title provided
|
|
275
|
+
const quickMode = cmdOptions.quick || false;
|
|
276
|
+
|
|
277
|
+
// If quick mode with all required args, skip prompts
|
|
278
|
+
if (quickMode && type && title) {
|
|
279
|
+
let filePath;
|
|
280
|
+
const tags = cmdOptions.tags ? parseTags(cmdOptions.tags) : [];
|
|
281
|
+
|
|
282
|
+
switch (type.toLowerCase()) {
|
|
283
|
+
case 'post':
|
|
284
|
+
filePath = createPost(config, {
|
|
285
|
+
title,
|
|
286
|
+
tags,
|
|
287
|
+
draft: cmdOptions.draft !== false,
|
|
288
|
+
excerpt: null
|
|
289
|
+
});
|
|
290
|
+
console.log(`\n⨠Created new post: ${filePath}`);
|
|
291
|
+
if (cmdOptions.draft !== false) {
|
|
292
|
+
console.log(' š Saved as draft - remove "draft: true" when ready to publish.');
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case 'page':
|
|
297
|
+
filePath = createPage(config, { title, permalink: null });
|
|
298
|
+
console.log(`\n⨠Created new page: ${filePath}`);
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case 'note':
|
|
302
|
+
filePath = createNote(config, { title, content: title, tags });
|
|
303
|
+
console.log(`\n⨠Created new note: ${filePath}`);
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
default:
|
|
307
|
+
console.error(`\nā Unknown content type: ${type}`);
|
|
308
|
+
console.log(' Available types: post, page, note\n');
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
console.log('');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log('\nš Create new content\n');
|
|
316
|
+
|
|
317
|
+
// If no type provided, prompt for it
|
|
318
|
+
let contentType = type;
|
|
319
|
+
if (!contentType) {
|
|
320
|
+
contentType = await promptForType();
|
|
321
|
+
if (!contentType) {
|
|
322
|
+
console.log('\nā Cancelled.\n');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let filePath;
|
|
328
|
+
let options;
|
|
329
|
+
|
|
330
|
+
switch (contentType.toLowerCase()) {
|
|
331
|
+
case 'post':
|
|
332
|
+
options = await promptForPost(title);
|
|
333
|
+
if (!options) {
|
|
334
|
+
console.log('\nā Cancelled.\n');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Apply command line options if provided
|
|
338
|
+
if (cmdOptions.tags) {
|
|
339
|
+
options.tags = [...options.tags, ...parseTags(cmdOptions.tags)];
|
|
340
|
+
}
|
|
341
|
+
if (cmdOptions.draft !== undefined) {
|
|
342
|
+
options.draft = cmdOptions.draft;
|
|
343
|
+
}
|
|
344
|
+
filePath = createPost(config, options);
|
|
345
|
+
console.log(`\n⨠Created new post: ${filePath}`);
|
|
346
|
+
if (options.draft) {
|
|
347
|
+
console.log(' š Saved as draft - remove "draft: true" when ready to publish.');
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
|
|
351
|
+
case 'page':
|
|
352
|
+
options = await promptForPage(title);
|
|
353
|
+
if (!options) {
|
|
354
|
+
console.log('\nā Cancelled.\n');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
filePath = createPage(config, options);
|
|
358
|
+
console.log(`\n⨠Created new page: ${filePath}`);
|
|
359
|
+
break;
|
|
360
|
+
|
|
361
|
+
case 'note':
|
|
362
|
+
options = await promptForNote(title);
|
|
363
|
+
if (!options) {
|
|
364
|
+
console.log('\nā Cancelled.\n');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// Apply command line options if provided
|
|
368
|
+
if (cmdOptions.tags) {
|
|
369
|
+
options.tags = [...(options.tags || []), ...parseTags(cmdOptions.tags)];
|
|
370
|
+
}
|
|
371
|
+
filePath = createNote(config, options);
|
|
372
|
+
console.log(`\n⨠Created new note: ${filePath}`);
|
|
373
|
+
break;
|
|
374
|
+
|
|
375
|
+
default:
|
|
376
|
+
console.error(`\nā Unknown content type: ${contentType}`);
|
|
377
|
+
console.log(' Available types: post, page, note\n');
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log('');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export default { newCommand };
|
package/lib/server.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
|
+
import { join, extname } from 'path';
|
|
4
|
+
import { WebSocketServer } from 'ws';
|
|
5
|
+
import chokidar from 'chokidar';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
7
|
+
import { build } from './build.js';
|
|
8
|
+
|
|
9
|
+
// MIME types for common file extensions
|
|
10
|
+
const MIME_TYPES = {
|
|
11
|
+
'.html': 'text/html',
|
|
12
|
+
'.css': 'text/css',
|
|
13
|
+
'.js': 'application/javascript',
|
|
14
|
+
'.json': 'application/json',
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
'.jpg': 'image/jpeg',
|
|
17
|
+
'.jpeg': 'image/jpeg',
|
|
18
|
+
'.gif': 'image/gif',
|
|
19
|
+
'.svg': 'image/svg+xml',
|
|
20
|
+
'.webp': 'image/webp',
|
|
21
|
+
'.ico': 'image/x-icon',
|
|
22
|
+
'.woff': 'font/woff',
|
|
23
|
+
'.woff2': 'font/woff2',
|
|
24
|
+
'.ttf': 'font/ttf',
|
|
25
|
+
'.xml': 'application/xml',
|
|
26
|
+
'.txt': 'text/plain',
|
|
27
|
+
'.pdf': 'application/pdf'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Live reload script to inject into HTML pages
|
|
31
|
+
const LIVE_RELOAD_SCRIPT = `
|
|
32
|
+
<script>
|
|
33
|
+
(function() {
|
|
34
|
+
const ws = new WebSocket('ws://' + window.location.hostname + ':{{WS_PORT}}');
|
|
35
|
+
ws.onmessage = function(event) {
|
|
36
|
+
if (event.data === 'reload') {
|
|
37
|
+
console.log('[Sia] Reloading...');
|
|
38
|
+
window.location.reload();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
ws.onopen = function() {
|
|
42
|
+
console.log('[Sia] Live reload connected');
|
|
43
|
+
};
|
|
44
|
+
ws.onclose = function() {
|
|
45
|
+
console.log('[Sia] Live reload disconnected');
|
|
46
|
+
// Try to reconnect
|
|
47
|
+
setTimeout(function() {
|
|
48
|
+
window.location.reload();
|
|
49
|
+
}, 1000);
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
</script>
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Inject live reload script into HTML
|
|
57
|
+
*/
|
|
58
|
+
function injectLiveReload(html, wsPort) {
|
|
59
|
+
const script = LIVE_RELOAD_SCRIPT.replace('{{WS_PORT}}', wsPort);
|
|
60
|
+
return html.replace('</body>', script + '</body>');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create the HTTP server
|
|
65
|
+
*/
|
|
66
|
+
function createHttpServer(config, wsPort) {
|
|
67
|
+
const server = createServer((req, res) => {
|
|
68
|
+
// Parse URL
|
|
69
|
+
let urlPath = req.url.split('?')[0];
|
|
70
|
+
|
|
71
|
+
// Handle root and trailing slashes
|
|
72
|
+
if (urlPath.endsWith('/')) {
|
|
73
|
+
urlPath += 'index.html';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Build the file path
|
|
77
|
+
let filePath = join(config.outputDir, urlPath);
|
|
78
|
+
|
|
79
|
+
// Check if file exists, try adding .html
|
|
80
|
+
if (!existsSync(filePath)) {
|
|
81
|
+
const htmlPath = filePath + '.html';
|
|
82
|
+
if (existsSync(htmlPath)) {
|
|
83
|
+
filePath = htmlPath;
|
|
84
|
+
} else {
|
|
85
|
+
// Try index.html in directory
|
|
86
|
+
const indexPath = join(filePath, 'index.html');
|
|
87
|
+
if (existsSync(indexPath)) {
|
|
88
|
+
filePath = indexPath;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 404 if file still doesn't exist
|
|
94
|
+
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
|
|
95
|
+
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
96
|
+
res.end('<h1>404 - Not Found</h1>');
|
|
97
|
+
console.log(` 404 ${req.url}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get MIME type
|
|
102
|
+
const ext = extname(filePath).toLowerCase();
|
|
103
|
+
const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
104
|
+
|
|
105
|
+
// Read and serve file
|
|
106
|
+
try {
|
|
107
|
+
let content = readFileSync(filePath);
|
|
108
|
+
|
|
109
|
+
// Inject live reload script into HTML files
|
|
110
|
+
if (ext === '.html') {
|
|
111
|
+
content = injectLiveReload(content.toString(), wsPort);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
res.writeHead(200, { 'Content-Type': mimeType });
|
|
115
|
+
res.end(content);
|
|
116
|
+
console.log(` 200 ${req.url}`);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
119
|
+
res.end('<h1>500 - Internal Server Error</h1>');
|
|
120
|
+
console.error(` 500 ${req.url}: ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return server;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create WebSocket server for live reload
|
|
129
|
+
*/
|
|
130
|
+
function createWsServer(port) {
|
|
131
|
+
const wss = new WebSocketServer({ port });
|
|
132
|
+
|
|
133
|
+
wss.on('connection', (ws) => {
|
|
134
|
+
console.log('š Live reload client connected');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return wss;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Notify all connected clients to reload
|
|
142
|
+
*/
|
|
143
|
+
function notifyReload(wss) {
|
|
144
|
+
wss.clients.forEach((client) => {
|
|
145
|
+
if (client.readyState === 1) { // WebSocket.OPEN
|
|
146
|
+
client.send('reload');
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Set up file watcher
|
|
153
|
+
*/
|
|
154
|
+
function setupWatcher(config, wss) {
|
|
155
|
+
// Watch source files and config
|
|
156
|
+
const watchPaths = [
|
|
157
|
+
config.inputDir,
|
|
158
|
+
config.layoutsDir,
|
|
159
|
+
config.includesDir,
|
|
160
|
+
join(config.rootDir, '_config.yml'),
|
|
161
|
+
join(config.rootDir, '_config.json'),
|
|
162
|
+
join(config.rootDir, 'styles')
|
|
163
|
+
].filter(p => existsSync(p));
|
|
164
|
+
|
|
165
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
166
|
+
ignored: /(^|[\/\\])\../, // Ignore dot files
|
|
167
|
+
persistent: true,
|
|
168
|
+
ignoreInitial: true
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
let rebuildTimeout = null;
|
|
172
|
+
|
|
173
|
+
const triggerRebuild = async (event, path) => {
|
|
174
|
+
// Debounce rapid changes
|
|
175
|
+
if (rebuildTimeout) {
|
|
176
|
+
clearTimeout(rebuildTimeout);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
rebuildTimeout = setTimeout(async () => {
|
|
180
|
+
console.log(`\nš ${event}: ${path}`);
|
|
181
|
+
console.log('š Rebuilding...\n');
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await build({ clean: false, rootDir: config.rootDir });
|
|
185
|
+
notifyReload(wss);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error('ā Rebuild failed:', err.message);
|
|
188
|
+
}
|
|
189
|
+
}, 100);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
watcher.on('change', (path) => triggerRebuild('Changed', path));
|
|
193
|
+
watcher.on('add', (path) => triggerRebuild('Added', path));
|
|
194
|
+
watcher.on('unlink', (path) => triggerRebuild('Removed', path));
|
|
195
|
+
|
|
196
|
+
return watcher;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Start the development server
|
|
201
|
+
*/
|
|
202
|
+
export async function startServer(options = {}) {
|
|
203
|
+
const config = loadConfig(options.rootDir || process.cwd());
|
|
204
|
+
const httpPort = parseInt(options.port) || config.server.port || 3000;
|
|
205
|
+
const wsPort = httpPort + 1;
|
|
206
|
+
|
|
207
|
+
console.log('\nā” Sia - Development Server\n');
|
|
208
|
+
|
|
209
|
+
// Initial build
|
|
210
|
+
await build({ clean: true, rootDir: config.rootDir });
|
|
211
|
+
|
|
212
|
+
// Create servers
|
|
213
|
+
const httpServer = createHttpServer(config, wsPort);
|
|
214
|
+
const wss = createWsServer(wsPort);
|
|
215
|
+
|
|
216
|
+
// Start HTTP server
|
|
217
|
+
httpServer.listen(httpPort, () => {
|
|
218
|
+
console.log(`\nš Server running at http://localhost:${httpPort}`);
|
|
219
|
+
console.log(`š Live reload on ws://localhost:${wsPort}`);
|
|
220
|
+
console.log('\nš Watching for changes...\n');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Set up file watcher
|
|
224
|
+
const watcher = setupWatcher(config, wss);
|
|
225
|
+
|
|
226
|
+
// Handle shutdown
|
|
227
|
+
const shutdown = () => {
|
|
228
|
+
console.log('\n\nš Shutting down...');
|
|
229
|
+
watcher.close();
|
|
230
|
+
wss.close();
|
|
231
|
+
httpServer.close();
|
|
232
|
+
process.exit(0);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
process.on('SIGINT', shutdown);
|
|
236
|
+
process.on('SIGTERM', shutdown);
|
|
237
|
+
|
|
238
|
+
return { httpServer, wss, watcher };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Dev command handler for CLI
|
|
243
|
+
*/
|
|
244
|
+
export async function devCommand(options) {
|
|
245
|
+
try {
|
|
246
|
+
await startServer({
|
|
247
|
+
port: options.port,
|
|
248
|
+
rootDir: process.cwd()
|
|
249
|
+
});
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.error('ā Server failed to start:', err.message);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export default { startServer, devCommand };
|
|
257
|
+
|