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.
- package/index.js +150 -14
- 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) {
|
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": {
|