basecampjs 0.0.5 → 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.
Files changed (2) hide show
  1. package/index.js +150 -14
  2. 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
- integrations: { nunjucks: true, liquid: false, vue: false, alpine: false }
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) {
@@ -128,18 +131,20 @@ async function walkFiles(dir) {
128
131
  return results;
129
132
  }
130
133
 
131
- function createNunjucksEnv(layoutsDir, pagesDir, srcDir) {
132
- // Allow templates to resolve from layouts, pages, or the src root
134
+ function createNunjucksEnv(layoutsDir, pagesDir, srcDir, partialsDir) {
135
+ // Allow templates to resolve from layouts, partials, pages, or the src root
136
+ const searchPaths = [layoutsDir, partialsDir, pagesDir, srcDir].filter(Boolean);
133
137
  return new nunjucks.Environment(
134
- new nunjucks.FileSystemLoader([layoutsDir, pagesDir, srcDir], { noCache: true }),
138
+ new nunjucks.FileSystemLoader(searchPaths, { noCache: true }),
135
139
  { autoescape: false }
136
140
  );
137
141
  }
138
142
 
139
- function createLiquidEnv(layoutsDir, pagesDir, srcDir) {
143
+ function createLiquidEnv(layoutsDir, pagesDir, srcDir, partialsDir) {
140
144
  // Liquid loader will search these roots for partials/layouts
145
+ const root = [layoutsDir, partialsDir, pagesDir, srcDir].filter(Boolean);
141
146
  return new Liquid({
142
- root: [layoutsDir, pagesDir, srcDir],
147
+ root,
143
148
  extname: ".liquid",
144
149
  cache: false
145
150
  });
@@ -173,7 +178,7 @@ function shouldRenderMarkdown(frontmatter, config, defaultValue) {
173
178
  return defaultValue;
174
179
  }
175
180
 
176
- async function renderWithLayout(layoutName, html, ctx, env, liquidEnv) {
181
+ async function renderWithLayout(layoutName, html, ctx, env, liquidEnv, layoutsDir) {
177
182
  if (!layoutName) return html;
178
183
  const ext = extname(layoutName).toLowerCase();
179
184
  const layoutCtx = {
@@ -191,6 +196,14 @@ async function renderWithLayout(layoutName, html, ctx, env, liquidEnv) {
191
196
  return liquidEnv.renderFile(layoutName, layoutCtx);
192
197
  }
193
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
+
194
207
  // Unknown layout type, return unwrapped content
195
208
  return html;
196
209
  }
@@ -208,7 +221,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
208
221
  const parsed = matter(raw);
209
222
  const html = md.render(parsed.content);
210
223
  const ctx = pageContext(parsed.data, html, config, rel, data, path);
211
- const rendered = await renderWithLayout(parsed.data.layout, html, ctx, env, liquidEnv);
224
+ const rendered = await renderWithLayout(parsed.data.layout, html, ctx, env, liquidEnv, layoutsDir);
212
225
  await writeFile(outPath, rendered, "utf8");
213
226
  return;
214
227
  }
@@ -222,7 +235,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
222
235
  if (shouldRenderMarkdown(parsed.data, config, false)) {
223
236
  pageHtml = md.render(pageHtml);
224
237
  }
225
- const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv);
238
+ const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir);
226
239
  await writeFile(outPath, rendered, "utf8");
227
240
  return;
228
241
  }
@@ -235,7 +248,20 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
235
248
  if (shouldRenderMarkdown(parsed.data, config, false)) {
236
249
  pageHtml = md.render(pageHtml);
237
250
  }
238
- 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);
239
265
  await writeFile(outPath, rendered, "utf8");
240
266
  return;
241
267
  }
@@ -248,7 +274,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
248
274
  if (shouldRenderMarkdown(parsed.data, config, false)) {
249
275
  pageHtml = md.render(pageHtml);
250
276
  }
251
- const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv);
277
+ const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir);
252
278
  await writeFile(outPath, rendered, "utf8");
253
279
  return;
254
280
  }
@@ -256,16 +282,115 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
256
282
  await cp(filePath, outPath);
257
283
  }
258
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
+
259
367
  async function build(cwdArg = cwd) {
260
368
  const config = await loadConfig(cwdArg);
261
369
  const srcDir = resolve(cwdArg, config.srcDir || "src");
262
370
  const pagesDir = join(srcDir, "pages");
263
371
  const layoutsDir = join(srcDir, "layouts");
372
+ const partialsDir = join(srcDir, "partials");
264
373
  const dataDir = join(srcDir, "data");
265
374
  const publicDir = resolve(cwdArg, "public");
266
375
  const outDir = resolve(cwdArg, config.outDir || "dist");
267
- const env = createNunjucksEnv(layoutsDir, pagesDir, srcDir);
268
- const liquidEnv = createLiquidEnv(layoutsDir, pagesDir, srcDir);
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
+ }
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
+ }
269
394
  const data = await loadData(dataDir);
270
395
 
271
396
  await cleanDir(outDir);
@@ -289,6 +414,17 @@ async function build(cwdArg = cwd) {
289
414
  console.log(kolor.green("HTML minified"));
290
415
  }
291
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
+
292
428
  console.log(kolor.green(`Built ${files.length} page(s) → ${relative(cwdArg, outDir)}`));
293
429
  }
294
430
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basecampjs",
3
- "version": "0.0.5",
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": {