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.
- package/index.js +81 -9
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|