basecampjs 0.0.9 → 0.0.11
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 +196 -29
- package/package.json +4 -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
|
|
|
@@ -61,7 +70,7 @@ function showHelp() {
|
|
|
61
70
|
console.log(kolor.dim("Build and manage your static campsite.\n"));
|
|
62
71
|
|
|
63
72
|
console.log(kolor.bold("Usage:"));
|
|
64
|
-
console.log("
|
|
73
|
+
console.log(" camper <command> [arguments] [options]\n");
|
|
65
74
|
|
|
66
75
|
console.log(kolor.bold("Project Commands:"));
|
|
67
76
|
console.log(" " + kolor.cyan("init") + " Initialize a new Campsite project in current directory");
|
|
@@ -101,15 +110,15 @@ function showHelp() {
|
|
|
101
110
|
|
|
102
111
|
console.log(kolor.bold("Examples:"));
|
|
103
112
|
console.log(" " + kolor.dim("# Initialize a new project"));
|
|
104
|
-
console.log("
|
|
113
|
+
console.log(" camper init\n");
|
|
105
114
|
console.log(" " + kolor.dim("# Start development"));
|
|
106
|
-
console.log("
|
|
115
|
+
console.log(" camper dev\n");
|
|
107
116
|
console.log(" " + kolor.dim("# Create new content"));
|
|
108
|
-
console.log("
|
|
109
|
-
console.log("
|
|
110
|
-
console.log("
|
|
117
|
+
console.log(" camper make:page about");
|
|
118
|
+
console.log(" camper make:post \"My First Post\"");
|
|
119
|
+
console.log(" camper make:collection products\n");
|
|
111
120
|
console.log(" " + kolor.dim("# Build and preview"));
|
|
112
|
-
console.log("
|
|
121
|
+
console.log(" camper preview\n");
|
|
113
122
|
console.log(kolor.dim("For more information, visit: https://campsitejs.dev"));
|
|
114
123
|
console.log();
|
|
115
124
|
}
|
|
@@ -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) {
|
|
@@ -622,17 +789,17 @@ async function makeContent(type) {
|
|
|
622
789
|
|
|
623
790
|
if (args.length === 0) {
|
|
624
791
|
console.log(kolor.red("❌ Missing name argument"));
|
|
625
|
-
console.log(kolor.dim(`Usage:
|
|
792
|
+
console.log(kolor.dim(`Usage: camper make:${type} <name> [name2, name3, ...]`));
|
|
626
793
|
console.log(kolor.dim("\nExamples:"));
|
|
627
|
-
console.log(kolor.dim("
|
|
628
|
-
console.log(kolor.dim("
|
|
629
|
-
console.log(kolor.dim("
|
|
794
|
+
console.log(kolor.dim(" camper make:page about"));
|
|
795
|
+
console.log(kolor.dim(" camper make:page home, about, contact"));
|
|
796
|
+
console.log(kolor.dim(" camper make:collection products, categories\n"));
|
|
630
797
|
exit(1);
|
|
631
798
|
}
|
|
632
799
|
|
|
633
800
|
// Join all args and split by comma to support both formats:
|
|
634
|
-
//
|
|
635
|
-
//
|
|
801
|
+
// camper make:page home about contact
|
|
802
|
+
// camper make:page home, about, contact
|
|
636
803
|
const namesString = args.join(" ");
|
|
637
804
|
const names = namesString.split(",").map(n => n.trim()).filter(n => n.length > 0);
|
|
638
805
|
|
|
@@ -857,7 +1024,7 @@ async function init() {
|
|
|
857
1024
|
// Check if already initialized
|
|
858
1025
|
if (existsSync(join(targetDir, "campsite.config.js"))) {
|
|
859
1026
|
console.log(kolor.yellow("⚠️ This directory already has a campsite.config.js file."));
|
|
860
|
-
console.log(kolor.dim("Run '
|
|
1027
|
+
console.log(kolor.dim("Run 'camper dev' to start developing.\n"));
|
|
861
1028
|
return;
|
|
862
1029
|
}
|
|
863
1030
|
|
|
@@ -920,7 +1087,7 @@ Your cozy static site is ready to build.
|
|
|
920
1087
|
|
|
921
1088
|
## Get Started
|
|
922
1089
|
|
|
923
|
-
- Run \`
|
|
1090
|
+
- Run \`camper dev\` to start developing
|
|
924
1091
|
- Edit pages in \`src/pages/\`
|
|
925
1092
|
- Customize layouts in \`src/layouts/\`
|
|
926
1093
|
|
|
@@ -961,10 +1128,10 @@ dist/
|
|
|
961
1128
|
version: "0.0.1",
|
|
962
1129
|
type: "module",
|
|
963
1130
|
scripts: {
|
|
964
|
-
dev: "
|
|
965
|
-
build: "
|
|
966
|
-
serve: "
|
|
967
|
-
preview: "
|
|
1131
|
+
dev: "camper dev",
|
|
1132
|
+
build: "camper build",
|
|
1133
|
+
serve: "camper serve",
|
|
1134
|
+
preview: "camper preview"
|
|
968
1135
|
},
|
|
969
1136
|
dependencies: {
|
|
970
1137
|
basecampjs: "^0.0.8"
|
|
@@ -975,7 +1142,7 @@ dist/
|
|
|
975
1142
|
console.log(kolor.green("✅ Campsite initialized successfully!\n"));
|
|
976
1143
|
console.log(kolor.bold("Next steps:"));
|
|
977
1144
|
console.log(kolor.dim(" 1. Install dependencies: npm install"));
|
|
978
|
-
console.log(kolor.dim(" 2. Start developing:
|
|
1145
|
+
console.log(kolor.dim(" 2. Start developing: camper dev\n"));
|
|
979
1146
|
}
|
|
980
1147
|
|
|
981
1148
|
async function clean() {
|
|
@@ -1000,7 +1167,7 @@ async function check() {
|
|
|
1000
1167
|
const configPath = join(cwd, "campsite.config.js");
|
|
1001
1168
|
if (!existsSync(configPath)) {
|
|
1002
1169
|
console.log(kolor.red("❌ campsite.config.js not found"));
|
|
1003
|
-
console.log(kolor.dim(" Run '
|
|
1170
|
+
console.log(kolor.dim(" Run 'camper init' to initialize a project\n"));
|
|
1004
1171
|
hasIssues = true;
|
|
1005
1172
|
} else {
|
|
1006
1173
|
console.log(kolor.green("✅ campsite.config.js found"));
|
|
@@ -1133,7 +1300,7 @@ async function upgrade() {
|
|
|
1133
1300
|
} catch {}
|
|
1134
1301
|
|
|
1135
1302
|
console.log();
|
|
1136
|
-
console.log(kolor.dim("🌲 Tip: Run '
|
|
1303
|
+
console.log(kolor.dim("🌲 Tip: Run 'camper dev' to start developing with the latest version\n"));
|
|
1137
1304
|
resolve();
|
|
1138
1305
|
} else {
|
|
1139
1306
|
console.log();
|
|
@@ -1236,7 +1403,7 @@ async function list() {
|
|
|
1236
1403
|
}
|
|
1237
1404
|
}
|
|
1238
1405
|
|
|
1239
|
-
console.log(kolor.dim("🌲 Tip: Use '
|
|
1406
|
+
console.log(kolor.dim("🌲 Tip: Use 'camper make:<type> <name>' to create new content\n"));
|
|
1240
1407
|
}
|
|
1241
1408
|
|
|
1242
1409
|
async function preview() {
|
|
@@ -1269,7 +1436,7 @@ async function main() {
|
|
|
1269
1436
|
const type = command.substring(5); // Remove 'make:' prefix
|
|
1270
1437
|
if (!type) {
|
|
1271
1438
|
console.log(kolor.red("❌ No type specified"));
|
|
1272
|
-
console.log(kolor.dim("Run '
|
|
1439
|
+
console.log(kolor.dim("Run 'camper --help' for available make commands.\n"));
|
|
1273
1440
|
exit(1);
|
|
1274
1441
|
}
|
|
1275
1442
|
await makeContent(type);
|
|
@@ -1312,7 +1479,7 @@ async function main() {
|
|
|
1312
1479
|
break;
|
|
1313
1480
|
default:
|
|
1314
1481
|
console.log(kolor.yellow(`Unknown command: ${command}`));
|
|
1315
|
-
console.log(kolor.dim("Run '
|
|
1482
|
+
console.log(kolor.dim("Run 'camper --help' for usage information."));
|
|
1316
1483
|
exit(1);
|
|
1317
1484
|
}
|
|
1318
1485
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "basecampjs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "BasecampJS engine for CampsiteJS static site generator.",
|
|
6
6
|
"bin": {
|
|
7
|
+
"camper": "./index.js",
|
|
7
8
|
"campsite": "./index.js"
|
|
8
9
|
},
|
|
9
10
|
"license": "MIT",
|
|
@@ -19,7 +20,8 @@
|
|
|
19
20
|
"liquidjs": "^10.12.0",
|
|
20
21
|
"markdown-it": "^14.1.0",
|
|
21
22
|
"mustache": "^4.2.0",
|
|
22
|
-
"nunjucks": "^3.2.4"
|
|
23
|
+
"nunjucks": "^3.2.4",
|
|
24
|
+
"sharp": "^0.33.5"
|
|
23
25
|
},
|
|
24
26
|
"engines": {
|
|
25
27
|
"node": ">=18"
|