basecampjs 0.0.13 → 0.0.14
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/dist/build/assets.d.ts +36 -0
- package/dist/build/assets.d.ts.map +1 -0
- package/dist/build/assets.js +272 -0
- package/dist/build/assets.js.map +1 -0
- package/dist/build/data.d.ts +6 -0
- package/dist/build/data.d.ts.map +1 -0
- package/dist/build/data.js +33 -0
- package/dist/build/data.js.map +1 -0
- package/dist/build/pages.d.ts +19 -0
- package/dist/build/pages.d.ts.map +1 -0
- package/dist/build/pages.js +110 -0
- package/dist/build/pages.js.map +1 -0
- package/dist/build/pipeline.d.ts +6 -0
- package/dist/build/pipeline.d.ts.map +1 -0
- package/dist/build/pipeline.js +98 -0
- package/dist/build/pipeline.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +140 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +60 -0
- package/dist/config.js.map +1 -0
- package/dist/dev/server.d.ts +6 -0
- package/dist/dev/server.d.ts.map +1 -0
- package/dist/dev/server.js +64 -0
- package/dist/dev/server.js.map +1 -0
- package/dist/dev/watcher.d.ts +5 -0
- package/dist/dev/watcher.d.ts.map +1 -0
- package/dist/dev/watcher.js +49 -0
- package/dist/dev/watcher.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/render/engines.d.ts +30 -0
- package/dist/render/engines.d.ts.map +1 -0
- package/dist/render/engines.js +102 -0
- package/dist/render/engines.js.map +1 -0
- package/dist/scaffolding.d.ts +25 -0
- package/dist/scaffolding.d.ts.map +1 -0
- package/dist/scaffolding.js +596 -0
- package/dist/scaffolding.js.map +1 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/fs.d.ts +29 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +104 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/paths.d.ts +17 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +55 -0
- package/dist/utils/paths.js.map +1 -0
- package/package.json +36 -6
- package/src/build/assets.ts +314 -0
- package/src/build/data.ts +32 -0
- package/src/build/pages.ts +143 -0
- package/src/build/pipeline.ts +111 -0
- package/src/cli.ts +155 -0
- package/src/config.ts +61 -0
- package/src/dev/server.ts +66 -0
- package/src/dev/watcher.ts +52 -0
- package/src/index.ts +45 -0
- package/src/render/engines.ts +139 -0
- package/src/scaffolding.ts +656 -0
- package/src/types.ts +110 -0
- package/src/utils/fs.ts +109 -0
- package/src/utils/logger.ts +27 -0
- package/src/utils/paths.ts +56 -0
- package/index.js +0 -1532
package/index.js
DELETED
|
@@ -1,1532 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { argv, exit } from "process";
|
|
3
|
-
import { createServer } from "http";
|
|
4
|
-
import { existsSync } from "fs";
|
|
5
|
-
import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "fs/promises";
|
|
6
|
-
import { basename, dirname, extname, join, relative, resolve } from "path";
|
|
7
|
-
import { pathToFileURL, fileURLToPath } from "url";
|
|
8
|
-
import { createHash } from "crypto";
|
|
9
|
-
import * as kolor from "kolorist";
|
|
10
|
-
import chokidar from "chokidar";
|
|
11
|
-
import matter from "gray-matter";
|
|
12
|
-
import MarkdownIt from "markdown-it";
|
|
13
|
-
import Mustache from "mustache";
|
|
14
|
-
import nunjucks from "nunjucks";
|
|
15
|
-
import { Liquid } from "liquidjs";
|
|
16
|
-
import { minify as minifyCss } from "csso";
|
|
17
|
-
import { minify as minifyHtml } from "html-minifier-terser";
|
|
18
|
-
import sharp from "sharp";
|
|
19
|
-
|
|
20
|
-
const cwd = process.cwd();
|
|
21
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
const md = new MarkdownIt({ html: true, linkify: true, typographer: true });
|
|
23
|
-
|
|
24
|
-
const defaultConfig = {
|
|
25
|
-
siteName: "Campsite",
|
|
26
|
-
siteUrl: "https://example.com",
|
|
27
|
-
srcDir: "src",
|
|
28
|
-
outDir: "dist",
|
|
29
|
-
templateEngine: "nunjucks",
|
|
30
|
-
frontmatter: true,
|
|
31
|
-
minifyCSS: false,
|
|
32
|
-
minifyHTML: false,
|
|
33
|
-
cacheBustAssets: false,
|
|
34
|
-
excludeFiles: [],
|
|
35
|
-
compressPhotos: false,
|
|
36
|
-
compressionSettings: {
|
|
37
|
-
quality: 80,
|
|
38
|
-
formats: [".webp"],
|
|
39
|
-
inputFormats: [".jpg", ".jpeg", ".png"],
|
|
40
|
-
preserveOriginal: true
|
|
41
|
-
},
|
|
42
|
-
port: 4173,
|
|
43
|
-
integrations: { nunjucks: true, liquid: false, mustache: false, vue: false, alpine: false }
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
async function loadConfig(root) {
|
|
47
|
-
const configPath = join(root, "campsite.config.js");
|
|
48
|
-
if (!existsSync(configPath)) return { ...defaultConfig };
|
|
49
|
-
try {
|
|
50
|
-
const imported = await import(pathToFileURL(configPath));
|
|
51
|
-
const user = imported.default || imported;
|
|
52
|
-
return { ...defaultConfig, ...user };
|
|
53
|
-
} catch (err) {
|
|
54
|
-
console.error(kolor.red(`Failed to load config: ${err.message}`));
|
|
55
|
-
return { ...defaultConfig };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function getVersion() {
|
|
60
|
-
try {
|
|
61
|
-
const pkgPath = join(__dirname, "package.json");
|
|
62
|
-
const raw = await readFile(pkgPath, "utf8");
|
|
63
|
-
const pkg = JSON.parse(raw);
|
|
64
|
-
return pkg.version || "0.0.0";
|
|
65
|
-
} catch {
|
|
66
|
-
return "0.0.0";
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function showHelp() {
|
|
71
|
-
console.log(kolor.cyan(kolor.bold("\n🏕️ CampsiteJS CLI")));
|
|
72
|
-
console.log(kolor.dim("Build and manage your static campsite.\n"));
|
|
73
|
-
|
|
74
|
-
console.log(kolor.bold("Usage:"));
|
|
75
|
-
console.log(" camper <command> [arguments] [options]\n");
|
|
76
|
-
|
|
77
|
-
console.log(kolor.bold("Project Commands:"));
|
|
78
|
-
console.log(" " + kolor.cyan("init") + " Initialize a new Campsite project in current directory");
|
|
79
|
-
console.log(" Creates config, folder structure, and starter files\n");
|
|
80
|
-
|
|
81
|
-
console.log(kolor.bold("Development Commands:"));
|
|
82
|
-
console.log(" " + kolor.cyan("dev") + " Start development server with hot reloading");
|
|
83
|
-
console.log(" Watches for file changes and rebuilds automatically");
|
|
84
|
-
console.log(" " + kolor.cyan("build") + " Build your site for production");
|
|
85
|
-
console.log(" Optimizes and outputs to dist/ directory");
|
|
86
|
-
console.log(" " + kolor.cyan("serve") + " Serve the built site locally");
|
|
87
|
-
console.log(" Serves from dist/ folder on http://localhost:4173");
|
|
88
|
-
console.log(" " + kolor.cyan("preview") + " Build and serve in production mode");
|
|
89
|
-
console.log(" Combines build + serve for testing production output\n");
|
|
90
|
-
|
|
91
|
-
console.log(kolor.bold("Utility Commands:"));
|
|
92
|
-
console.log(" " + kolor.cyan("list") + " List all content (pages, layouts, components, etc.)");
|
|
93
|
-
console.log(" Overview of your project structure");
|
|
94
|
-
console.log(" " + kolor.cyan("clean") + " Remove build output directory");
|
|
95
|
-
console.log(" Deletes dist/ folder for a fresh build");
|
|
96
|
-
console.log(" " + kolor.cyan("check") + " Validate config and check for issues");
|
|
97
|
-
console.log(" Diagnoses project structure and dependencies");
|
|
98
|
-
console.log(" " + kolor.cyan("upgrade") + " Update CampsiteJS to the latest version");
|
|
99
|
-
console.log(" Checks and upgrades basecampjs and dependencies\n");
|
|
100
|
-
|
|
101
|
-
console.log(kolor.bold("Make Commands:"));
|
|
102
|
-
console.log(" " + kolor.cyan("make:page") + " " + kolor.dim("<name>") + " Create a new page in src/pages/");
|
|
103
|
-
console.log(" " + kolor.cyan("make:post") + " " + kolor.dim("<name>") + " Create a new blog post in src/pages/blog/");
|
|
104
|
-
console.log(" " + kolor.cyan("make:layout") + " " + kolor.dim("<name>") + " Create a new layout in src/layouts/");
|
|
105
|
-
console.log(" " + kolor.cyan("make:component") + " " + kolor.dim("<name>") + " Create a new component in src/components/");
|
|
106
|
-
console.log(" " + kolor.cyan("make:partial") + " " + kolor.dim("<name>") + " Create a new partial in src/partials/");
|
|
107
|
-
console.log(" " + kolor.cyan("make:collection") + " " + kolor.dim("<name>") + " Create a new JSON collection in src/collections/\n");
|
|
108
|
-
|
|
109
|
-
console.log(kolor.bold("Options:"));
|
|
110
|
-
console.log(" -h, --help Show this help message");
|
|
111
|
-
console.log(" -v, --version Show version number\n");
|
|
112
|
-
|
|
113
|
-
console.log(kolor.bold("Examples:"));
|
|
114
|
-
console.log(" " + kolor.dim("# Initialize a new project"));
|
|
115
|
-
console.log(" camper init\n");
|
|
116
|
-
console.log(" " + kolor.dim("# Start development"));
|
|
117
|
-
console.log(" camper dev\n");
|
|
118
|
-
console.log(" " + kolor.dim("# Create new content"));
|
|
119
|
-
console.log(" camper make:page about");
|
|
120
|
-
console.log(" camper make:post \"My First Post\"");
|
|
121
|
-
console.log(" camper make:collection products\n");
|
|
122
|
-
console.log(" " + kolor.dim("# Build and preview"));
|
|
123
|
-
console.log(" camper preview\n");
|
|
124
|
-
console.log(kolor.dim("For more information, visit: https://campsitejs.dev"));
|
|
125
|
-
console.log();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function ensureDir(dir) {
|
|
129
|
-
await mkdir(dir, { recursive: true });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function loadData(dataDirs) {
|
|
133
|
-
const collections = {};
|
|
134
|
-
// Support both string and array input
|
|
135
|
-
const dirs = Array.isArray(dataDirs) ? dataDirs : [dataDirs];
|
|
136
|
-
|
|
137
|
-
for (const dataDir of dirs) {
|
|
138
|
-
if (!existsSync(dataDir)) continue;
|
|
139
|
-
const files = await walkFiles(dataDir);
|
|
140
|
-
for (const file of files) {
|
|
141
|
-
if (extname(file).toLowerCase() !== ".json") continue;
|
|
142
|
-
const name = basename(file, ".json");
|
|
143
|
-
try {
|
|
144
|
-
const raw = await readFile(file, "utf8");
|
|
145
|
-
collections[name] = JSON.parse(raw);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
console.error(kolor.red(`Failed to load data ${relative(dataDir, file)}: ${err.message}`));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return collections;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async function cleanDir(dir) {
|
|
155
|
-
await rm(dir, { recursive: true, force: true });
|
|
156
|
-
await mkdir(dir, { recursive: true });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function shouldExcludeFile(filePath, excludePatterns) {
|
|
160
|
-
if (!excludePatterns || excludePatterns.length === 0) return false;
|
|
161
|
-
|
|
162
|
-
const fileName = basename(filePath).toLowerCase();
|
|
163
|
-
const ext = extname(filePath).toLowerCase();
|
|
164
|
-
|
|
165
|
-
return excludePatterns.some(pattern => {
|
|
166
|
-
const normalized = pattern.toLowerCase();
|
|
167
|
-
// Support extension patterns like '.pdf' or 'pdf'
|
|
168
|
-
if (normalized.startsWith('.')) {
|
|
169
|
-
return ext === normalized;
|
|
170
|
-
}
|
|
171
|
-
if (normalized.startsWith('*.')) {
|
|
172
|
-
return ext === normalized.slice(1);
|
|
173
|
-
}
|
|
174
|
-
// Support exact filename matches
|
|
175
|
-
if (fileName === normalized) {
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
// Support glob-like patterns with wildcards
|
|
179
|
-
if (normalized.includes('*')) {
|
|
180
|
-
const regex = new RegExp('^' + normalized.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
181
|
-
return regex.test(fileName);
|
|
182
|
-
}
|
|
183
|
-
return false;
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async function copyPublic(publicDir, outDir, excludePatterns = []) {
|
|
188
|
-
if (!existsSync(publicDir)) return;
|
|
189
|
-
|
|
190
|
-
const files = await walkFiles(publicDir);
|
|
191
|
-
for (const file of files) {
|
|
192
|
-
const rel = relative(publicDir, file);
|
|
193
|
-
|
|
194
|
-
// Skip excluded files
|
|
195
|
-
if (shouldExcludeFile(file, excludePatterns)) {
|
|
196
|
-
console.log(kolor.dim(`Skipping excluded file: ${rel}`));
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const destPath = join(outDir, rel);
|
|
201
|
-
await ensureDir(dirname(destPath));
|
|
202
|
-
await cp(file, destPath);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function shouldProcessImage(filePath, config) {
|
|
207
|
-
if (!config.compressPhotos) return false;
|
|
208
|
-
const ext = extname(filePath).toLowerCase();
|
|
209
|
-
const inputFormats = config.compressionSettings?.inputFormats || [".jpg", ".jpeg", ".png"];
|
|
210
|
-
return inputFormats.includes(ext);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function processImage(inputPath, outDir, settings) {
|
|
214
|
-
const ext = extname(inputPath);
|
|
215
|
-
const baseName = basename(inputPath, ext);
|
|
216
|
-
const dir = dirname(inputPath);
|
|
217
|
-
const relDir = relative(outDir, dir);
|
|
218
|
-
|
|
219
|
-
const results = [];
|
|
220
|
-
const quality = settings.quality || 80;
|
|
221
|
-
const formats = settings.formats || [".webp"];
|
|
222
|
-
|
|
223
|
-
for (const format of formats) {
|
|
224
|
-
try {
|
|
225
|
-
const outputName = `${baseName}${format}`;
|
|
226
|
-
const outputPath = join(dir, outputName);
|
|
227
|
-
|
|
228
|
-
const sharpInstance = sharp(inputPath);
|
|
229
|
-
|
|
230
|
-
if (format === ".webp") {
|
|
231
|
-
await sharpInstance.webp({ quality }).toFile(outputPath);
|
|
232
|
-
} else if (format === ".avif") {
|
|
233
|
-
await sharpInstance.avif({ quality }).toFile(outputPath);
|
|
234
|
-
} else if (format === ".jpg" || format === ".jpeg") {
|
|
235
|
-
await sharpInstance.jpeg({ quality }).toFile(outputPath);
|
|
236
|
-
} else if (format === ".png") {
|
|
237
|
-
await sharpInstance.png({ quality }).toFile(outputPath);
|
|
238
|
-
} else {
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const stats = await stat(outputPath);
|
|
243
|
-
results.push({
|
|
244
|
-
path: outputPath,
|
|
245
|
-
format,
|
|
246
|
-
size: stats.size
|
|
247
|
-
});
|
|
248
|
-
} catch (err) {
|
|
249
|
-
console.error(kolor.red(`Failed to convert ${basename(inputPath)} to ${format}: ${err.message}`));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return results;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async function processImages(outDir, config) {
|
|
257
|
-
if (!config.compressPhotos) return;
|
|
258
|
-
|
|
259
|
-
const settings = {
|
|
260
|
-
quality: config.compressionSettings?.quality || 80,
|
|
261
|
-
formats: config.compressionSettings?.formats || [".webp"],
|
|
262
|
-
preserveOriginal: config.compressionSettings?.preserveOriginal !== false
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
console.log(kolor.cyan("🖼️ Processing images..."));
|
|
266
|
-
|
|
267
|
-
const files = await walkFiles(outDir);
|
|
268
|
-
const imageFiles = files.filter((file) => shouldProcessImage(file, config));
|
|
269
|
-
|
|
270
|
-
if (imageFiles.length === 0) {
|
|
271
|
-
console.log(kolor.dim("No images found to process"));
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
let totalGenerated = 0;
|
|
276
|
-
let totalOriginalSize = 0;
|
|
277
|
-
let totalConvertedSize = 0;
|
|
278
|
-
|
|
279
|
-
await Promise.all(imageFiles.map(async (file) => {
|
|
280
|
-
const originalStats = await stat(file);
|
|
281
|
-
totalOriginalSize += originalStats.size;
|
|
282
|
-
|
|
283
|
-
const results = await processImage(file, outDir, settings);
|
|
284
|
-
|
|
285
|
-
if (results.length > 0) {
|
|
286
|
-
const formats = results.map(r => r.format.slice(1)).join(", ");
|
|
287
|
-
const rel = relative(outDir, file);
|
|
288
|
-
console.log(kolor.dim(` ${rel} → ${formats}`));
|
|
289
|
-
|
|
290
|
-
results.forEach(r => {
|
|
291
|
-
totalConvertedSize += r.size;
|
|
292
|
-
totalGenerated++;
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Remove original if preserveOriginal is false
|
|
297
|
-
if (!settings.preserveOriginal && results.length > 0) {
|
|
298
|
-
await rm(file, { force: true });
|
|
299
|
-
}
|
|
300
|
-
}));
|
|
301
|
-
|
|
302
|
-
const savedBytes = totalOriginalSize - totalConvertedSize;
|
|
303
|
-
const savedPercent = totalOriginalSize > 0 ? ((savedBytes / totalOriginalSize) * 100).toFixed(1) : 0;
|
|
304
|
-
|
|
305
|
-
console.log(kolor.green(`✓ Generated ${totalGenerated} image(s)`));
|
|
306
|
-
if (!settings.preserveOriginal) {
|
|
307
|
-
console.log(kolor.green(` Saved ${formatBytes(savedBytes)} (${savedPercent}% reduction)`));
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function formatBytes(bytes) {
|
|
312
|
-
if (bytes === 0) return "0 B";
|
|
313
|
-
const k = 1024;
|
|
314
|
-
const sizes = ["B", "KB", "MB", "GB"];
|
|
315
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
316
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async function minifyCSSFiles(outDir) {
|
|
320
|
-
const files = await walkFiles(outDir);
|
|
321
|
-
const cssFiles = files.filter((file) => extname(file).toLowerCase() === ".css");
|
|
322
|
-
|
|
323
|
-
await Promise.all(cssFiles.map(async (file) => {
|
|
324
|
-
try {
|
|
325
|
-
const css = await readFile(file, "utf8");
|
|
326
|
-
const { css: minified } = minifyCss(css);
|
|
327
|
-
await writeFile(file, minified, "utf8");
|
|
328
|
-
} catch (err) {
|
|
329
|
-
console.error(kolor.red(`Failed to minify CSS ${relative(outDir, file)}: ${err.message}`));
|
|
330
|
-
}
|
|
331
|
-
}));
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
async function minifyHTMLFiles(outDir, config) {
|
|
335
|
-
const files = await walkFiles(outDir);
|
|
336
|
-
const htmlFiles = files.filter((file) => extname(file).toLowerCase() === ".html");
|
|
337
|
-
|
|
338
|
-
await Promise.all(htmlFiles.map(async (file) => {
|
|
339
|
-
try {
|
|
340
|
-
const html = await readFile(file, "utf8");
|
|
341
|
-
const minified = await minifyHtml(html, {
|
|
342
|
-
collapseWhitespace: true,
|
|
343
|
-
removeComments: true,
|
|
344
|
-
minifyCSS: !!config.minifyCSS,
|
|
345
|
-
minifyJS: true,
|
|
346
|
-
keepClosingSlash: true,
|
|
347
|
-
removeRedundantAttributes: true,
|
|
348
|
-
removeScriptTypeAttributes: true,
|
|
349
|
-
removeStyleLinkTypeAttributes: true
|
|
350
|
-
});
|
|
351
|
-
await writeFile(file, minified, "utf8");
|
|
352
|
-
} catch (err) {
|
|
353
|
-
console.error(kolor.red(`Failed to minify HTML ${relative(outDir, file)}: ${err.message}`));
|
|
354
|
-
}
|
|
355
|
-
}));
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async function walkFiles(dir) {
|
|
359
|
-
const results = [];
|
|
360
|
-
if (!existsSync(dir)) return results;
|
|
361
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
362
|
-
for (const entry of entries) {
|
|
363
|
-
if (entry.name.startsWith(".")) continue;
|
|
364
|
-
const full = join(dir, entry.name);
|
|
365
|
-
if (entry.isDirectory()) {
|
|
366
|
-
results.push(...await walkFiles(full));
|
|
367
|
-
} else {
|
|
368
|
-
results.push(full);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return results;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function createNunjucksEnv(layoutsDir, pagesDir, srcDir, partialsDir) {
|
|
375
|
-
// Allow templates to resolve from layouts, partials, pages, or the src root
|
|
376
|
-
const searchPaths = [layoutsDir, partialsDir, pagesDir, srcDir].filter(Boolean);
|
|
377
|
-
return new nunjucks.Environment(
|
|
378
|
-
new nunjucks.FileSystemLoader(searchPaths, { noCache: true }),
|
|
379
|
-
{ autoescape: false }
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function createLiquidEnv(layoutsDir, pagesDir, srcDir, partialsDir) {
|
|
384
|
-
// Liquid loader will search these roots for partials/layouts
|
|
385
|
-
const root = [layoutsDir, partialsDir, pagesDir, srcDir].filter(Boolean);
|
|
386
|
-
return new Liquid({
|
|
387
|
-
root,
|
|
388
|
-
extname: ".liquid",
|
|
389
|
-
cache: false
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function loadMustachePartials(partialsDir) {
|
|
394
|
-
const partials = {};
|
|
395
|
-
if (!existsSync(partialsDir)) return partials;
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
const files = await walkFiles(partialsDir);
|
|
399
|
-
await Promise.all(files.map(async (file) => {
|
|
400
|
-
if (extname(file).toLowerCase() === ".mustache") {
|
|
401
|
-
const content = await readFile(file, "utf8");
|
|
402
|
-
const partialName = basename(file, ".mustache");
|
|
403
|
-
partials[partialName] = content;
|
|
404
|
-
}
|
|
405
|
-
}));
|
|
406
|
-
} catch (err) {
|
|
407
|
-
console.error(kolor.yellow(`Warning: Failed to load Mustache partials: ${err.message}`));
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return partials;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function toUrlPath(outRel) {
|
|
414
|
-
const normalized = outRel.replace(/\\/g, "/");
|
|
415
|
-
let path = `/${normalized}`;
|
|
416
|
-
// Remove trailing index.html for directory-style URLs
|
|
417
|
-
if (path.endsWith("index.html")) {
|
|
418
|
-
path = path.slice(0, -"index.html".length);
|
|
419
|
-
}
|
|
420
|
-
// Strip trailing slash except for root
|
|
421
|
-
if (path !== "/" && path.endsWith("/")) {
|
|
422
|
-
path = path.slice(0, -1);
|
|
423
|
-
}
|
|
424
|
-
return path || "/";
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function pageContext(frontmatter, html, config, relPath, data, path = "/") {
|
|
428
|
-
return {
|
|
429
|
-
site: { name: config.siteName, config },
|
|
430
|
-
page: { ...frontmatter, content: html, source: relPath, path },
|
|
431
|
-
collections: data,
|
|
432
|
-
...data
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function shouldRenderMarkdown(frontmatter, config, defaultValue) {
|
|
437
|
-
if (typeof frontmatter?.markdown === "boolean") return frontmatter.markdown;
|
|
438
|
-
return defaultValue;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async function renderWithLayout(layoutName, html, ctx, env, liquidEnv, layoutsDir, partials = {}) {
|
|
442
|
-
if (!layoutName) return html;
|
|
443
|
-
const ext = extname(layoutName).toLowerCase();
|
|
444
|
-
const layoutCtx = {
|
|
445
|
-
...ctx,
|
|
446
|
-
frontmatter: ctx.page || {},
|
|
447
|
-
content: html,
|
|
448
|
-
title: ctx.page?.title ?? ctx.site?.name
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
if (ext === ".njk") {
|
|
452
|
-
return env.render(layoutName, layoutCtx);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (ext === ".liquid" || layoutName.toLowerCase().endsWith(".liquid.html")) {
|
|
456
|
-
return liquidEnv.renderFile(layoutName, layoutCtx);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (ext === ".mustache") {
|
|
460
|
-
const layoutPath = join(layoutsDir, layoutName);
|
|
461
|
-
if (existsSync(layoutPath)) {
|
|
462
|
-
const layoutTemplate = await readFile(layoutPath, "utf8");
|
|
463
|
-
return Mustache.render(layoutTemplate, layoutCtx, partials);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Unknown layout type, return unwrapped content
|
|
468
|
-
return html;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async function renderPage(filePath, { pagesDir, layoutsDir, outDir, env, liquidEnv, config, data, partialsDir }) {
|
|
472
|
-
const rel = relative(pagesDir, filePath);
|
|
473
|
-
const ext = extname(filePath).toLowerCase();
|
|
474
|
-
const outRel = rel.replace(/\.liquid(\.html)?$/i, ".html").replace(ext, ".html");
|
|
475
|
-
const outPath = join(outDir, outRel);
|
|
476
|
-
const path = toUrlPath(outRel);
|
|
477
|
-
await ensureDir(dirname(outPath));
|
|
478
|
-
|
|
479
|
-
// Load Mustache partials if needed
|
|
480
|
-
const partials = (ext === ".mustache" || (await readFile(filePath, "utf8")).match(/layout:.*\.mustache/))
|
|
481
|
-
? await loadMustachePartials(partialsDir)
|
|
482
|
-
: {};
|
|
483
|
-
|
|
484
|
-
if (ext === ".md") {
|
|
485
|
-
const raw = await readFile(filePath, "utf8");
|
|
486
|
-
const parsed = matter(raw);
|
|
487
|
-
const html = md.render(parsed.content);
|
|
488
|
-
const ctx = pageContext(parsed.data, html, config, rel, data, path);
|
|
489
|
-
const rendered = await renderWithLayout(parsed.data.layout, html, ctx, env, liquidEnv, layoutsDir, partials);
|
|
490
|
-
await writeFile(outPath, rendered, "utf8");
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (ext === ".njk") {
|
|
495
|
-
const raw = await readFile(filePath, "utf8");
|
|
496
|
-
const parsed = matter(raw);
|
|
497
|
-
const ctx = pageContext(parsed.data, parsed.content, config, rel, data, path);
|
|
498
|
-
const templateName = rel.replace(/\\/g, "/");
|
|
499
|
-
let pageHtml = env.renderString(parsed.content, ctx, { path: templateName });
|
|
500
|
-
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
501
|
-
pageHtml = md.render(pageHtml);
|
|
502
|
-
}
|
|
503
|
-
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir, partials);
|
|
504
|
-
await writeFile(outPath, rendered, "utf8");
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (ext === ".liquid" || filePath.toLowerCase().endsWith(".liquid.html")) {
|
|
509
|
-
const raw = await readFile(filePath, "utf8");
|
|
510
|
-
const parsed = matter(raw);
|
|
511
|
-
const ctx = pageContext(parsed.data, parsed.content, config, rel, data, path);
|
|
512
|
-
let pageHtml = await liquidEnv.parseAndRender(parsed.content, ctx);
|
|
513
|
-
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
514
|
-
pageHtml = md.render(pageHtml);
|
|
515
|
-
}
|
|
516
|
-
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir, partials);
|
|
517
|
-
await writeFile(outPath, rendered, "utf8");
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (ext === ".mustache") {
|
|
522
|
-
const raw = await readFile(filePath, "utf8");
|
|
523
|
-
const parsed = matter(raw);
|
|
524
|
-
const ctx = pageContext(parsed.data, parsed.content, config, rel, data, path);
|
|
525
|
-
let pageHtml = Mustache.render(parsed.content, ctx, partials);
|
|
526
|
-
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
527
|
-
pageHtml = md.render(pageHtml);
|
|
528
|
-
}
|
|
529
|
-
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir, partials);
|
|
530
|
-
await writeFile(outPath, rendered, "utf8");
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (ext === ".html") {
|
|
535
|
-
const raw = await readFile(filePath, "utf8");
|
|
536
|
-
const parsed = matter(raw);
|
|
537
|
-
const ctx = pageContext(parsed.data, parsed.content, config, rel, data, path);
|
|
538
|
-
let pageHtml = parsed.content;
|
|
539
|
-
if (shouldRenderMarkdown(parsed.data, config, false)) {
|
|
540
|
-
pageHtml = md.render(pageHtml);
|
|
541
|
-
}
|
|
542
|
-
const rendered = await renderWithLayout(parsed.data.layout, pageHtml, ctx, env, liquidEnv, layoutsDir, partials);
|
|
543
|
-
await writeFile(outPath, rendered, "utf8");
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
await cp(filePath, outPath);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
async function cacheBustAssets(outDir) {
|
|
551
|
-
const assetMap = {}; // original path -> hashed path
|
|
552
|
-
const files = await walkFiles(outDir);
|
|
553
|
-
const assetFiles = files.filter((file) => {
|
|
554
|
-
const ext = extname(file).toLowerCase();
|
|
555
|
-
return ext === ".css" || ext === ".js";
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
// Hash and rename each asset
|
|
559
|
-
for (const file of assetFiles) {
|
|
560
|
-
try {
|
|
561
|
-
const content = await readFile(file);
|
|
562
|
-
const hash = createHash("sha256").update(content).digest("hex").slice(0, 10);
|
|
563
|
-
const ext = extname(file);
|
|
564
|
-
const base = basename(file, ext);
|
|
565
|
-
const dir = dirname(file);
|
|
566
|
-
const hashedName = `${base}-${hash}${ext}`;
|
|
567
|
-
const hashedPath = join(dir, hashedName);
|
|
568
|
-
|
|
569
|
-
await rename(file, hashedPath);
|
|
570
|
-
|
|
571
|
-
// Store mapping of original relative path to hashed relative path
|
|
572
|
-
const originalRel = relative(outDir, file).replace(/\\/g, "/");
|
|
573
|
-
const hashedRel = relative(outDir, hashedPath).replace(/\\/g, "/");
|
|
574
|
-
assetMap[originalRel] = hashedRel;
|
|
575
|
-
|
|
576
|
-
// Log the cache-busted file
|
|
577
|
-
console.log(kolor.dim(` ${originalRel}`) + kolor.cyan(` → `) + kolor.green(hashedRel));
|
|
578
|
-
} catch (err) {
|
|
579
|
-
console.error(kolor.red(`Failed to cache-bust ${relative(outDir, file)}: ${err.message}`));
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Update HTML files to reference hashed assets
|
|
584
|
-
const htmlFiles = files.filter((file) => extname(file).toLowerCase() === ".html");
|
|
585
|
-
|
|
586
|
-
for (const htmlFile of htmlFiles) {
|
|
587
|
-
try {
|
|
588
|
-
let html = await readFile(htmlFile, "utf8");
|
|
589
|
-
let updated = false;
|
|
590
|
-
|
|
591
|
-
// Update <link> tags (CSS)
|
|
592
|
-
for (const [original, hashed] of Object.entries(assetMap)) {
|
|
593
|
-
if (original.endsWith(".css")) {
|
|
594
|
-
// Match href="/path" or href="path" but ensure we stay within the tag
|
|
595
|
-
const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
596
|
-
const pattern = new RegExp(`(<link[^>]*?\\shref=["'])/?${escaped}(["'])`, "gi");
|
|
597
|
-
|
|
598
|
-
const newHtml = html.replace(pattern, `$1/${hashed}$2`);
|
|
599
|
-
if (newHtml !== html) {
|
|
600
|
-
html = newHtml;
|
|
601
|
-
updated = true;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// Update <script> tags (JS)
|
|
607
|
-
for (const [original, hashed] of Object.entries(assetMap)) {
|
|
608
|
-
if (original.endsWith(".js")) {
|
|
609
|
-
// Match src="/path" or src="path" but ensure we stay within the tag
|
|
610
|
-
const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
611
|
-
const pattern = new RegExp(`(<script[^>]*?\\ssrc=["'])/?${escaped}(["'])`, "gi");
|
|
612
|
-
|
|
613
|
-
const newHtml = html.replace(pattern, `$1/${hashed}$2`);
|
|
614
|
-
if (newHtml !== html) {
|
|
615
|
-
html = newHtml;
|
|
616
|
-
updated = true;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (updated) {
|
|
622
|
-
await writeFile(htmlFile, html, "utf8");
|
|
623
|
-
}
|
|
624
|
-
} catch (err) {
|
|
625
|
-
console.error(kolor.red(`Failed to update asset references in ${relative(outDir, htmlFile)}: ${err.message}`));
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
return assetMap;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
async function build(cwdArg = cwd, options = {}) {
|
|
633
|
-
const config = await loadConfig(cwdArg);
|
|
634
|
-
const srcDir = resolve(cwdArg, config.srcDir || "src");
|
|
635
|
-
const pagesDir = join(srcDir, "pages");
|
|
636
|
-
const layoutsDir = join(srcDir, "layouts");
|
|
637
|
-
const partialsDir = join(srcDir, "partials");
|
|
638
|
-
const dataDir = join(srcDir, "data");
|
|
639
|
-
const collectionsDir = join(srcDir, "collections");
|
|
640
|
-
const publicDir = resolve(cwdArg, "public");
|
|
641
|
-
const outDir = resolve(cwdArg, config.outDir || "dist");
|
|
642
|
-
const env = createNunjucksEnv(layoutsDir, pagesDir, srcDir, partialsDir);
|
|
643
|
-
// Allow user config to extend the Nunjucks environment (e.g., custom filters)
|
|
644
|
-
if (config?.hooks?.nunjucksEnv && typeof config.hooks.nunjucksEnv === "function") {
|
|
645
|
-
try {
|
|
646
|
-
config.hooks.nunjucksEnv(env);
|
|
647
|
-
} catch (err) {
|
|
648
|
-
console.error(kolor.red(`Failed to apply nunjucksEnv hook: ${err.message}`));
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
const liquidEnv = createLiquidEnv(layoutsDir, pagesDir, srcDir, partialsDir);
|
|
652
|
-
// Allow user config to extend the Liquid environment (e.g., custom filters)
|
|
653
|
-
if (config?.hooks?.liquidEnv && typeof config.hooks.liquidEnv === "function") {
|
|
654
|
-
try {
|
|
655
|
-
config.hooks.liquidEnv(liquidEnv);
|
|
656
|
-
} catch (err) {
|
|
657
|
-
console.error(kolor.red(`Failed to apply liquidEnv hook: ${err.message}`));
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
const data = await loadData([dataDir, collectionsDir]);
|
|
661
|
-
|
|
662
|
-
await cleanDir(outDir);
|
|
663
|
-
await copyPublic(publicDir, outDir, config.excludeFiles);
|
|
664
|
-
|
|
665
|
-
// Only compress photos during production builds, not during dev mode
|
|
666
|
-
const shouldCompressPhotos = options.skipImageCompression !== true && config.compressPhotos;
|
|
667
|
-
if (shouldCompressPhotos) {
|
|
668
|
-
await processImages(outDir, config);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const files = await walkFiles(pagesDir);
|
|
672
|
-
if (files.length === 0) {
|
|
673
|
-
console.log(kolor.yellow("No pages found in src/pages."));
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
await Promise.all(files.map((file) => renderPage(file, { pagesDir, layoutsDir, outDir, env, liquidEnv, config, data, partialsDir })));
|
|
678
|
-
|
|
679
|
-
// Skip minification and cache busting in dev mode for faster rebuilds
|
|
680
|
-
const isDevMode = options.devMode === true;
|
|
681
|
-
|
|
682
|
-
if (!isDevMode && config.minifyCSS) {
|
|
683
|
-
await minifyCSSFiles(outDir);
|
|
684
|
-
console.log(kolor.green("CSS minified"));
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (!isDevMode && config.minifyHTML) {
|
|
688
|
-
await minifyHTMLFiles(outDir, config);
|
|
689
|
-
console.log(kolor.green("HTML minified"));
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (!isDevMode && config.cacheBustAssets) {
|
|
693
|
-
console.log(kolor.cyan("Cache-busting assets..."));
|
|
694
|
-
const assetMap = await cacheBustAssets(outDir);
|
|
695
|
-
const assetCount = Object.keys(assetMap).length;
|
|
696
|
-
if (assetCount > 0) {
|
|
697
|
-
console.log(kolor.green(`✓ Cache-busted ${assetCount} asset(s)`));
|
|
698
|
-
} else {
|
|
699
|
-
console.log(kolor.yellow("No assets found to cache-bust"));
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// Generate robots.txt dynamically if it doesn't exist in public directory
|
|
704
|
-
const publicRobotsTxt = join(publicDir, "robots.txt");
|
|
705
|
-
const distRobotsTxt = join(outDir, "robots.txt");
|
|
706
|
-
if (!existsSync(publicRobotsTxt) && !existsSync(distRobotsTxt)) {
|
|
707
|
-
const robotsTxt = `User-agent: *\nAllow: /\n\nSitemap: ${config.siteUrl}/sitemap.xml\n`;
|
|
708
|
-
await writeFile(distRobotsTxt, robotsTxt, "utf8");
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
console.log(kolor.green(`Built ${files.length} page(s) → ${relative(cwdArg, outDir)}`));
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function serve(outDir, port = 4173) {
|
|
715
|
-
const mime = {
|
|
716
|
-
".html": "text/html",
|
|
717
|
-
".css": "text/css",
|
|
718
|
-
".js": "application/javascript",
|
|
719
|
-
".json": "application/json",
|
|
720
|
-
".svg": "image/svg+xml",
|
|
721
|
-
".png": "image/png",
|
|
722
|
-
".jpg": "image/jpeg",
|
|
723
|
-
".jpeg": "image/jpeg",
|
|
724
|
-
".gif": "image/gif",
|
|
725
|
-
".webp": "image/webp",
|
|
726
|
-
".ico": "image/x-icon"
|
|
727
|
-
};
|
|
728
|
-
|
|
729
|
-
const server = createServer(async (req, res) => {
|
|
730
|
-
const urlPath = decodeURI((req.url || "/").split("?")[0]);
|
|
731
|
-
const safePath = urlPath.replace(/\.\.+/g, "");
|
|
732
|
-
const requestPath = safePath.replace(/^\/+/, "") || "index.html";
|
|
733
|
-
let filePath = join(outDir, requestPath);
|
|
734
|
-
const notFoundPath = join(outDir, "404.html");
|
|
735
|
-
const indexPath = join(outDir, "index.html");
|
|
736
|
-
let isNotFoundResponse = false;
|
|
737
|
-
let stats;
|
|
738
|
-
|
|
739
|
-
try {
|
|
740
|
-
stats = await stat(filePath);
|
|
741
|
-
if (stats.isDirectory()) {
|
|
742
|
-
filePath = join(filePath, "index.html");
|
|
743
|
-
stats = await stat(filePath);
|
|
744
|
-
}
|
|
745
|
-
} catch {
|
|
746
|
-
if (existsSync(notFoundPath)) {
|
|
747
|
-
filePath = notFoundPath;
|
|
748
|
-
isNotFoundResponse = true;
|
|
749
|
-
} else {
|
|
750
|
-
filePath = indexPath;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
try {
|
|
755
|
-
const data = await readFile(filePath);
|
|
756
|
-
const type = mime[extname(filePath).toLowerCase()] || "text/plain";
|
|
757
|
-
res.writeHead(isNotFoundResponse ? 404 : 200, { "Content-Type": type });
|
|
758
|
-
res.end(data);
|
|
759
|
-
} catch {
|
|
760
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
761
|
-
res.end("Not found");
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
server.listen(port, () => {
|
|
766
|
-
console.log(kolor.green(`Serving dist at http://localhost:${port}`));
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
return server;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
async function dev(cwdArg = cwd) {
|
|
773
|
-
let building = false;
|
|
774
|
-
let pending = false;
|
|
775
|
-
|
|
776
|
-
const runBuild = async () => {
|
|
777
|
-
if (building) {
|
|
778
|
-
pending = true;
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
building = true;
|
|
782
|
-
try {
|
|
783
|
-
// Skip image compression, minification, and cache busting during dev mode for faster rebuilds
|
|
784
|
-
await build(cwdArg, { skipImageCompression: true, devMode: true });
|
|
785
|
-
} catch (err) {
|
|
786
|
-
console.error(kolor.red(`Build failed: ${err.message}`));
|
|
787
|
-
} finally {
|
|
788
|
-
building = false;
|
|
789
|
-
if (pending) {
|
|
790
|
-
pending = false;
|
|
791
|
-
runBuild();
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
};
|
|
795
|
-
|
|
796
|
-
await runBuild();
|
|
797
|
-
|
|
798
|
-
const config = await loadConfig(cwdArg);
|
|
799
|
-
const srcDir = resolve(cwdArg, config.srcDir || "src");
|
|
800
|
-
const dataDir = join(srcDir, "data");
|
|
801
|
-
const collectionsDir = join(srcDir, "collections");
|
|
802
|
-
const publicDir = resolve(cwdArg, "public");
|
|
803
|
-
const outDir = resolve(cwdArg, config.outDir || "dist");
|
|
804
|
-
const watcher = chokidar.watch([srcDir, publicDir, dataDir, collectionsDir], { ignoreInitial: true });
|
|
805
|
-
|
|
806
|
-
watcher.on("all", (event, path) => {
|
|
807
|
-
console.log(kolor.cyan(`↻ ${event}: ${relative(cwdArg, path)}`));
|
|
808
|
-
runBuild();
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
serve(outDir, config.port || 4173);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function slugify(text) {
|
|
815
|
-
return text
|
|
816
|
-
.toLowerCase()
|
|
817
|
-
.trim()
|
|
818
|
-
.replace(/[^\w\s-]/g, "")
|
|
819
|
-
.replace(/[\s_-]+/g, "-")
|
|
820
|
-
.replace(/^-+|-+$/g, "");
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function formatDate(date) {
|
|
824
|
-
return date.toISOString().split("T")[0];
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
async function makeContent(type) {
|
|
828
|
-
// Get all arguments after the command and join them
|
|
829
|
-
const args = argv.slice(3);
|
|
830
|
-
|
|
831
|
-
if (args.length === 0) {
|
|
832
|
-
console.log(kolor.red("❌ Missing name argument"));
|
|
833
|
-
console.log(kolor.dim(`Usage: camper make:${type} <name> [name2, name3, ...]`));
|
|
834
|
-
console.log(kolor.dim("\nExamples:"));
|
|
835
|
-
console.log(kolor.dim(" camper make:page about"));
|
|
836
|
-
console.log(kolor.dim(" camper make:page home, about, contact"));
|
|
837
|
-
console.log(kolor.dim(" camper make:collection products, categories\n"));
|
|
838
|
-
exit(1);
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// Join all args and split by comma to support both formats:
|
|
842
|
-
// camper make:page home about contact
|
|
843
|
-
// camper make:page home, about, contact
|
|
844
|
-
const namesString = args.join(" ");
|
|
845
|
-
const names = namesString.split(",").map(n => n.trim()).filter(n => n.length > 0);
|
|
846
|
-
|
|
847
|
-
if (names.length === 0) {
|
|
848
|
-
console.log(kolor.red("❌ No valid names provided\n"));
|
|
849
|
-
exit(1);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
console.log(kolor.cyan(`\n🏕️ Creating ${names.length} ${type}(s)...\n`));
|
|
853
|
-
|
|
854
|
-
const config = await loadConfig(cwd);
|
|
855
|
-
const srcDir = resolve(cwd, config.srcDir || "src");
|
|
856
|
-
|
|
857
|
-
// Determine file extension based on template engine
|
|
858
|
-
const engineExtMap = {
|
|
859
|
-
nunjucks: ".njk",
|
|
860
|
-
liquid: ".liquid",
|
|
861
|
-
mustache: ".mustache"
|
|
862
|
-
};
|
|
863
|
-
const defaultExt = engineExtMap[config.templateEngine] || ".njk";
|
|
864
|
-
|
|
865
|
-
let successCount = 0;
|
|
866
|
-
let skipCount = 0;
|
|
867
|
-
|
|
868
|
-
for (const name of names) {
|
|
869
|
-
const result = await createSingleContent(type, name, srcDir, config, defaultExt);
|
|
870
|
-
if (result.success) successCount++;
|
|
871
|
-
if (result.skipped) skipCount++;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
console.log();
|
|
875
|
-
if (successCount > 0) {
|
|
876
|
-
console.log(kolor.green(`✅ Created ${successCount} ${type}(s)`));
|
|
877
|
-
}
|
|
878
|
-
if (skipCount > 0) {
|
|
879
|
-
console.log(kolor.yellow(`⚠️ Skipped ${skipCount} existing file(s)`));
|
|
880
|
-
}
|
|
881
|
-
console.log(kolor.dim("\n🌲 Happy camping!\n"));
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
async function createSingleContent(type, name, srcDir, config, defaultExt) {
|
|
885
|
-
// Check if user provided an extension
|
|
886
|
-
const hasExtension = name.includes(".");
|
|
887
|
-
const providedExt = hasExtension ? extname(name) : null;
|
|
888
|
-
const nameWithoutExt = hasExtension ? basename(name, providedExt) : name;
|
|
889
|
-
|
|
890
|
-
const slug = slugify(nameWithoutExt);
|
|
891
|
-
const today = formatDate(new Date());
|
|
892
|
-
const title = nameWithoutExt.charAt(0).toUpperCase() + nameWithoutExt.slice(1);
|
|
893
|
-
|
|
894
|
-
let targetPath;
|
|
895
|
-
let content;
|
|
896
|
-
let fileExt;
|
|
897
|
-
|
|
898
|
-
switch (type.toLowerCase()) {
|
|
899
|
-
case "page": {
|
|
900
|
-
// Priority: provided extension > template engine
|
|
901
|
-
if (providedExt) {
|
|
902
|
-
fileExt = providedExt;
|
|
903
|
-
} else {
|
|
904
|
-
fileExt = defaultExt;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
targetPath = join(srcDir, "pages", `${slug}${fileExt}`);
|
|
908
|
-
|
|
909
|
-
// Determine if we should use markdown content based on extension
|
|
910
|
-
const useMarkdown = fileExt === ".md";
|
|
911
|
-
|
|
912
|
-
if (useMarkdown) {
|
|
913
|
-
content = `---
|
|
914
|
-
layout: base${defaultExt}
|
|
915
|
-
title: ${title}
|
|
916
|
-
---
|
|
917
|
-
|
|
918
|
-
# ${title}
|
|
919
|
-
|
|
920
|
-
Your new page content goes here.
|
|
921
|
-
`;
|
|
922
|
-
} else {
|
|
923
|
-
content = `---
|
|
924
|
-
layout: base${defaultExt}
|
|
925
|
-
title: ${title}
|
|
926
|
-
---
|
|
927
|
-
|
|
928
|
-
<h1>${title}</h1>
|
|
929
|
-
<p>Your new page content goes here.</p>
|
|
930
|
-
`;
|
|
931
|
-
}
|
|
932
|
-
break;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
case "post": {
|
|
936
|
-
const postsDir = join(srcDir, "pages", "blog");
|
|
937
|
-
await ensureDir(postsDir);
|
|
938
|
-
|
|
939
|
-
if (providedExt) {
|
|
940
|
-
fileExt = providedExt;
|
|
941
|
-
} else {
|
|
942
|
-
fileExt = defaultExt;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
targetPath = join(postsDir, `${slug}${fileExt}`);
|
|
946
|
-
|
|
947
|
-
const useMarkdown = fileExt === ".md";
|
|
948
|
-
|
|
949
|
-
if (useMarkdown) {
|
|
950
|
-
content = `---
|
|
951
|
-
layout: base${defaultExt}
|
|
952
|
-
title: ${title}
|
|
953
|
-
date: ${today}
|
|
954
|
-
author: Your Name
|
|
955
|
-
---
|
|
956
|
-
|
|
957
|
-
# ${title}
|
|
958
|
-
|
|
959
|
-
Your blog post content goes here.
|
|
960
|
-
`;
|
|
961
|
-
} else {
|
|
962
|
-
content = `---
|
|
963
|
-
layout: base${defaultExt}
|
|
964
|
-
title: ${title}
|
|
965
|
-
date: ${today}
|
|
966
|
-
author: Your Name
|
|
967
|
-
---
|
|
968
|
-
|
|
969
|
-
<h1>${title}</h1>
|
|
970
|
-
<p>Your blog post content goes here.</p>
|
|
971
|
-
`;
|
|
972
|
-
}
|
|
973
|
-
break;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
case "layout": {
|
|
977
|
-
const layoutsDir = join(srcDir, "layouts");
|
|
978
|
-
await ensureDir(layoutsDir);
|
|
979
|
-
targetPath = join(layoutsDir, `${slug}.njk`);
|
|
980
|
-
fileExt = ".njk";
|
|
981
|
-
content = `<!DOCTYPE html>
|
|
982
|
-
<html lang="en">
|
|
983
|
-
<head>
|
|
984
|
-
<meta charset="UTF-8">
|
|
985
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
986
|
-
<title>{{ title or site.name }}</title>
|
|
987
|
-
<link rel="stylesheet" href="/style.css">
|
|
988
|
-
</head>
|
|
989
|
-
<body>
|
|
990
|
-
<main>
|
|
991
|
-
{% block content %}
|
|
992
|
-
{{ content | safe }}
|
|
993
|
-
{% endblock %}
|
|
994
|
-
</main>
|
|
995
|
-
</body>
|
|
996
|
-
</html>
|
|
997
|
-
`;
|
|
998
|
-
break;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
case "component": {
|
|
1002
|
-
const componentsDir = join(srcDir, "components");
|
|
1003
|
-
await ensureDir(componentsDir);
|
|
1004
|
-
targetPath = join(componentsDir, `${slug}.njk`);
|
|
1005
|
-
fileExt = ".njk";
|
|
1006
|
-
content = `{# ${title} Component #}
|
|
1007
|
-
<div class="${slug}">
|
|
1008
|
-
{{ content | safe }}
|
|
1009
|
-
</div>
|
|
1010
|
-
`;
|
|
1011
|
-
break;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
case "partial": {
|
|
1015
|
-
const partialsDir = join(srcDir, "partials");
|
|
1016
|
-
await ensureDir(partialsDir);
|
|
1017
|
-
targetPath = join(partialsDir, `${slug}.njk`);
|
|
1018
|
-
fileExt = ".njk";
|
|
1019
|
-
content = `{# ${title} Partial #}
|
|
1020
|
-
<div class="${slug}">
|
|
1021
|
-
{# Your partial content here #}
|
|
1022
|
-
</div>
|
|
1023
|
-
`;
|
|
1024
|
-
break;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
case "collection": {
|
|
1028
|
-
const collectionsDir = join(srcDir, "collections");
|
|
1029
|
-
await ensureDir(collectionsDir);
|
|
1030
|
-
targetPath = join(collectionsDir, `${slug}.json`);
|
|
1031
|
-
fileExt = ".json";
|
|
1032
|
-
content = `[
|
|
1033
|
-
{
|
|
1034
|
-
"id": 1,
|
|
1035
|
-
"title": "Sample ${title} Item",
|
|
1036
|
-
"description": "Add your collection items here"
|
|
1037
|
-
}
|
|
1038
|
-
]
|
|
1039
|
-
`;
|
|
1040
|
-
break;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
default:
|
|
1044
|
-
console.log(kolor.red(`❌ Unknown content type: ${type}`));
|
|
1045
|
-
console.log(kolor.dim("\nSupported types: page, post, layout, component, partial, collection\n"));
|
|
1046
|
-
return { success: false, skipped: false };
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
if (existsSync(targetPath)) {
|
|
1050
|
-
console.log(kolor.dim(` ⚠️ Skipped ${relative(cwd, targetPath)} (already exists)`));
|
|
1051
|
-
return { success: false, skipped: true };
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
await ensureDir(dirname(targetPath));
|
|
1055
|
-
await writeFile(targetPath, content, "utf8");
|
|
1056
|
-
|
|
1057
|
-
console.log(kolor.dim(` ✅ ${relative(cwd, targetPath)}`));
|
|
1058
|
-
return { success: true, skipped: false };
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
async function init() {
|
|
1062
|
-
const targetDir = cwd;
|
|
1063
|
-
console.log(kolor.cyan(kolor.bold("🏕️ Initializing Campsite in current directory...")));
|
|
1064
|
-
|
|
1065
|
-
// Check if already initialized
|
|
1066
|
-
if (existsSync(join(targetDir, "campsite.config.js"))) {
|
|
1067
|
-
console.log(kolor.yellow("⚠️ This directory already has a campsite.config.js file."));
|
|
1068
|
-
console.log(kolor.dim("Run 'camper dev' to start developing.\n"));
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// Create basic structure
|
|
1073
|
-
const dirs = [
|
|
1074
|
-
join(targetDir, "src", "pages"),
|
|
1075
|
-
join(targetDir, "src", "layouts"),
|
|
1076
|
-
join(targetDir, "public")
|
|
1077
|
-
];
|
|
1078
|
-
|
|
1079
|
-
for (const dir of dirs) {
|
|
1080
|
-
await ensureDir(dir);
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// Create basic config file
|
|
1084
|
-
const configContent = `export default {
|
|
1085
|
-
siteName: "My Campsite",
|
|
1086
|
-
srcDir: "src",
|
|
1087
|
-
outDir: "dist",
|
|
1088
|
-
templateEngine: "nunjucks",
|
|
1089
|
-
markdown: true,
|
|
1090
|
-
integrations: {
|
|
1091
|
-
nunjucks: true,
|
|
1092
|
-
liquid: false,
|
|
1093
|
-
mustache: false,
|
|
1094
|
-
vue: false,
|
|
1095
|
-
alpine: false
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
|
-
`;
|
|
1099
|
-
await writeFile(join(targetDir, "campsite.config.js"), configContent, "utf8");
|
|
1100
|
-
|
|
1101
|
-
// Create basic layout
|
|
1102
|
-
const layoutContent = `<!DOCTYPE html>
|
|
1103
|
-
<html lang="en">
|
|
1104
|
-
<head>
|
|
1105
|
-
<meta charset="UTF-8">
|
|
1106
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1107
|
-
<title>{{ title or site.name }}</title>
|
|
1108
|
-
<link rel="stylesheet" href="/style.css">
|
|
1109
|
-
</head>
|
|
1110
|
-
<body>
|
|
1111
|
-
{% block content %}
|
|
1112
|
-
{{ content | safe }}
|
|
1113
|
-
{% endblock %}
|
|
1114
|
-
</body>
|
|
1115
|
-
</html>
|
|
1116
|
-
`;
|
|
1117
|
-
await writeFile(join(targetDir, "src", "layouts", "base.njk"), layoutContent, "utf8");
|
|
1118
|
-
|
|
1119
|
-
// Create sample page
|
|
1120
|
-
const pageContent = `---
|
|
1121
|
-
layout: base.njk
|
|
1122
|
-
title: Welcome to Campsite
|
|
1123
|
-
---
|
|
1124
|
-
|
|
1125
|
-
# Welcome to Campsite! 🏕️
|
|
1126
|
-
|
|
1127
|
-
Your cozy static site is ready to build.
|
|
1128
|
-
|
|
1129
|
-
## Get Started
|
|
1130
|
-
|
|
1131
|
-
- Run \`camper dev\` to start developing
|
|
1132
|
-
- Edit pages in \`src/pages/\`
|
|
1133
|
-
- Customize layouts in \`src/layouts/\`
|
|
1134
|
-
|
|
1135
|
-
Happy camping! 🌲🦊
|
|
1136
|
-
`;
|
|
1137
|
-
await writeFile(join(targetDir, "src", "pages", "index.md"), pageContent, "utf8");
|
|
1138
|
-
|
|
1139
|
-
// Create basic CSS
|
|
1140
|
-
const cssContent = `* {
|
|
1141
|
-
margin: 0;
|
|
1142
|
-
padding: 0;
|
|
1143
|
-
box-sizing: border-box;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
body {
|
|
1147
|
-
font-family: system-ui, -apple-system, sans-serif;
|
|
1148
|
-
line-height: 1.6;
|
|
1149
|
-
padding: 2rem;
|
|
1150
|
-
max-width: 800px;
|
|
1151
|
-
margin: 0 auto;
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
h1 { color: #2d5016; margin-bottom: 1rem; }
|
|
1155
|
-
h2 { color: #4a7c2c; margin-top: 1.5rem; }
|
|
1156
|
-
`;
|
|
1157
|
-
await writeFile(join(targetDir, "public", "style.css"), cssContent, "utf8");
|
|
1158
|
-
|
|
1159
|
-
// Create .gitignore
|
|
1160
|
-
const gitignoreContent = `node_modules/
|
|
1161
|
-
dist/
|
|
1162
|
-
.DS_Store
|
|
1163
|
-
`;
|
|
1164
|
-
await writeFile(join(targetDir, ".gitignore"), gitignoreContent, "utf8");
|
|
1165
|
-
|
|
1166
|
-
// Create package.json
|
|
1167
|
-
const packageJson = {
|
|
1168
|
-
name: basename(targetDir),
|
|
1169
|
-
version: "0.0.1",
|
|
1170
|
-
type: "module",
|
|
1171
|
-
scripts: {
|
|
1172
|
-
dev: "camper dev",
|
|
1173
|
-
build: "camper build",
|
|
1174
|
-
serve: "camper serve",
|
|
1175
|
-
preview: "camper preview"
|
|
1176
|
-
},
|
|
1177
|
-
dependencies: {
|
|
1178
|
-
basecampjs: "^0.0.8"
|
|
1179
|
-
}
|
|
1180
|
-
};
|
|
1181
|
-
await writeFile(join(targetDir, "package.json"), JSON.stringify(packageJson, null, 2), "utf8");
|
|
1182
|
-
|
|
1183
|
-
console.log(kolor.green("✅ Campsite initialized successfully!\n"));
|
|
1184
|
-
console.log(kolor.bold("Next steps:"));
|
|
1185
|
-
console.log(kolor.dim(" 1. Install dependencies: npm install"));
|
|
1186
|
-
console.log(kolor.dim(" 2. Start developing: camper dev\n"));
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
async function clean() {
|
|
1190
|
-
const config = await loadConfig(cwd);
|
|
1191
|
-
const outDir = resolve(cwd, config.outDir || "dist");
|
|
1192
|
-
|
|
1193
|
-
if (!existsSync(outDir)) {
|
|
1194
|
-
console.log(kolor.dim(`Nothing to clean. ${outDir} does not exist.`));
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
console.log(kolor.cyan(`🧹 Cleaning ${relative(cwd, outDir)}...`));
|
|
1199
|
-
await rm(outDir, { recursive: true, force: true });
|
|
1200
|
-
console.log(kolor.green(`✅ Cleaned ${relative(cwd, outDir)}\n`));
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
async function check() {
|
|
1204
|
-
console.log(kolor.cyan(kolor.bold("🔍 Checking Campsite project...\n")));
|
|
1205
|
-
let hasIssues = false;
|
|
1206
|
-
|
|
1207
|
-
// Check if campsite.config.js exists
|
|
1208
|
-
const configPath = join(cwd, "campsite.config.js");
|
|
1209
|
-
if (!existsSync(configPath)) {
|
|
1210
|
-
console.log(kolor.red("❌ campsite.config.js not found"));
|
|
1211
|
-
console.log(kolor.dim(" Run 'camper init' to initialize a project\n"));
|
|
1212
|
-
hasIssues = true;
|
|
1213
|
-
} else {
|
|
1214
|
-
console.log(kolor.green("✅ campsite.config.js found"));
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
// Load and validate config
|
|
1218
|
-
const config = await loadConfig(cwd);
|
|
1219
|
-
const srcDir = resolve(cwd, config.srcDir || "src");
|
|
1220
|
-
const pagesDir = join(srcDir, "pages");
|
|
1221
|
-
const layoutsDir = join(srcDir, "layouts");
|
|
1222
|
-
const publicDir = resolve(cwd, "public");
|
|
1223
|
-
|
|
1224
|
-
// Check src directory
|
|
1225
|
-
if (!existsSync(srcDir)) {
|
|
1226
|
-
console.log(kolor.red(`❌ Source directory not found: ${relative(cwd, srcDir)}`));
|
|
1227
|
-
hasIssues = true;
|
|
1228
|
-
} else {
|
|
1229
|
-
console.log(kolor.green(`✅ Source directory exists: ${relative(cwd, srcDir)}`));
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// Check pages directory
|
|
1233
|
-
if (!existsSync(pagesDir)) {
|
|
1234
|
-
console.log(kolor.yellow(`⚠️ Pages directory not found: ${relative(cwd, pagesDir)}`));
|
|
1235
|
-
hasIssues = true;
|
|
1236
|
-
} else {
|
|
1237
|
-
const files = await walkFiles(pagesDir);
|
|
1238
|
-
if (files.length === 0) {
|
|
1239
|
-
console.log(kolor.yellow(`⚠️ No pages found in ${relative(cwd, pagesDir)}`));
|
|
1240
|
-
hasIssues = true;
|
|
1241
|
-
} else {
|
|
1242
|
-
console.log(kolor.green(`✅ Found ${files.length} page(s) in ${relative(cwd, pagesDir)}`));
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// Check layouts directory
|
|
1247
|
-
if (existsSync(layoutsDir)) {
|
|
1248
|
-
const layouts = await readdir(layoutsDir).catch(() => []);
|
|
1249
|
-
console.log(kolor.green(`✅ Found ${layouts.length} layout(s) in ${relative(cwd, layoutsDir)}`));
|
|
1250
|
-
} else {
|
|
1251
|
-
console.log(kolor.dim(`ℹ️ No layouts directory (${relative(cwd, layoutsDir)})`));
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// Check public directory
|
|
1255
|
-
if (existsSync(publicDir)) {
|
|
1256
|
-
console.log(kolor.green(`✅ Public directory exists: ${relative(cwd, publicDir)}`));
|
|
1257
|
-
} else {
|
|
1258
|
-
console.log(kolor.dim(`ℹ️ No public directory (${relative(cwd, publicDir)})`));
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// Check for package.json and dependencies
|
|
1262
|
-
const pkgPath = join(cwd, "package.json");
|
|
1263
|
-
if (existsSync(pkgPath)) {
|
|
1264
|
-
try {
|
|
1265
|
-
const pkgRaw = await readFile(pkgPath, "utf8");
|
|
1266
|
-
const pkg = JSON.parse(pkgRaw);
|
|
1267
|
-
if (pkg.dependencies?.basecampjs || pkg.devDependencies?.basecampjs) {
|
|
1268
|
-
console.log(kolor.green("✅ basecampjs dependency found"));
|
|
1269
|
-
} else {
|
|
1270
|
-
console.log(kolor.yellow("⚠️ basecampjs not listed in dependencies"));
|
|
1271
|
-
console.log(kolor.dim(" Consider adding: npm install basecampjs"));
|
|
1272
|
-
}
|
|
1273
|
-
} catch {
|
|
1274
|
-
console.log(kolor.yellow("⚠️ Could not parse package.json"));
|
|
1275
|
-
}
|
|
1276
|
-
} else {
|
|
1277
|
-
console.log(kolor.dim("ℹ️ No package.json found"));
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
console.log();
|
|
1281
|
-
if (hasIssues) {
|
|
1282
|
-
console.log(kolor.yellow("⚠️ Some issues found. Review the messages above."));
|
|
1283
|
-
} else {
|
|
1284
|
-
console.log(kolor.green(kolor.bold("🎉 Everything looks good! Ready to build.")));
|
|
1285
|
-
}
|
|
1286
|
-
console.log();
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
async function upgrade() {
|
|
1290
|
-
console.log(kolor.cyan(kolor.bold("⬆️ Checking for CampsiteJS updates...\n")));
|
|
1291
|
-
|
|
1292
|
-
// Check if package.json exists
|
|
1293
|
-
const pkgPath = join(cwd, "package.json");
|
|
1294
|
-
if (!existsSync(pkgPath)) {
|
|
1295
|
-
console.log(kolor.red("❌ package.json not found"));
|
|
1296
|
-
console.log(kolor.dim("This command should be run in a Campsite project directory.\n"));
|
|
1297
|
-
exit(1);
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
// Read current package.json
|
|
1301
|
-
let pkg;
|
|
1302
|
-
try {
|
|
1303
|
-
const pkgRaw = await readFile(pkgPath, "utf8");
|
|
1304
|
-
pkg = JSON.parse(pkgRaw);
|
|
1305
|
-
} catch {
|
|
1306
|
-
console.log(kolor.red("❌ Could not read package.json\n"));
|
|
1307
|
-
exit(1);
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
const currentVersion = pkg.dependencies?.basecampjs || pkg.devDependencies?.basecampjs;
|
|
1311
|
-
if (!currentVersion) {
|
|
1312
|
-
console.log(kolor.yellow("⚠️ basecampjs not found in dependencies"));
|
|
1313
|
-
console.log(kolor.dim("Install it with: npm install basecampjs\n"));
|
|
1314
|
-
exit(1);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
console.log(kolor.dim(`Current version: ${currentVersion}`));
|
|
1318
|
-
console.log(kolor.cyan("\nUpgrading basecampjs to latest version...\n"));
|
|
1319
|
-
|
|
1320
|
-
// Use dynamic import to run npm commands
|
|
1321
|
-
const { spawn } = await import("child_process");
|
|
1322
|
-
|
|
1323
|
-
return new Promise((resolve, reject) => {
|
|
1324
|
-
const child = spawn("npm", ["install", "basecampjs@latest"], {
|
|
1325
|
-
cwd,
|
|
1326
|
-
stdio: "inherit",
|
|
1327
|
-
shell: process.platform === "win32"
|
|
1328
|
-
});
|
|
1329
|
-
|
|
1330
|
-
child.on("close", async (code) => {
|
|
1331
|
-
if (code === 0) {
|
|
1332
|
-
console.log();
|
|
1333
|
-
console.log(kolor.green("✅ CampsiteJS updated successfully!"));
|
|
1334
|
-
|
|
1335
|
-
// Read updated version
|
|
1336
|
-
try {
|
|
1337
|
-
const updatedPkgRaw = await readFile(pkgPath, "utf8");
|
|
1338
|
-
const updatedPkg = JSON.parse(updatedPkgRaw);
|
|
1339
|
-
const newVersion = updatedPkg.dependencies?.basecampjs || updatedPkg.devDependencies?.basecampjs;
|
|
1340
|
-
console.log(kolor.dim(`New version: ${newVersion}`));
|
|
1341
|
-
} catch {}
|
|
1342
|
-
|
|
1343
|
-
console.log();
|
|
1344
|
-
console.log(kolor.dim("🌲 Tip: Run 'camper dev' to start developing with the latest version\n"));
|
|
1345
|
-
resolve();
|
|
1346
|
-
} else {
|
|
1347
|
-
console.log();
|
|
1348
|
-
console.log(kolor.red(`❌ Update failed with code ${code}\n`));
|
|
1349
|
-
reject(new Error(`npm install failed with code ${code}`));
|
|
1350
|
-
}
|
|
1351
|
-
});
|
|
1352
|
-
|
|
1353
|
-
child.on("error", (err) => {
|
|
1354
|
-
console.log(kolor.red(`❌ Update failed: ${err.message}\n`));
|
|
1355
|
-
reject(err);
|
|
1356
|
-
});
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
async function list() {
|
|
1361
|
-
console.log(kolor.cyan(kolor.bold("🗺️ Listing Campsite content...\n")));
|
|
1362
|
-
|
|
1363
|
-
const config = await loadConfig(cwd);
|
|
1364
|
-
const srcDir = resolve(cwd, config.srcDir || "src");
|
|
1365
|
-
const pagesDir = join(srcDir, "pages");
|
|
1366
|
-
const layoutsDir = join(srcDir, "layouts");
|
|
1367
|
-
const componentsDir = join(srcDir, "components");
|
|
1368
|
-
const partialsDir = join(srcDir, "partials");
|
|
1369
|
-
const collectionsDir = join(srcDir, "collections");
|
|
1370
|
-
const dataDir = join(srcDir, "data");
|
|
1371
|
-
|
|
1372
|
-
// List pages
|
|
1373
|
-
if (existsSync(pagesDir)) {
|
|
1374
|
-
const pages = await walkFiles(pagesDir);
|
|
1375
|
-
if (pages.length > 0) {
|
|
1376
|
-
console.log(kolor.bold("📄 Pages (") + kolor.cyan(pages.length.toString()) + kolor.bold(")"));
|
|
1377
|
-
pages.forEach(page => {
|
|
1378
|
-
const rel = relative(pagesDir, page);
|
|
1379
|
-
console.log(" " + kolor.dim("• ") + rel);
|
|
1380
|
-
});
|
|
1381
|
-
console.log();
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
// List layouts
|
|
1386
|
-
if (existsSync(layoutsDir)) {
|
|
1387
|
-
const layouts = await readdir(layoutsDir).catch(() => []);
|
|
1388
|
-
if (layouts.length > 0) {
|
|
1389
|
-
console.log(kolor.bold("📝 Layouts (") + kolor.cyan(layouts.length.toString()) + kolor.bold(")"));
|
|
1390
|
-
layouts.forEach(layout => {
|
|
1391
|
-
console.log(" " + kolor.dim("• ") + layout);
|
|
1392
|
-
});
|
|
1393
|
-
console.log();
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// List components
|
|
1398
|
-
if (existsSync(componentsDir)) {
|
|
1399
|
-
const components = await readdir(componentsDir).catch(() => []);
|
|
1400
|
-
if (components.length > 0) {
|
|
1401
|
-
console.log(kolor.bold("🧩 Components (") + kolor.cyan(components.length.toString()) + kolor.bold(")"));
|
|
1402
|
-
components.forEach(component => {
|
|
1403
|
-
console.log(" " + kolor.dim("• ") + component);
|
|
1404
|
-
});
|
|
1405
|
-
console.log();
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// List partials
|
|
1410
|
-
if (existsSync(partialsDir)) {
|
|
1411
|
-
const partials = await readdir(partialsDir).catch(() => []);
|
|
1412
|
-
if (partials.length > 0) {
|
|
1413
|
-
console.log(kolor.bold("🧰 Partials (") + kolor.cyan(partials.length.toString()) + kolor.bold(")"));
|
|
1414
|
-
partials.forEach(partial => {
|
|
1415
|
-
console.log(" " + kolor.dim("• ") + partial);
|
|
1416
|
-
});
|
|
1417
|
-
console.log();
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
// List collections
|
|
1422
|
-
if (existsSync(collectionsDir)) {
|
|
1423
|
-
const collections = await readdir(collectionsDir).catch(() => []);
|
|
1424
|
-
const jsonFiles = collections.filter(f => f.endsWith(".json"));
|
|
1425
|
-
if (jsonFiles.length > 0) {
|
|
1426
|
-
console.log(kolor.bold("📁 Collections (") + kolor.cyan(jsonFiles.length.toString()) + kolor.bold(")"));
|
|
1427
|
-
jsonFiles.forEach(collection => {
|
|
1428
|
-
console.log(" " + kolor.dim("• ") + collection);
|
|
1429
|
-
});
|
|
1430
|
-
console.log();
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// List data files
|
|
1435
|
-
if (existsSync(dataDir)) {
|
|
1436
|
-
const dataFiles = await readdir(dataDir).catch(() => []);
|
|
1437
|
-
const jsonFiles = dataFiles.filter(f => f.endsWith(".json"));
|
|
1438
|
-
if (jsonFiles.length > 0) {
|
|
1439
|
-
console.log(kolor.bold("📊 Data (") + kolor.cyan(jsonFiles.length.toString()) + kolor.bold(")"));
|
|
1440
|
-
jsonFiles.forEach(dataFile => {
|
|
1441
|
-
console.log(" " + kolor.dim("• ") + dataFile);
|
|
1442
|
-
});
|
|
1443
|
-
console.log();
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
console.log(kolor.dim("🌲 Tip: Use 'camper make:<type> <name>' to create new content\n"));
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
async function preview() {
|
|
1451
|
-
console.log(kolor.cyan(kolor.bold("🏔️ Building for production preview...\n")));
|
|
1452
|
-
await build();
|
|
1453
|
-
console.log();
|
|
1454
|
-
const config = await loadConfig(cwd);
|
|
1455
|
-
const outDir = resolve(cwd, config.outDir || "dist");
|
|
1456
|
-
console.log(kolor.cyan(kolor.bold("🔥 Starting preview server...\n")));
|
|
1457
|
-
serve(outDir, config.port || 4173);
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
async function main() {
|
|
1461
|
-
const command = argv[2] || "help";
|
|
1462
|
-
|
|
1463
|
-
// Handle flags
|
|
1464
|
-
if (command === "-h" || command === "--help" || command === "help") {
|
|
1465
|
-
showHelp();
|
|
1466
|
-
exit(0);
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
if (command === "-v" || command === "--version") {
|
|
1470
|
-
const version = await getVersion();
|
|
1471
|
-
console.log(`v${version}`);
|
|
1472
|
-
exit(0);
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// Handle make:type commands
|
|
1476
|
-
if (command.startsWith("make:")) {
|
|
1477
|
-
const type = command.substring(5); // Remove 'make:' prefix
|
|
1478
|
-
if (!type) {
|
|
1479
|
-
console.log(kolor.red("❌ No type specified"));
|
|
1480
|
-
console.log(kolor.dim("Run 'camper --help' for available make commands.\n"));
|
|
1481
|
-
exit(1);
|
|
1482
|
-
}
|
|
1483
|
-
await makeContent(type);
|
|
1484
|
-
return;
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
switch (command) {
|
|
1488
|
-
case "init":
|
|
1489
|
-
await init();
|
|
1490
|
-
break;
|
|
1491
|
-
case "dev":
|
|
1492
|
-
await dev();
|
|
1493
|
-
break;
|
|
1494
|
-
case "build":
|
|
1495
|
-
await build();
|
|
1496
|
-
break;
|
|
1497
|
-
case "serve": {
|
|
1498
|
-
const config = await loadConfig(cwd);
|
|
1499
|
-
const outDir = resolve(cwd, config.outDir || "dist");
|
|
1500
|
-
if (!existsSync(outDir)) {
|
|
1501
|
-
await build();
|
|
1502
|
-
}
|
|
1503
|
-
serve(outDir, config.port || 4173);
|
|
1504
|
-
break;
|
|
1505
|
-
}
|
|
1506
|
-
case "preview":
|
|
1507
|
-
await preview();
|
|
1508
|
-
break;
|
|
1509
|
-
case "clean":
|
|
1510
|
-
case "cleanup":
|
|
1511
|
-
await clean();
|
|
1512
|
-
break;
|
|
1513
|
-
case "check":
|
|
1514
|
-
await check();
|
|
1515
|
-
break;
|
|
1516
|
-
case "list":
|
|
1517
|
-
await list();
|
|
1518
|
-
break;
|
|
1519
|
-
case "upgrade":
|
|
1520
|
-
await upgrade();
|
|
1521
|
-
break;
|
|
1522
|
-
default:
|
|
1523
|
-
console.log(kolor.yellow(`Unknown command: ${command}`));
|
|
1524
|
-
console.log(kolor.dim("Run 'camper --help' for usage information."));
|
|
1525
|
-
exit(1);
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
main().catch((err) => {
|
|
1530
|
-
console.error(err);
|
|
1531
|
-
exit(1);
|
|
1532
|
-
});
|