basecampjs 0.0.1 → 0.0.3

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 +81 -9
  2. package/package.json +3 -1
package/index.js CHANGED
@@ -11,6 +11,8 @@ import matter from "gray-matter";
11
11
  import MarkdownIt from "markdown-it";
12
12
  import nunjucks from "nunjucks";
13
13
  import { Liquid } from "liquidjs";
14
+ import { minify as minifyCss } from "csso";
15
+ import { minify as minifyHtml } from "html-minifier-terser";
14
16
 
15
17
  const cwd = process.cwd();
16
18
  const md = new MarkdownIt({ html: true, linkify: true, typographer: true });
@@ -21,6 +23,8 @@ const defaultConfig = {
21
23
  outDir: "dist",
22
24
  templateEngine: "nunjucks",
23
25
  markdown: true,
26
+ minifyCSS: false,
27
+ minifyHTML: false,
24
28
  integrations: { nunjucks: true, liquid: false, vue: false, alpine: false }
25
29
  };
26
30
 
@@ -69,6 +73,45 @@ async function copyPublic(publicDir, outDir) {
69
73
  }
70
74
  }
71
75
 
76
+ async function minifyCSSFiles(outDir) {
77
+ const files = await walkFiles(outDir);
78
+ const cssFiles = files.filter((file) => extname(file).toLowerCase() === ".css");
79
+
80
+ await Promise.all(cssFiles.map(async (file) => {
81
+ try {
82
+ const css = await readFile(file, "utf8");
83
+ const { css: minified } = minifyCss(css);
84
+ await writeFile(file, minified, "utf8");
85
+ } catch (err) {
86
+ console.error(kolor.red(`Failed to minify CSS ${relative(outDir, file)}: ${err.message}`));
87
+ }
88
+ }));
89
+ }
90
+
91
+ async function minifyHTMLFiles(outDir, config) {
92
+ const files = await walkFiles(outDir);
93
+ const htmlFiles = files.filter((file) => extname(file).toLowerCase() === ".html");
94
+
95
+ await Promise.all(htmlFiles.map(async (file) => {
96
+ try {
97
+ const html = await readFile(file, "utf8");
98
+ const minified = await minifyHtml(html, {
99
+ collapseWhitespace: true,
100
+ removeComments: true,
101
+ minifyCSS: !!config.minifyCSS,
102
+ minifyJS: true,
103
+ keepClosingSlash: true,
104
+ removeRedundantAttributes: true,
105
+ removeScriptTypeAttributes: true,
106
+ removeStyleLinkTypeAttributes: true
107
+ });
108
+ await writeFile(file, minified, "utf8");
109
+ } catch (err) {
110
+ console.error(kolor.red(`Failed to minify HTML ${relative(outDir, file)}: ${err.message}`));
111
+ }
112
+ }));
113
+ }
114
+
72
115
  async function walkFiles(dir) {
73
116
  const results = [];
74
117
  if (!existsSync(dir)) return results;
@@ -102,10 +145,19 @@ function createLiquidEnv(layoutsDir, pagesDir, srcDir) {
102
145
  });
103
146
  }
104
147
 
105
- function pageContext(frontmatter, html, config, relPath, data) {
148
+ function toUrlPath(outRel) {
149
+ const normalized = outRel.replace(/\\/g, "/");
150
+ if (normalized.endsWith("index.html")) {
151
+ const trimmed = normalized.slice(0, -"index.html".length);
152
+ return trimmed ? `/${trimmed}` : "/";
153
+ }
154
+ return `/${normalized}`;
155
+ }
156
+
157
+ function pageContext(frontmatter, html, config, relPath, data, url = "/") {
106
158
  return {
107
159
  site: { name: config.siteName, config },
108
- page: { ...frontmatter, content: html, source: relPath },
160
+ page: { ...frontmatter, content: html, source: relPath, url },
109
161
  frontmatter,
110
162
  content: html,
111
163
  data,
@@ -145,13 +197,14 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
145
197
  const ext = extname(filePath).toLowerCase();
146
198
  const outRel = rel.replace(/\.liquid(\.html)?$/i, ".html").replace(ext, ".html");
147
199
  const outPath = join(outDir, outRel);
200
+ const url = toUrlPath(outRel);
148
201
  await ensureDir(dirname(outPath));
149
202
 
150
203
  if (ext === ".md") {
151
204
  const raw = await readFile(filePath, "utf8");
152
205
  const parsed = matter(raw);
153
206
  const html = md.render(parsed.content);
154
- const ctx = pageContext(parsed.data, html, config, rel, data);
207
+ const ctx = pageContext(parsed.data, html, config, rel, data, url);
155
208
  const rendered = await renderWithLayout(parsed.data.layout, html, ctx, env, liquidEnv);
156
209
  await writeFile(outPath, rendered, "utf8");
157
210
  return;
@@ -160,7 +213,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
160
213
  if (ext === ".njk") {
161
214
  const raw = await readFile(filePath, "utf8");
162
215
  const parsed = matter(raw);
163
- const ctx = pageContext(parsed.data, parsed.content, config, rel, data);
216
+ const ctx = pageContext(parsed.data, parsed.content, config, rel, data, url);
164
217
  const templateName = rel.replace(/\\/g, "/");
165
218
  let pageHtml = env.renderString(parsed.content, ctx, { path: templateName });
166
219
  if (shouldRenderMarkdown(parsed.data, config, false)) {
@@ -174,7 +227,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
174
227
  if (ext === ".liquid" || filePath.toLowerCase().endsWith(".liquid.html")) {
175
228
  const raw = await readFile(filePath, "utf8");
176
229
  const parsed = matter(raw);
177
- const ctx = pageContext(parsed.data, parsed.content, config, rel, data);
230
+ const ctx = pageContext(parsed.data, parsed.content, config, rel, data, url);
178
231
  let pageHtml = await liquidEnv.parseAndRender(parsed.content, ctx);
179
232
  if (shouldRenderMarkdown(parsed.data, config, false)) {
180
233
  pageHtml = md.render(pageHtml);
@@ -187,7 +240,7 @@ async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidE
187
240
  if (ext === ".html") {
188
241
  const raw = await readFile(filePath, "utf8");
189
242
  const parsed = matter(raw);
190
- const ctx = pageContext(parsed.data, parsed.content, config, rel, data);
243
+ const ctx = pageContext(parsed.data, parsed.content, config, rel, data, url);
191
244
  let pageHtml = parsed.content;
192
245
  if (shouldRenderMarkdown(parsed.data, config, false)) {
193
246
  pageHtml = md.render(pageHtml);
@@ -223,6 +276,16 @@ async function build(cwdArg = cwd) {
223
276
 
224
277
  await Promise.all(files.map((file) => renderPage(file, { pagesDir, layoutsDir, outDir, env, liquidEnv, config, data })));
225
278
 
279
+ if (config.minifyCSS) {
280
+ await minifyCSSFiles(outDir);
281
+ console.log(kolor.green("CSS minified"));
282
+ }
283
+
284
+ if (config.minifyHTML) {
285
+ await minifyHTMLFiles(outDir, config);
286
+ console.log(kolor.green("HTML minified"));
287
+ }
288
+
226
289
  console.log(kolor.green(`Built ${files.length} page(s) → ${relative(cwdArg, outDir)}`));
227
290
  }
228
291
 
@@ -244,7 +307,11 @@ function serve(outDir, port = 4173) {
244
307
  const server = createServer(async (req, res) => {
245
308
  const urlPath = decodeURI((req.url || "/").split("?")[0]);
246
309
  const safePath = urlPath.replace(/\.\.+/g, "");
247
- let filePath = join(outDir, safePath);
310
+ const requestPath = safePath.replace(/^\/+/, "") || "index.html";
311
+ let filePath = join(outDir, requestPath);
312
+ const notFoundPath = join(outDir, "404.html");
313
+ const indexPath = join(outDir, "index.html");
314
+ let isNotFoundResponse = false;
248
315
  let stats;
249
316
 
250
317
  try {
@@ -254,13 +321,18 @@ function serve(outDir, port = 4173) {
254
321
  stats = await stat(filePath);
255
322
  }
256
323
  } catch {
257
- filePath = join(outDir, "index.html");
324
+ if (existsSync(notFoundPath)) {
325
+ filePath = notFoundPath;
326
+ isNotFoundResponse = true;
327
+ } else {
328
+ filePath = indexPath;
329
+ }
258
330
  }
259
331
 
260
332
  try {
261
333
  const data = await readFile(filePath);
262
334
  const type = mime[extname(filePath).toLowerCase()] || "text/plain";
263
- res.writeHead(200, { "Content-Type": type });
335
+ res.writeHead(isNotFoundResponse ? 404 : 200, { "Content-Type": type });
264
336
  res.end(data);
265
337
  } catch {
266
338
  res.writeHead(404, { "Content-Type": "text/plain" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basecampjs",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "description": "BasecampJS engine for Campsite static site generator.",
6
6
  "bin": {
@@ -11,6 +11,8 @@
11
11
  ".": "./index.js"
12
12
  },
13
13
  "dependencies": {
14
+ "csso": "^5.0.5",
15
+ "html-minifier-terser": "^7.2.0",
14
16
  "chokidar": "^3.6.0",
15
17
  "gray-matter": "^4.0.3",
16
18
  "kolorist": "^1.8.0",