basecampjs 0.0.9 → 0.0.10
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 +171 -4
- package/package.json +3 -2
package/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import nunjucks from "nunjucks";
|
|
|
15
15
|
import { Liquid } from "liquidjs";
|
|
16
16
|
import { minify as minifyCss } from "csso";
|
|
17
17
|
import { minify as minifyHtml } from "html-minifier-terser";
|
|
18
|
+
import sharp from "sharp";
|
|
18
19
|
|
|
19
20
|
const cwd = process.cwd();
|
|
20
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -29,6 +30,14 @@ const defaultConfig = {
|
|
|
29
30
|
minifyCSS: false,
|
|
30
31
|
minifyHTML: false,
|
|
31
32
|
cacheBustAssets: false,
|
|
33
|
+
excludeFiles: [],
|
|
34
|
+
compressPhotos: false,
|
|
35
|
+
compressionSettings: {
|
|
36
|
+
quality: 80,
|
|
37
|
+
formats: [".webp"],
|
|
38
|
+
inputFormats: [".jpg", ".jpeg", ".png"],
|
|
39
|
+
preserveOriginal: true
|
|
40
|
+
},
|
|
32
41
|
integrations: { nunjucks: true, liquid: false, mustache: false, vue: false, alpine: false }
|
|
33
42
|
};
|
|
34
43
|
|
|
@@ -145,12 +154,166 @@ async function cleanDir(dir) {
|
|
|
145
154
|
await mkdir(dir, { recursive: true });
|
|
146
155
|
}
|
|
147
156
|
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
|
|
157
|
+
function shouldExcludeFile(filePath, excludePatterns) {
|
|
158
|
+
if (!excludePatterns || excludePatterns.length === 0) return false;
|
|
159
|
+
|
|
160
|
+
const fileName = basename(filePath).toLowerCase();
|
|
161
|
+
const ext = extname(filePath).toLowerCase();
|
|
162
|
+
|
|
163
|
+
return excludePatterns.some(pattern => {
|
|
164
|
+
const normalized = pattern.toLowerCase();
|
|
165
|
+
// Support extension patterns like '.pdf' or 'pdf'
|
|
166
|
+
if (normalized.startsWith('.')) {
|
|
167
|
+
return ext === normalized;
|
|
168
|
+
}
|
|
169
|
+
if (normalized.startsWith('*.')) {
|
|
170
|
+
return ext === normalized.slice(1);
|
|
171
|
+
}
|
|
172
|
+
// Support exact filename matches
|
|
173
|
+
if (fileName === normalized) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
// Support glob-like patterns with wildcards
|
|
177
|
+
if (normalized.includes('*')) {
|
|
178
|
+
const regex = new RegExp('^' + normalized.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
179
|
+
return regex.test(fileName);
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function copyPublic(publicDir, outDir, excludePatterns = []) {
|
|
186
|
+
if (!existsSync(publicDir)) return;
|
|
187
|
+
|
|
188
|
+
const files = await walkFiles(publicDir);
|
|
189
|
+
for (const file of files) {
|
|
190
|
+
const rel = relative(publicDir, file);
|
|
191
|
+
|
|
192
|
+
// Skip excluded files
|
|
193
|
+
if (shouldExcludeFile(file, excludePatterns)) {
|
|
194
|
+
console.log(kolor.dim(`Skipping excluded file: ${rel}`));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const destPath = join(outDir, rel);
|
|
199
|
+
await ensureDir(dirname(destPath));
|
|
200
|
+
await cp(file, destPath);
|
|
151
201
|
}
|
|
152
202
|
}
|
|
153
203
|
|
|
204
|
+
function shouldProcessImage(filePath, config) {
|
|
205
|
+
if (!config.compressPhotos) return false;
|
|
206
|
+
const ext = extname(filePath).toLowerCase();
|
|
207
|
+
const inputFormats = config.compressionSettings?.inputFormats || [".jpg", ".jpeg", ".png"];
|
|
208
|
+
return inputFormats.includes(ext);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function processImage(inputPath, outDir, settings) {
|
|
212
|
+
const ext = extname(inputPath);
|
|
213
|
+
const baseName = basename(inputPath, ext);
|
|
214
|
+
const dir = dirname(inputPath);
|
|
215
|
+
const relDir = relative(outDir, dir);
|
|
216
|
+
|
|
217
|
+
const results = [];
|
|
218
|
+
const quality = settings.quality || 80;
|
|
219
|
+
const formats = settings.formats || [".webp"];
|
|
220
|
+
|
|
221
|
+
for (const format of formats) {
|
|
222
|
+
try {
|
|
223
|
+
const outputName = `${baseName}${format}`;
|
|
224
|
+
const outputPath = join(dir, outputName);
|
|
225
|
+
|
|
226
|
+
const sharpInstance = sharp(inputPath);
|
|
227
|
+
|
|
228
|
+
if (format === ".webp") {
|
|
229
|
+
await sharpInstance.webp({ quality }).toFile(outputPath);
|
|
230
|
+
} else if (format === ".avif") {
|
|
231
|
+
await sharpInstance.avif({ quality }).toFile(outputPath);
|
|
232
|
+
} else if (format === ".jpg" || format === ".jpeg") {
|
|
233
|
+
await sharpInstance.jpeg({ quality }).toFile(outputPath);
|
|
234
|
+
} else if (format === ".png") {
|
|
235
|
+
await sharpInstance.png({ quality }).toFile(outputPath);
|
|
236
|
+
} else {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const stats = await stat(outputPath);
|
|
241
|
+
results.push({
|
|
242
|
+
path: outputPath,
|
|
243
|
+
format,
|
|
244
|
+
size: stats.size
|
|
245
|
+
});
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.error(kolor.red(`Failed to convert ${basename(inputPath)} to ${format}: ${err.message}`));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function processImages(outDir, config) {
|
|
255
|
+
if (!config.compressPhotos) return;
|
|
256
|
+
|
|
257
|
+
const settings = {
|
|
258
|
+
quality: config.compressionSettings?.quality || 80,
|
|
259
|
+
formats: config.compressionSettings?.formats || [".webp"],
|
|
260
|
+
preserveOriginal: config.compressionSettings?.preserveOriginal !== false
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
console.log(kolor.cyan("🖼️ Processing images..."));
|
|
264
|
+
|
|
265
|
+
const files = await walkFiles(outDir);
|
|
266
|
+
const imageFiles = files.filter((file) => shouldProcessImage(file, config));
|
|
267
|
+
|
|
268
|
+
if (imageFiles.length === 0) {
|
|
269
|
+
console.log(kolor.dim("No images found to process"));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let totalGenerated = 0;
|
|
274
|
+
let totalOriginalSize = 0;
|
|
275
|
+
let totalConvertedSize = 0;
|
|
276
|
+
|
|
277
|
+
await Promise.all(imageFiles.map(async (file) => {
|
|
278
|
+
const originalStats = await stat(file);
|
|
279
|
+
totalOriginalSize += originalStats.size;
|
|
280
|
+
|
|
281
|
+
const results = await processImage(file, outDir, settings);
|
|
282
|
+
|
|
283
|
+
if (results.length > 0) {
|
|
284
|
+
const formats = results.map(r => r.format.slice(1)).join(", ");
|
|
285
|
+
const rel = relative(outDir, file);
|
|
286
|
+
console.log(kolor.dim(` ${rel} → ${formats}`));
|
|
287
|
+
|
|
288
|
+
results.forEach(r => {
|
|
289
|
+
totalConvertedSize += r.size;
|
|
290
|
+
totalGenerated++;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Remove original if preserveOriginal is false
|
|
295
|
+
if (!settings.preserveOriginal && results.length > 0) {
|
|
296
|
+
await rm(file, { force: true });
|
|
297
|
+
}
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
const savedBytes = totalOriginalSize - totalConvertedSize;
|
|
301
|
+
const savedPercent = totalOriginalSize > 0 ? ((savedBytes / totalOriginalSize) * 100).toFixed(1) : 0;
|
|
302
|
+
|
|
303
|
+
console.log(kolor.green(`✓ Generated ${totalGenerated} image(s)`));
|
|
304
|
+
if (!settings.preserveOriginal) {
|
|
305
|
+
console.log(kolor.green(` Saved ${formatBytes(savedBytes)} (${savedPercent}% reduction)`));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function formatBytes(bytes) {
|
|
310
|
+
if (bytes === 0) return "0 B";
|
|
311
|
+
const k = 1024;
|
|
312
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
313
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
314
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
315
|
+
}
|
|
316
|
+
|
|
154
317
|
async function minifyCSSFiles(outDir) {
|
|
155
318
|
const files = await walkFiles(outDir);
|
|
156
319
|
const cssFiles = files.filter((file) => extname(file).toLowerCase() === ".css");
|
|
@@ -470,7 +633,11 @@ async function build(cwdArg = cwd) {
|
|
|
470
633
|
const data = await loadData([dataDir, collectionsDir]);
|
|
471
634
|
|
|
472
635
|
await cleanDir(outDir);
|
|
473
|
-
await copyPublic(publicDir, outDir);
|
|
636
|
+
await copyPublic(publicDir, outDir, config.excludeFiles);
|
|
637
|
+
|
|
638
|
+
if (config.compressPhotos) {
|
|
639
|
+
await processImages(outDir, config);
|
|
640
|
+
}
|
|
474
641
|
|
|
475
642
|
const files = await walkFiles(pagesDir);
|
|
476
643
|
if (files.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "basecampjs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "BasecampJS engine for CampsiteJS static site generator.",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"liquidjs": "^10.12.0",
|
|
20
20
|
"markdown-it": "^14.1.0",
|
|
21
21
|
"mustache": "^4.2.0",
|
|
22
|
-
"nunjucks": "^3.2.4"
|
|
22
|
+
"nunjucks": "^3.2.4",
|
|
23
|
+
"sharp": "^0.33.5"
|
|
23
24
|
},
|
|
24
25
|
"engines": {
|
|
25
26
|
"node": ">=18"
|