basecampjs 0.0.6 → 0.0.7
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 +140 -7
- 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) {
|
|
@@ -175,7 +178,7 @@ function shouldRenderMarkdown(frontmatter, config, defaultValue) {
|
|
|
175
178
|
return defaultValue;
|
|
176
179
|
}
|
|
177
180
|
|
|
178
|
-
async function renderWithLayout(layoutName, html, ctx, env, liquidEnv) {
|
|
181
|
+
async function renderWithLayout(layoutName, html, ctx, env, liquidEnv, layoutsDir) {
|
|
179
182
|
if (!layoutName) return html;
|
|
180
183
|
const ext = extname(layoutName).toLowerCase();
|
|
181
184
|
const layoutCtx = {
|
|
@@ -193,6 +196,14 @@ async function renderWithLayout(layoutName, html, ctx, env, liquidEnv) {
|
|
|
193
196
|
return liquidEnv.renderFile(layoutName, layoutCtx);
|
|
194
197
|
}
|
|
195
198
|
|
|
199
|
+
if (ext === ".mustache") {
|
|
200
|
+
const layoutPath = join(layoutsDir, layoutName);
|
|
201
|
+
if (existsSync(layoutPath)) {
|
|
202
|
+
const layoutTemplate = await readFile(layoutPath, "utf8");
|
|
203
|
+
return Mustache.render(layoutTemplate, layoutCtx);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
196
207
|
// Unknown layout type, return unwrapped content
|
|
197
208
|
return html;
|
|
198
209
|
}
|
|
@@ -210,7 +221,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
|
|
|
210
221
|
const parsed = matter(raw);
|
|
211
222
|
const html = md.render(parsed.content);
|
|
212
223
|
const ctx = pageContext(parsed.data, html, config, rel, data, path);
|
|
213
|
-
const rendered = await renderWithLayout(parsed.data.layout, html, ctx, env, liquidEnv);
|
|
224
|
+
const rendered = await renderWithLayout(parsed.data.layout, html, ctx, env, liquidEnv, layoutsDir);
|
|
214
225
|
await writeFile(outPath, rendered, "utf8");
|
|
215
226
|
return;
|
|
216
227
|
}
|
|
@@ -224,7 +235,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
|
|
|
224
235
|
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
225
236
|
pageHtml = md.render(pageHtml);
|
|
226
237
|
}
|
|
227
|
-
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv);
|
|
238
|
+
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir);
|
|
228
239
|
await writeFile(outPath, rendered, "utf8");
|
|
229
240
|
return;
|
|
230
241
|
}
|
|
@@ -237,7 +248,20 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
|
|
|
237
248
|
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
238
249
|
pageHtml = md.render(pageHtml);
|
|
239
250
|
}
|
|
240
|
-
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv);
|
|
251
|
+
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir);
|
|
252
|
+
await writeFile(outPath, rendered, "utf8");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (ext === ".mustache") {
|
|
257
|
+
const raw = await readFile(filePath, "utf8");
|
|
258
|
+
const parsed = matter(raw);
|
|
259
|
+
const ctx = pageContext(parsed.data, parsed.content, config, rel, data, path);
|
|
260
|
+
let pageHtml = Mustache.render(parsed.content, ctx);
|
|
261
|
+
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
262
|
+
pageHtml = md.render(pageHtml);
|
|
263
|
+
}
|
|
264
|
+
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir);
|
|
241
265
|
await writeFile(outPath, rendered, "utf8");
|
|
242
266
|
return;
|
|
243
267
|
}
|
|
@@ -250,7 +274,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
|
|
|
250
274
|
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
251
275
|
pageHtml = md.render(pageHtml);
|
|
252
276
|
}
|
|
253
|
-
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv);
|
|
277
|
+
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir);
|
|
254
278
|
await writeFile(outPath, rendered, "utf8");
|
|
255
279
|
return;
|
|
256
280
|
}
|
|
@@ -258,6 +282,88 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
|
|
|
258
282
|
await cp(filePath, outPath);
|
|
259
283
|
}
|
|
260
284
|
|
|
285
|
+
async function cacheBustAssets(outDir) {
|
|
286
|
+
const assetMap = {}; // original path -> hashed path
|
|
287
|
+
const files = await walkFiles(outDir);
|
|
288
|
+
const assetFiles = files.filter((file) => {
|
|
289
|
+
const ext = extname(file).toLowerCase();
|
|
290
|
+
return ext === ".css" || ext === ".js";
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Hash and rename each asset
|
|
294
|
+
for (const file of assetFiles) {
|
|
295
|
+
try {
|
|
296
|
+
const content = await readFile(file);
|
|
297
|
+
const hash = createHash("sha256").update(content).digest("hex").slice(0, 10);
|
|
298
|
+
const ext = extname(file);
|
|
299
|
+
const base = basename(file, ext);
|
|
300
|
+
const dir = dirname(file);
|
|
301
|
+
const hashedName = `${base}-${hash}${ext}`;
|
|
302
|
+
const hashedPath = join(dir, hashedName);
|
|
303
|
+
|
|
304
|
+
await rename(file, hashedPath);
|
|
305
|
+
|
|
306
|
+
// Store mapping of original relative path to hashed relative path
|
|
307
|
+
const originalRel = relative(outDir, file).replace(/\\/g, "/");
|
|
308
|
+
const hashedRel = relative(outDir, hashedPath).replace(/\\/g, "/");
|
|
309
|
+
assetMap[originalRel] = hashedRel;
|
|
310
|
+
|
|
311
|
+
// Log the cache-busted file
|
|
312
|
+
console.log(kolor.dim(` ${originalRel}`) + kolor.cyan(` → `) + kolor.green(hashedRel));
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.error(kolor.red(`Failed to cache-bust ${relative(outDir, file)}: ${err.message}`));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Update HTML files to reference hashed assets
|
|
319
|
+
const htmlFiles = files.filter((file) => extname(file).toLowerCase() === ".html");
|
|
320
|
+
|
|
321
|
+
for (const htmlFile of htmlFiles) {
|
|
322
|
+
try {
|
|
323
|
+
let html = await readFile(htmlFile, "utf8");
|
|
324
|
+
let updated = false;
|
|
325
|
+
|
|
326
|
+
// Update <link> tags (CSS)
|
|
327
|
+
for (const [original, hashed] of Object.entries(assetMap)) {
|
|
328
|
+
if (original.endsWith(".css")) {
|
|
329
|
+
// Match href="/path" or href="path" but ensure we stay within the tag
|
|
330
|
+
const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
331
|
+
const pattern = new RegExp(`(<link[^>]*?\\shref=["'])/?${escaped}(["'])`, "gi");
|
|
332
|
+
|
|
333
|
+
const newHtml = html.replace(pattern, `$1/${hashed}$2`);
|
|
334
|
+
if (newHtml !== html) {
|
|
335
|
+
html = newHtml;
|
|
336
|
+
updated = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Update <script> tags (JS)
|
|
342
|
+
for (const [original, hashed] of Object.entries(assetMap)) {
|
|
343
|
+
if (original.endsWith(".js")) {
|
|
344
|
+
// Match src="/path" or src="path" but ensure we stay within the tag
|
|
345
|
+
const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
346
|
+
const pattern = new RegExp(`(<script[^>]*?\\ssrc=["'])/?${escaped}(["'])`, "gi");
|
|
347
|
+
|
|
348
|
+
const newHtml = html.replace(pattern, `$1/${hashed}$2`);
|
|
349
|
+
if (newHtml !== html) {
|
|
350
|
+
html = newHtml;
|
|
351
|
+
updated = true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (updated) {
|
|
357
|
+
await writeFile(htmlFile, html, "utf8");
|
|
358
|
+
}
|
|
359
|
+
} catch (err) {
|
|
360
|
+
console.error(kolor.red(`Failed to update asset references in ${relative(outDir, htmlFile)}: ${err.message}`));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return assetMap;
|
|
365
|
+
}
|
|
366
|
+
|
|
261
367
|
async function build(cwdArg = cwd) {
|
|
262
368
|
const config = await loadConfig(cwdArg);
|
|
263
369
|
const srcDir = resolve(cwdArg, config.srcDir || "src");
|
|
@@ -268,7 +374,23 @@ async function build(cwdArg = cwd) {
|
|
|
268
374
|
const publicDir = resolve(cwdArg, "public");
|
|
269
375
|
const outDir = resolve(cwdArg, config.outDir || "dist");
|
|
270
376
|
const env = createNunjucksEnv(layoutsDir, pagesDir, srcDir, partialsDir);
|
|
377
|
+
// Allow user config to extend the Nunjucks environment (e.g., custom filters)
|
|
378
|
+
if (config?.hooks?.nunjucksEnv && typeof config.hooks.nunjucksEnv === "function") {
|
|
379
|
+
try {
|
|
380
|
+
config.hooks.nunjucksEnv(env);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
console.error(kolor.red(`Failed to apply nunjucksEnv hook: ${err.message}`));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
271
385
|
const liquidEnv = createLiquidEnv(layoutsDir, pagesDir, srcDir, partialsDir);
|
|
386
|
+
// Allow user config to extend the Liquid environment (e.g., custom filters)
|
|
387
|
+
if (config?.hooks?.liquidEnv && typeof config.hooks.liquidEnv === "function") {
|
|
388
|
+
try {
|
|
389
|
+
config.hooks.liquidEnv(liquidEnv);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error(kolor.red(`Failed to apply liquidEnv hook: ${err.message}`));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
272
394
|
const data = await loadData(dataDir);
|
|
273
395
|
|
|
274
396
|
await cleanDir(outDir);
|
|
@@ -292,6 +414,17 @@ async function build(cwdArg = cwd) {
|
|
|
292
414
|
console.log(kolor.green("HTML minified"));
|
|
293
415
|
}
|
|
294
416
|
|
|
417
|
+
if (config.cacheBustAssets) {
|
|
418
|
+
console.log(kolor.cyan("Cache-busting assets..."));
|
|
419
|
+
const assetMap = await cacheBustAssets(outDir);
|
|
420
|
+
const assetCount = Object.keys(assetMap).length;
|
|
421
|
+
if (assetCount > 0) {
|
|
422
|
+
console.log(kolor.green(`✓ Cache-busted ${assetCount} asset(s)`));
|
|
423
|
+
} else {
|
|
424
|
+
console.log(kolor.yellow("No assets found to cache-bust"));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
295
428
|
console.log(kolor.green(`Built ${files.length} page(s) → ${relative(cwdArg, outDir)}`));
|
|
296
429
|
}
|
|
297
430
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "basecampjs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
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": {
|