basecampjs 0.0.6 → 0.0.8
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/index.js +160 -20
- package/package.json +2 -1
package/index.js
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
import { argv, exit } from "process";
|
|
3
3
|
import { createServer } from "http";
|
|
4
4
|
import { existsSync } from "fs";
|
|
5
|
-
import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "fs/promises";
|
|
5
|
+
import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "fs/promises";
|
|
6
6
|
import { basename, dirname, extname, join, relative, resolve } from "path";
|
|
7
7
|
import { pathToFileURL } from "url";
|
|
8
|
+
import { createHash } from "crypto";
|
|
8
9
|
import * as kolor from "kolorist";
|
|
9
10
|
import chokidar from "chokidar";
|
|
10
11
|
import matter from "gray-matter";
|
|
11
12
|
import MarkdownIt from "markdown-it";
|
|
13
|
+
import Mustache from "mustache";
|
|
12
14
|
import nunjucks from "nunjucks";
|
|
13
15
|
import { Liquid } from "liquidjs";
|
|
14
16
|
import { minify as minifyCss } from "csso";
|
|
@@ -25,7 +27,8 @@ const defaultConfig = {
|
|
|
25
27
|
markdown: true,
|
|
26
28
|
minifyCSS: false,
|
|
27
29
|
minifyHTML: false,
|
|
28
|
-
|
|
30
|
+
cacheBustAssets: false,
|
|
31
|
+
integrations: { nunjucks: true, liquid: false, mustache: false, vue: false, alpine: false }
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
async function loadConfig(root) {
|
|
@@ -45,18 +48,23 @@ async function ensureDir(dir) {
|
|
|
45
48
|
await mkdir(dir, { recursive: true });
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
async function loadData(
|
|
51
|
+
async function loadData(dataDirs) {
|
|
49
52
|
const collections = {};
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
// Support both string and array input
|
|
54
|
+
const dirs = Array.isArray(dataDirs) ? dataDirs : [dataDirs];
|
|
55
|
+
|
|
56
|
+
for (const dataDir of dirs) {
|
|
57
|
+
if (!existsSync(dataDir)) continue;
|
|
58
|
+
const files = await walkFiles(dataDir);
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
if (extname(file).toLowerCase() !== ".json") continue;
|
|
61
|
+
const name = basename(file, ".json");
|
|
62
|
+
try {
|
|
63
|
+
const raw = await readFile(file, "utf8");
|
|
64
|
+
collections[name] = JSON.parse(raw);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(kolor.red(`Failed to load data ${relative(dataDir, file)}: ${err.message}`));
|
|
67
|
+
}
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
return collections;
|
|
@@ -175,7 +183,7 @@ function shouldRenderMarkdown(frontmatter, config, defaultValue) {
|
|
|
175
183
|
return defaultValue;
|
|
176
184
|
}
|
|
177
185
|
|
|
178
|
-
async function renderWithLayout(layoutName, html, ctx, env, liquidEnv) {
|
|
186
|
+
async function renderWithLayout(layoutName, html, ctx, env, liquidEnv, layoutsDir) {
|
|
179
187
|
if (!layoutName) return html;
|
|
180
188
|
const ext = extname(layoutName).toLowerCase();
|
|
181
189
|
const layoutCtx = {
|
|
@@ -193,6 +201,14 @@ async function renderWithLayout(layoutName, html, ctx, env, liquidEnv) {
|
|
|
193
201
|
return liquidEnv.renderFile(layoutName, layoutCtx);
|
|
194
202
|
}
|
|
195
203
|
|
|
204
|
+
if (ext === ".mustache") {
|
|
205
|
+
const layoutPath = join(layoutsDir, layoutName);
|
|
206
|
+
if (existsSync(layoutPath)) {
|
|
207
|
+
const layoutTemplate = await readFile(layoutPath, "utf8");
|
|
208
|
+
return Mustache.render(layoutTemplate, layoutCtx);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
196
212
|
// Unknown layout type, return unwrapped content
|
|
197
213
|
return html;
|
|
198
214
|
}
|
|
@@ -210,7 +226,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
|
|
|
210
226
|
const parsed = matter(raw);
|
|
211
227
|
const html = md.render(parsed.content);
|
|
212
228
|
const ctx = pageContext(parsed.data, html, config, rel, data, path);
|
|
213
|
-
const rendered = await renderWithLayout(parsed.data.layout, html, ctx, env, liquidEnv);
|
|
229
|
+
const rendered = await renderWithLayout(parsed.data.layout, html, ctx, env, liquidEnv, layoutsDir);
|
|
214
230
|
await writeFile(outPath, rendered, "utf8");
|
|
215
231
|
return;
|
|
216
232
|
}
|
|
@@ -224,7 +240,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
|
|
|
224
240
|
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
225
241
|
pageHtml = md.render(pageHtml);
|
|
226
242
|
}
|
|
227
|
-
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv);
|
|
243
|
+
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir);
|
|
228
244
|
await writeFile(outPath, rendered, "utf8");
|
|
229
245
|
return;
|
|
230
246
|
}
|
|
@@ -237,7 +253,20 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
|
|
|
237
253
|
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
238
254
|
pageHtml = md.render(pageHtml);
|
|
239
255
|
}
|
|
240
|
-
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv);
|
|
256
|
+
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir);
|
|
257
|
+
await writeFile(outPath, rendered, "utf8");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (ext === ".mustache") {
|
|
262
|
+
const raw = await readFile(filePath, "utf8");
|
|
263
|
+
const parsed = matter(raw);
|
|
264
|
+
const ctx = pageContext(parsed.data, parsed.content, config, rel, data, path);
|
|
265
|
+
let pageHtml = Mustache.render(parsed.content, ctx);
|
|
266
|
+
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
267
|
+
pageHtml = md.render(pageHtml);
|
|
268
|
+
}
|
|
269
|
+
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir);
|
|
241
270
|
await writeFile(outPath, rendered, "utf8");
|
|
242
271
|
return;
|
|
243
272
|
}
|
|
@@ -250,7 +279,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
|
|
|
250
279
|
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
251
280
|
pageHtml = md.render(pageHtml);
|
|
252
281
|
}
|
|
253
|
-
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv);
|
|
282
|
+
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir);
|
|
254
283
|
await writeFile(outPath, rendered, "utf8");
|
|
255
284
|
return;
|
|
256
285
|
}
|
|
@@ -258,6 +287,88 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
|
|
|
258
287
|
await cp(filePath, outPath);
|
|
259
288
|
}
|
|
260
289
|
|
|
290
|
+
async function cacheBustAssets(outDir) {
|
|
291
|
+
const assetMap = {}; // original path -> hashed path
|
|
292
|
+
const files = await walkFiles(outDir);
|
|
293
|
+
const assetFiles = files.filter((file) => {
|
|
294
|
+
const ext = extname(file).toLowerCase();
|
|
295
|
+
return ext === ".css" || ext === ".js";
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Hash and rename each asset
|
|
299
|
+
for (const file of assetFiles) {
|
|
300
|
+
try {
|
|
301
|
+
const content = await readFile(file);
|
|
302
|
+
const hash = createHash("sha256").update(content).digest("hex").slice(0, 10);
|
|
303
|
+
const ext = extname(file);
|
|
304
|
+
const base = basename(file, ext);
|
|
305
|
+
const dir = dirname(file);
|
|
306
|
+
const hashedName = `${base}-${hash}${ext}`;
|
|
307
|
+
const hashedPath = join(dir, hashedName);
|
|
308
|
+
|
|
309
|
+
await rename(file, hashedPath);
|
|
310
|
+
|
|
311
|
+
// Store mapping of original relative path to hashed relative path
|
|
312
|
+
const originalRel = relative(outDir, file).replace(/\\/g, "/");
|
|
313
|
+
const hashedRel = relative(outDir, hashedPath).replace(/\\/g, "/");
|
|
314
|
+
assetMap[originalRel] = hashedRel;
|
|
315
|
+
|
|
316
|
+
// Log the cache-busted file
|
|
317
|
+
console.log(kolor.dim(` ${originalRel}`) + kolor.cyan(` → `) + kolor.green(hashedRel));
|
|
318
|
+
} catch (err) {
|
|
319
|
+
console.error(kolor.red(`Failed to cache-bust ${relative(outDir, file)}: ${err.message}`));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Update HTML files to reference hashed assets
|
|
324
|
+
const htmlFiles = files.filter((file) => extname(file).toLowerCase() === ".html");
|
|
325
|
+
|
|
326
|
+
for (const htmlFile of htmlFiles) {
|
|
327
|
+
try {
|
|
328
|
+
let html = await readFile(htmlFile, "utf8");
|
|
329
|
+
let updated = false;
|
|
330
|
+
|
|
331
|
+
// Update <link> tags (CSS)
|
|
332
|
+
for (const [original, hashed] of Object.entries(assetMap)) {
|
|
333
|
+
if (original.endsWith(".css")) {
|
|
334
|
+
// Match href="/path" or href="path" but ensure we stay within the tag
|
|
335
|
+
const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
336
|
+
const pattern = new RegExp(`(<link[^>]*?\\shref=["'])/?${escaped}(["'])`, "gi");
|
|
337
|
+
|
|
338
|
+
const newHtml = html.replace(pattern, `$1/${hashed}$2`);
|
|
339
|
+
if (newHtml !== html) {
|
|
340
|
+
html = newHtml;
|
|
341
|
+
updated = true;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Update <script> tags (JS)
|
|
347
|
+
for (const [original, hashed] of Object.entries(assetMap)) {
|
|
348
|
+
if (original.endsWith(".js")) {
|
|
349
|
+
// Match src="/path" or src="path" but ensure we stay within the tag
|
|
350
|
+
const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
351
|
+
const pattern = new RegExp(`(<script[^>]*?\\ssrc=["'])/?${escaped}(["'])`, "gi");
|
|
352
|
+
|
|
353
|
+
const newHtml = html.replace(pattern, `$1/${hashed}$2`);
|
|
354
|
+
if (newHtml !== html) {
|
|
355
|
+
html = newHtml;
|
|
356
|
+
updated = true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (updated) {
|
|
362
|
+
await writeFile(htmlFile, html, "utf8");
|
|
363
|
+
}
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.error(kolor.red(`Failed to update asset references in ${relative(outDir, htmlFile)}: ${err.message}`));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return assetMap;
|
|
370
|
+
}
|
|
371
|
+
|
|
261
372
|
async function build(cwdArg = cwd) {
|
|
262
373
|
const config = await loadConfig(cwdArg);
|
|
263
374
|
const srcDir = resolve(cwdArg, config.srcDir || "src");
|
|
@@ -265,11 +376,28 @@ async function build(cwdArg = cwd) {
|
|
|
265
376
|
const layoutsDir = join(srcDir, "layouts");
|
|
266
377
|
const partialsDir = join(srcDir, "partials");
|
|
267
378
|
const dataDir = join(srcDir, "data");
|
|
379
|
+
const collectionsDir = join(srcDir, "collections");
|
|
268
380
|
const publicDir = resolve(cwdArg, "public");
|
|
269
381
|
const outDir = resolve(cwdArg, config.outDir || "dist");
|
|
270
382
|
const env = createNunjucksEnv(layoutsDir, pagesDir, srcDir, partialsDir);
|
|
383
|
+
// Allow user config to extend the Nunjucks environment (e.g., custom filters)
|
|
384
|
+
if (config?.hooks?.nunjucksEnv && typeof config.hooks.nunjucksEnv === "function") {
|
|
385
|
+
try {
|
|
386
|
+
config.hooks.nunjucksEnv(env);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.error(kolor.red(`Failed to apply nunjucksEnv hook: ${err.message}`));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
271
391
|
const liquidEnv = createLiquidEnv(layoutsDir, pagesDir, srcDir, partialsDir);
|
|
272
|
-
|
|
392
|
+
// Allow user config to extend the Liquid environment (e.g., custom filters)
|
|
393
|
+
if (config?.hooks?.liquidEnv && typeof config.hooks.liquidEnv === "function") {
|
|
394
|
+
try {
|
|
395
|
+
config.hooks.liquidEnv(liquidEnv);
|
|
396
|
+
} catch (err) {
|
|
397
|
+
console.error(kolor.red(`Failed to apply liquidEnv hook: ${err.message}`));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const data = await loadData([dataDir, collectionsDir]);
|
|
273
401
|
|
|
274
402
|
await cleanDir(outDir);
|
|
275
403
|
await copyPublic(publicDir, outDir);
|
|
@@ -292,6 +420,17 @@ async function build(cwdArg = cwd) {
|
|
|
292
420
|
console.log(kolor.green("HTML minified"));
|
|
293
421
|
}
|
|
294
422
|
|
|
423
|
+
if (config.cacheBustAssets) {
|
|
424
|
+
console.log(kolor.cyan("Cache-busting assets..."));
|
|
425
|
+
const assetMap = await cacheBustAssets(outDir);
|
|
426
|
+
const assetCount = Object.keys(assetMap).length;
|
|
427
|
+
if (assetCount > 0) {
|
|
428
|
+
console.log(kolor.green(`✓ Cache-busted ${assetCount} asset(s)`));
|
|
429
|
+
} else {
|
|
430
|
+
console.log(kolor.yellow("No assets found to cache-bust"));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
295
434
|
console.log(kolor.green(`Built ${files.length} page(s) → ${relative(cwdArg, outDir)}`));
|
|
296
435
|
}
|
|
297
436
|
|
|
@@ -381,9 +520,10 @@ async function dev(cwdArg = cwd) {
|
|
|
381
520
|
const config = await loadConfig(cwdArg);
|
|
382
521
|
const srcDir = resolve(cwdArg, config.srcDir || "src");
|
|
383
522
|
const dataDir = join(srcDir, "data");
|
|
523
|
+
const collectionsDir = join(srcDir, "collections");
|
|
384
524
|
const publicDir = resolve(cwdArg, "public");
|
|
385
525
|
const outDir = resolve(cwdArg, config.outDir || "dist");
|
|
386
|
-
const watcher = chokidar.watch([srcDir, publicDir, dataDir], { ignoreInitial: true });
|
|
526
|
+
const watcher = chokidar.watch([srcDir, publicDir, dataDir, collectionsDir], { ignoreInitial: true });
|
|
387
527
|
|
|
388
528
|
watcher.on("all", (event, path) => {
|
|
389
529
|
console.log(kolor.cyan(`↻ ${event}: ${relative(cwdArg, path)}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "basecampjs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "BasecampJS engine for CampsiteJS static site generator.",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"kolorist": "^1.8.0",
|
|
19
19
|
"liquidjs": "^10.12.0",
|
|
20
20
|
"markdown-it": "^14.1.0",
|
|
21
|
+
"mustache": "^4.2.0",
|
|
21
22
|
"nunjucks": "^3.2.4"
|
|
22
23
|
},
|
|
23
24
|
"engines": {
|