@daz4126/swifty 2.11.0 → 2.12.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/README.md +14 -3
- package/package.json +3 -2
- package/src/assets.js +86 -46
- package/src/build.js +57 -1
- package/src/cli.js +51 -7
- package/src/concurrency.js +25 -0
- package/src/config.js +22 -2
- package/src/data.js +70 -0
- package/src/init.js +36 -33
- package/src/layout.js +3 -3
- package/src/pages.js +231 -119
- package/src/pagination.js +58 -0
- package/src/partials.js +236 -42
- package/src/rss.js +6 -4
- package/src/sitemap.js +89 -0
- package/src/watcher.js +36 -9
package/src/partials.js
CHANGED
|
@@ -1,76 +1,270 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import fsExtra from "fs-extra";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { dirs } from "./config.js";
|
|
4
|
+
import { dirs, defaultConfig } from "./config.js";
|
|
5
5
|
import { marked } from "marked";
|
|
6
|
+
import { Eta } from "eta";
|
|
7
|
+
import { loadData } from "./data.js";
|
|
6
8
|
|
|
7
9
|
const partialCache = new Map();
|
|
10
|
+
const imageExtensionRegex = /\.(png|jpe?g|webp)(?=([?#]|$))/i;
|
|
11
|
+
const rewriteableImageTags = new Set(["a", "img", "source"]);
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
// Helper to escape HTML attribute values
|
|
14
|
+
const escapeAttr = (str) => {
|
|
15
|
+
if (!str) return '';
|
|
16
|
+
return String(str)
|
|
17
|
+
.replace(/&/g, '&')
|
|
18
|
+
.replace(/"/g, '"')
|
|
19
|
+
.replace(/</g, '<')
|
|
20
|
+
.replace(/>/g, '>');
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Count words in content (strips HTML and markdown)
|
|
24
|
+
const countWords = (content) => {
|
|
25
|
+
if (!content) return 0;
|
|
26
|
+
// Strip HTML tags
|
|
27
|
+
let text = content.replace(/<[^>]*>/g, ' ');
|
|
28
|
+
// Strip markdown syntax (links, images, emphasis, etc.)
|
|
29
|
+
text = text.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1'); // links/images
|
|
30
|
+
text = text.replace(/[*_~`#]+/g, ''); // emphasis, headers
|
|
31
|
+
text = text.replace(/\n/g, ' ');
|
|
32
|
+
// Split on whitespace and filter empty strings
|
|
33
|
+
const words = text.split(/\s+/).filter(word => word.length > 0);
|
|
34
|
+
return words.length;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Calculate reading time using config value or default 200 words per minute
|
|
38
|
+
const calculateReadingTime = (wordCount, wordsPerMinute = defaultConfig.words_per_minute || 200) => {
|
|
39
|
+
const minutes = Math.ceil(wordCount / wordsPerMinute);
|
|
40
|
+
return minutes === 1 ? '1 min read' : `${minutes} min read`;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Generate Open Graph meta tags from page data
|
|
44
|
+
const generateOgTags = (values) => {
|
|
45
|
+
const meta = values.meta || {};
|
|
46
|
+
const tags = [];
|
|
47
|
+
|
|
48
|
+
// Basic OG tags
|
|
49
|
+
const title = meta.title || values.title || '';
|
|
50
|
+
const sitename = meta.sitename || values.sitename || '';
|
|
51
|
+
const description = meta.description || meta.summary || '';
|
|
52
|
+
const url = values.url || '';
|
|
53
|
+
const siteUrl = (meta.site_url || values.site_url || '').replace(/\/$/, '');
|
|
54
|
+
const image = meta.image || meta.og_image || '';
|
|
55
|
+
const type = meta.og_type || (values.folder ? 'website' : 'article');
|
|
56
|
+
const author = meta.author || values.author || '';
|
|
57
|
+
|
|
58
|
+
// Open Graph tags
|
|
59
|
+
if (title) tags.push(`<meta property="og:title" content="${escapeAttr(title)}">`);
|
|
60
|
+
if (sitename) tags.push(`<meta property="og:site_name" content="${escapeAttr(sitename)}">`);
|
|
61
|
+
if (siteUrl && url) tags.push(`<meta property="og:url" content="${escapeAttr(siteUrl + url)}">`);
|
|
62
|
+
tags.push(`<meta property="og:type" content="${escapeAttr(type)}">`);
|
|
63
|
+
if (description) tags.push(`<meta property="og:description" content="${escapeAttr(description)}">`);
|
|
64
|
+
if (image) {
|
|
65
|
+
const imageUrl = image.startsWith('http') ? image : siteUrl + image;
|
|
66
|
+
tags.push(`<meta property="og:image" content="${escapeAttr(imageUrl)}">`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Twitter Card tags
|
|
70
|
+
tags.push(`<meta name="twitter:card" content="${image ? 'summary_large_image' : 'summary'}">`);
|
|
71
|
+
if (title) tags.push(`<meta name="twitter:title" content="${escapeAttr(title)}">`);
|
|
72
|
+
if (description) tags.push(`<meta name="twitter:description" content="${escapeAttr(description)}">`);
|
|
73
|
+
if (image) {
|
|
74
|
+
const imageUrl = image.startsWith('http') ? image : siteUrl + image;
|
|
75
|
+
tags.push(`<meta name="twitter:image" content="${escapeAttr(imageUrl)}">`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Article-specific tags
|
|
79
|
+
if (type === 'article') {
|
|
80
|
+
if (author) tags.push(`<meta property="article:author" content="${escapeAttr(author)}">`);
|
|
81
|
+
if (values.dateObj) {
|
|
82
|
+
tags.push(`<meta property="article:published_time" content="${values.dateObj.toISOString()}">`);
|
|
83
|
+
}
|
|
84
|
+
if (meta.tags && Array.isArray(meta.tags)) {
|
|
85
|
+
meta.tags.forEach(tag => {
|
|
86
|
+
tags.push(`<meta property="article:tag" content="${escapeAttr(tag)}">`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return tags.join('\n ');
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Configure Eta with useWith for cleaner variable access
|
|
95
|
+
const eta = new Eta({
|
|
96
|
+
views: dirs.partials,
|
|
97
|
+
autoEscape: false, // All output is raw (no HTML escaping)
|
|
98
|
+
autoTrim: false,
|
|
99
|
+
useWith: true, // Allows direct variable access without 'it.' prefix
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const loadPartialData = async (partialName) => {
|
|
10
103
|
if (partialCache.has(partialName)) {
|
|
11
104
|
return partialCache.get(partialName);
|
|
12
105
|
}
|
|
13
106
|
|
|
14
|
-
const
|
|
15
|
-
|
|
107
|
+
const partialFiles = [
|
|
108
|
+
{ path: path.join(dirs.partials, `${partialName}.md`), raw: false },
|
|
109
|
+
{ path: path.join(dirs.partials, `${partialName}.html`), raw: true },
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
for (const partialFile of partialFiles) {
|
|
113
|
+
const partialPath = partialFile.path;
|
|
114
|
+
if (!(await fsExtra.pathExists(partialPath))) continue;
|
|
115
|
+
|
|
16
116
|
const partialContent = await fs.readFile(partialPath, "utf-8");
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
console.warn(`Include "${partialName}" not found.`);
|
|
21
|
-
return `<p>Include "${partialName}" not found.</p>`;
|
|
117
|
+
const partialData = { content: partialContent, raw: partialFile.raw };
|
|
118
|
+
partialCache.set(partialName, partialData);
|
|
119
|
+
return partialData;
|
|
22
120
|
}
|
|
121
|
+
|
|
122
|
+
console.warn(`Include "${partialName}" not found.`);
|
|
123
|
+
return { content: `<p>Include "${partialName}" not found.</p>`, raw: true };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const loadPartial = async (partialName) => {
|
|
127
|
+
const partialData = await loadPartialData(partialName);
|
|
128
|
+
return partialData.content;
|
|
23
129
|
};
|
|
24
130
|
|
|
131
|
+
const shouldRewriteImageUrl = (url) => {
|
|
132
|
+
if (!url || !url.startsWith("/images/")) return false;
|
|
133
|
+
return imageExtensionRegex.test(url);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const rewriteImageUrl = (url) => {
|
|
137
|
+
if (!shouldRewriteImageUrl(url)) return url;
|
|
138
|
+
return url.replace(imageExtensionRegex, ".webp");
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const rewriteSrcset = (srcset) =>
|
|
142
|
+
srcset
|
|
143
|
+
.split(",")
|
|
144
|
+
.map((candidate) => {
|
|
145
|
+
const parts = candidate.trim().split(/\s+/);
|
|
146
|
+
if (!parts[0]) return candidate;
|
|
147
|
+
return [rewriteImageUrl(parts[0]), ...parts.slice(1)].join(" ");
|
|
148
|
+
})
|
|
149
|
+
.join(", ");
|
|
150
|
+
|
|
151
|
+
const rewriteLocalImageReferences = (html) =>
|
|
152
|
+
html.replace(/<([a-z][\w:-]*)(\s[^<>]*?)?>/gi, (tag, tagName) => {
|
|
153
|
+
const normalizedTagName = tagName.toLowerCase();
|
|
154
|
+
if (!rewriteableImageTags.has(normalizedTagName)) {
|
|
155
|
+
return tag;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return tag.replace(
|
|
159
|
+
/\s(src|href|srcset)=(["'])(.*?)\2/gi,
|
|
160
|
+
(attribute, attributeName, quote, value) => {
|
|
161
|
+
const nextValue =
|
|
162
|
+
attributeName.toLowerCase() === "srcset"
|
|
163
|
+
? rewriteSrcset(value)
|
|
164
|
+
: rewriteImageUrl(value);
|
|
165
|
+
return ` ${attributeName}=${quote}${nextValue}${quote}`;
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
25
170
|
const replacePlaceholders = async (template, values) => {
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
171
|
+
// Default values for optional variables
|
|
172
|
+
const defaults = {
|
|
173
|
+
pagination: '',
|
|
174
|
+
breadcrumbs: '',
|
|
175
|
+
nav_links: '',
|
|
176
|
+
links_to_children: '',
|
|
177
|
+
links_to_siblings: '',
|
|
178
|
+
links_to_tags: '',
|
|
179
|
+
links_to_self_and_siblings: '',
|
|
180
|
+
content: '',
|
|
181
|
+
title: '',
|
|
182
|
+
sitename: '',
|
|
183
|
+
author: '',
|
|
184
|
+
date: '',
|
|
185
|
+
created_at: '',
|
|
186
|
+
updated_at: '',
|
|
187
|
+
og_tags: '',
|
|
188
|
+
word_count: 0,
|
|
189
|
+
reading_time: '',
|
|
190
|
+
prev_page: '',
|
|
191
|
+
next_page: '',
|
|
35
192
|
};
|
|
36
193
|
|
|
37
|
-
//
|
|
38
|
-
|
|
194
|
+
// Load data files from data/ folder
|
|
195
|
+
const dataFiles = await loadData();
|
|
196
|
+
|
|
197
|
+
// Generate OG tags
|
|
198
|
+
const og_tags = generateOgTags(values);
|
|
199
|
+
|
|
200
|
+
// Calculate word count and reading time from content
|
|
201
|
+
const word_count = countWords(values.content);
|
|
202
|
+
const reading_time = calculateReadingTime(word_count);
|
|
203
|
+
|
|
204
|
+
// Build the data object for Eta
|
|
205
|
+
// Merge defaults, config values, page metadata, and computed values
|
|
206
|
+
const templateData = {
|
|
207
|
+
...defaults,
|
|
208
|
+
...values,
|
|
209
|
+
...(values.meta || {}),
|
|
210
|
+
og_tags,
|
|
211
|
+
word_count,
|
|
212
|
+
reading_time,
|
|
213
|
+
// Expose page namespace with meta
|
|
214
|
+
page: {
|
|
215
|
+
...defaults,
|
|
216
|
+
...values,
|
|
217
|
+
...(values.meta || {}),
|
|
218
|
+
meta: values.meta || {},
|
|
219
|
+
og_tags,
|
|
220
|
+
word_count,
|
|
221
|
+
reading_time,
|
|
222
|
+
},
|
|
223
|
+
// Expose data folder contents
|
|
224
|
+
data: dataFiles,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Protect code blocks BEFORE Eta processing
|
|
39
228
|
const codeBlockRegex =
|
|
40
229
|
/```[\s\S]*?\n```|`[^`\n]+`|<(pre|code)[^>]*>[\s\S]*?<\/\1>/g;
|
|
41
230
|
const codeBlocks = [];
|
|
42
231
|
template = template.replace(codeBlockRegex, (match) => {
|
|
43
232
|
codeBlocks.push(match);
|
|
44
|
-
return `
|
|
233
|
+
return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
|
|
45
234
|
});
|
|
46
235
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
partialRegex,
|
|
51
|
-
async (match, partialName) => {
|
|
52
|
-
let partialContent = await loadPartial(partialName);
|
|
53
|
-
partialContent = await replacePlaceholders(partialContent, values); // Recursive replacement
|
|
54
|
-
return marked(partialContent); // Convert Markdown to HTML
|
|
55
|
-
},
|
|
56
|
-
);
|
|
236
|
+
// Handle <%= partial: name %> syntax
|
|
237
|
+
const partialRegex = /<%=\s*partial:\s*([\w-]+)\s*%>/g;
|
|
238
|
+
const partialMatches = [...template.matchAll(partialRegex)];
|
|
57
239
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
240
|
+
for (const match of partialMatches) {
|
|
241
|
+
const [fullMatch, partialName] = match;
|
|
242
|
+
const partialData = await loadPartialData(partialName);
|
|
243
|
+
let partialContent = partialData.content;
|
|
244
|
+
partialContent = await replacePlaceholders(partialContent, values);
|
|
245
|
+
const renderedPartial = partialData.raw ? partialContent : marked(partialContent);
|
|
246
|
+
template = template.replace(fullMatch, renderedPartial);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Convert <%- to <%= since autoEscape is false (all output is raw)
|
|
250
|
+
// This provides EJS-style syntax compatibility
|
|
251
|
+
template = template.replace(/<%-/g, '<%=');
|
|
252
|
+
|
|
253
|
+
// Render with Eta
|
|
254
|
+
try {
|
|
255
|
+
template = eta.renderString(template, templateData);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.warn(`Eta template error: ${error.message}`);
|
|
258
|
+
}
|
|
66
259
|
|
|
67
260
|
// Restore code blocks
|
|
68
261
|
template = template.replace(
|
|
69
|
-
/
|
|
262
|
+
/__CODE_BLOCK_(\d+)__/g,
|
|
70
263
|
(_, index) => codeBlocks[index],
|
|
71
264
|
);
|
|
72
|
-
|
|
73
|
-
template = template
|
|
265
|
+
|
|
266
|
+
template = rewriteLocalImageReferences(template);
|
|
267
|
+
|
|
74
268
|
return template;
|
|
75
269
|
};
|
|
76
270
|
|
package/src/rss.js
CHANGED
|
@@ -57,8 +57,8 @@ const generateRssFeed = (feedConfig, pages, siteUrl) => {
|
|
|
57
57
|
const items = feedPages
|
|
58
58
|
.map((page) => {
|
|
59
59
|
const itemUrl = `${siteUrl}${page.url}`;
|
|
60
|
-
const itemTitle = escapeXml(page.
|
|
61
|
-
const itemDate = toRfc822(page.
|
|
60
|
+
const itemTitle = escapeXml(page.meta?.title || page.title || page.name);
|
|
61
|
+
const itemDate = toRfc822(page.meta?.date || page.updated_at || new Date());
|
|
62
62
|
const itemDescription = escapeXml(
|
|
63
63
|
truncate(stripHtml(page.content), 300)
|
|
64
64
|
);
|
|
@@ -98,8 +98,10 @@ const findPagesInFolder = (pages, folderName) => {
|
|
|
98
98
|
found.push(page);
|
|
99
99
|
}
|
|
100
100
|
// Recursively search nested pages
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
// Use allPages for paginated folders to include all items in RSS
|
|
102
|
+
const childPages = page.allPages || page.pages;
|
|
103
|
+
if (childPages) {
|
|
104
|
+
searchPages(childPages);
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
};
|
package/src/sitemap.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import fsExtra from "fs-extra";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { defaultConfig } from "./config.js";
|
|
5
|
+
|
|
6
|
+
const escapeXml = (str) =>
|
|
7
|
+
String(str || "")
|
|
8
|
+
.replace(/&/g, "&")
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">")
|
|
11
|
+
.replace(/"/g, """)
|
|
12
|
+
.replace(/'/g, "'");
|
|
13
|
+
|
|
14
|
+
const formatSitemapDate = (date) => {
|
|
15
|
+
const parsed = date instanceof Date ? date : new Date(date);
|
|
16
|
+
return isNaN(parsed.getTime()) ? null : parsed.toISOString().slice(0, 10);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const flattenPages = (pages) => {
|
|
20
|
+
const flattened = [];
|
|
21
|
+
|
|
22
|
+
const addPages = (pageList) => {
|
|
23
|
+
for (const page of pageList) {
|
|
24
|
+
if (page.url && page.meta?.sitemap !== false) {
|
|
25
|
+
flattened.push(page);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (page.pages) addPages(page.pages);
|
|
29
|
+
if (page.paginatedPages) addPages(page.paginatedPages);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
addPages(pages);
|
|
34
|
+
return flattened;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const pageUrl = (siteUrl, page) => {
|
|
38
|
+
const baseUrl = siteUrl.replace(/\/$/, "");
|
|
39
|
+
return `${baseUrl}${page.url === "/" ? "/" : page.url}`;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const generateSitemapXml = (pages, siteUrl = "") => {
|
|
43
|
+
const seenUrls = new Set();
|
|
44
|
+
const urls = flattenPages(pages)
|
|
45
|
+
.filter((page) => {
|
|
46
|
+
if (seenUrls.has(page.url)) return false;
|
|
47
|
+
seenUrls.add(page.url);
|
|
48
|
+
return true;
|
|
49
|
+
})
|
|
50
|
+
.map((page) => {
|
|
51
|
+
const lastmod = formatSitemapDate(
|
|
52
|
+
page.updatedAtObj || page.dateObj || page.updated_at || new Date(),
|
|
53
|
+
);
|
|
54
|
+
const lastmodLine = lastmod ? `\n <lastmod>${lastmod}</lastmod>` : "";
|
|
55
|
+
|
|
56
|
+
return ` <url>
|
|
57
|
+
<loc>${escapeXml(pageUrl(siteUrl, page))}</loc>${lastmodLine}
|
|
58
|
+
</url>`;
|
|
59
|
+
})
|
|
60
|
+
.join("\n");
|
|
61
|
+
|
|
62
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
63
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
64
|
+
${urls}
|
|
65
|
+
</urlset>
|
|
66
|
+
`;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const generateRobotsTxt = (siteUrl = "") => {
|
|
70
|
+
const lines = ["User-agent: *", "Allow: /"];
|
|
71
|
+
if (siteUrl) {
|
|
72
|
+
lines.push(`Sitemap: ${siteUrl.replace(/\/$/, "")}/sitemap.xml`);
|
|
73
|
+
}
|
|
74
|
+
return `${lines.join("\n")}\n`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const generateSeoFiles = async (pages, outputDir) => {
|
|
78
|
+
const siteUrl = defaultConfig.site_url || defaultConfig.url || "";
|
|
79
|
+
const sitemapXml = generateSitemapXml(pages, siteUrl);
|
|
80
|
+
const robotsTxt = generateRobotsTxt(siteUrl);
|
|
81
|
+
|
|
82
|
+
await fsExtra.ensureDir(outputDir);
|
|
83
|
+
await Promise.all([
|
|
84
|
+
fs.writeFile(path.join(outputDir, "sitemap.xml"), sitemapXml),
|
|
85
|
+
fs.writeFile(path.join(outputDir, "robots.txt"), robotsTxt),
|
|
86
|
+
]);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export { flattenPages, generateRobotsTxt, generateSeoFiles, generateSitemapXml };
|
package/src/watcher.js
CHANGED
|
@@ -2,6 +2,7 @@ import chokidar from "chokidar";
|
|
|
2
2
|
import livereload from "livereload";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import fs from "fs";
|
|
5
|
+
import { defaultConfig } from "./config.js";
|
|
5
6
|
|
|
6
7
|
// Directory to extension mapping for filtering
|
|
7
8
|
const dirExtensions = {
|
|
@@ -11,6 +12,7 @@ const dirExtensions = {
|
|
|
11
12
|
css: [".css"],
|
|
12
13
|
js: [".js"],
|
|
13
14
|
partials: [".md", ".html"],
|
|
15
|
+
data: [".json", ".yaml", ".yml"],
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
// Build watch paths
|
|
@@ -74,6 +76,7 @@ function getChangeType(filePath) {
|
|
|
74
76
|
if (topDir === "css") return "css";
|
|
75
77
|
if (topDir === "js") return "js";
|
|
76
78
|
if (topDir === "images") return "image";
|
|
79
|
+
if (topDir === "data") return "data";
|
|
77
80
|
return "full"; // pages, layouts, partials, config files
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -82,6 +85,7 @@ export default async function watch(outDir = "dist") {
|
|
|
82
85
|
const { copySingleAsset, optimizeSingleImage } = await import("./assets.js");
|
|
83
86
|
const { resetCaches } = await import("./layout.js");
|
|
84
87
|
const { clearCache: clearPartialCache } = await import("./partials.js");
|
|
88
|
+
const { clearDataCache } = await import("./data.js");
|
|
85
89
|
const watchPaths = getWatchPaths();
|
|
86
90
|
|
|
87
91
|
if (watchPaths.length === 0) {
|
|
@@ -92,22 +96,25 @@ export default async function watch(outDir = "dist") {
|
|
|
92
96
|
// Start livereload server
|
|
93
97
|
const lrServer = livereload.createServer({
|
|
94
98
|
usePolling: true,
|
|
95
|
-
delay: 100,
|
|
99
|
+
delay: defaultConfig.watcher_delay || 100,
|
|
96
100
|
});
|
|
97
101
|
const outPath = path.join(process.cwd(), outDir);
|
|
102
|
+
const livereloadPort = defaultConfig.livereload_port || 35729;
|
|
98
103
|
lrServer.watch(outPath);
|
|
99
|
-
console.log(
|
|
104
|
+
console.log(`LiveReload server started on port ${livereloadPort}`);
|
|
100
105
|
|
|
101
106
|
// Initialize watcher (chokidar 4.x)
|
|
102
107
|
// usePolling needed for network/cloud drives that don't emit native fs events
|
|
108
|
+
const watcherInterval = defaultConfig.watcher_interval || 500;
|
|
109
|
+
const watcherDelay = defaultConfig.watcher_delay || 100;
|
|
103
110
|
const watcher = chokidar.watch(watchPaths, {
|
|
104
111
|
persistent: true,
|
|
105
112
|
ignoreInitial: true,
|
|
106
113
|
usePolling: true,
|
|
107
|
-
interval:
|
|
114
|
+
interval: watcherInterval,
|
|
108
115
|
awaitWriteFinish: {
|
|
109
|
-
stabilityThreshold:
|
|
110
|
-
pollInterval:
|
|
116
|
+
stabilityThreshold: watcherDelay * 2,
|
|
117
|
+
pollInterval: watcherDelay,
|
|
111
118
|
},
|
|
112
119
|
});
|
|
113
120
|
|
|
@@ -123,25 +130,45 @@ export default async function watch(outDir = "dist") {
|
|
|
123
130
|
try {
|
|
124
131
|
if (event === "deleted") {
|
|
125
132
|
// For deletions, do a full rebuild to clean up
|
|
126
|
-
console.log(
|
|
133
|
+
console.log(`🗑️ File deleted: ${filename}. Running full build...`);
|
|
134
|
+
const rebuildStart = performance.now();
|
|
127
135
|
clearPartialCache();
|
|
128
136
|
await resetCaches();
|
|
129
137
|
await build.default(outDir);
|
|
138
|
+
const rebuildTime = performance.now() - rebuildStart;
|
|
139
|
+
console.log(` Rebuild completed in ${(rebuildTime / 1000).toFixed(2)}s`);
|
|
130
140
|
} else if (changeType === "css" || changeType === "js") {
|
|
131
141
|
// Full rebuild for CSS/JS to update cache-busting query strings in HTML
|
|
132
|
-
console.log(
|
|
142
|
+
console.log(`🎨 Asset ${event}: ${filename}. Rebuilding...`);
|
|
143
|
+
const rebuildStart = performance.now();
|
|
133
144
|
await resetCaches();
|
|
134
145
|
await build.default(outDir);
|
|
146
|
+
const rebuildTime = performance.now() - rebuildStart;
|
|
147
|
+
console.log(` Rebuild completed in ${(rebuildTime / 1000).toFixed(2)}s`);
|
|
135
148
|
} else if (changeType === "image") {
|
|
136
149
|
// Incremental: just process the changed image
|
|
137
|
-
|
|
150
|
+
const imageStart = performance.now();
|
|
151
|
+
console.log(`🖼️ Image ${event}: ${filename}`);
|
|
138
152
|
await optimizeSingleImage(filePath, outDir);
|
|
153
|
+
const imageTime = performance.now() - imageStart;
|
|
154
|
+
console.log(` Optimized in ${(imageTime / 1000).toFixed(2)}s`);
|
|
155
|
+
} else if (changeType === "data") {
|
|
156
|
+
// Data file changed - clear data cache and rebuild
|
|
157
|
+
console.log(`📊 Data ${event}: ${filename}. Rebuilding...`);
|
|
158
|
+
const rebuildStart = performance.now();
|
|
159
|
+
clearDataCache();
|
|
160
|
+
await build.default(outDir);
|
|
161
|
+
const rebuildTime = performance.now() - rebuildStart;
|
|
162
|
+
console.log(` Rebuild completed in ${(rebuildTime / 1000).toFixed(2)}s`);
|
|
139
163
|
} else {
|
|
140
164
|
// Full rebuild for pages, layouts, partials, config, and new CSS/JS files
|
|
141
|
-
console.log(
|
|
165
|
+
console.log(`📝 File ${event}: ${filename}. Running full build...`);
|
|
166
|
+
const rebuildStart = performance.now();
|
|
142
167
|
clearPartialCache();
|
|
143
168
|
await resetCaches();
|
|
144
169
|
await build.default(outDir);
|
|
170
|
+
const rebuildTime = performance.now() - rebuildStart;
|
|
171
|
+
console.log(` Rebuild completed in ${(rebuildTime / 1000).toFixed(2)}s`);
|
|
145
172
|
}
|
|
146
173
|
// Trigger browser refresh
|
|
147
174
|
lrServer.refresh("/");
|