vending-mocha 0.1.0
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/.github/workflows/npm-publish.yml +45 -0
- package/.github/workflows/regenerate-dist.yml +38 -0
- package/LICENSE +201 -0
- package/README.md +85 -0
- package/SKILL.md +82 -0
- package/bin/cli.js +441 -0
- package/eslint.config.js +23 -0
- package/index.html +16 -0
- package/package.json +57 -0
- package/posts/customization-guide.md +45 -0
- package/posts/deploy-to-github-pages.md +109 -0
- package/posts/hello-world.md +20 -0
- package/posts/markdown-features.md +57 -0
- package/prerender.js +221 -0
- package/projects/legacy-api.md +7 -0
- package/projects/task-master.md +7 -0
- package/projects/vending-mocha.md +7 -0
- package/scripts/generate-posts-data.js +41 -0
- package/scripts/generate-projects-data.js +40 -0
- package/scripts/generate-rss.js +75 -0
- package/src/App.css +566 -0
- package/src/App.tsx +33 -0
- package/src/components/Footer.tsx +11 -0
- package/src/components/MarkdownImage.tsx +40 -0
- package/src/components/Profile.tsx +45 -0
- package/src/components/SiteHeader.tsx +44 -0
- package/src/context/ThemeContext.tsx +75 -0
- package/src/entry-client.tsx +32 -0
- package/src/entry-server.tsx +26 -0
- package/src/pages/BlogPost.tsx +93 -0
- package/src/pages/HomePage.tsx +85 -0
- package/src/pages/Projects.tsx +50 -0
- package/src/site.config.ts +38 -0
- package/src/utils/basePath.ts +21 -0
- package/src/utils/date.ts +17 -0
- package/src/utils/frontmatter.ts +32 -0
- package/static/favicon.ico +0 -0
- package/static/images/profile.png +0 -0
- package/tsconfig.app.json +36 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +61 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import figlet from 'figlet';
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const IGNORE_FILES = [
|
|
16
|
+
'node_modules',
|
|
17
|
+
'.git',
|
|
18
|
+
'.github',
|
|
19
|
+
'dist',
|
|
20
|
+
'docs',
|
|
21
|
+
'bin',
|
|
22
|
+
'package-lock.json',
|
|
23
|
+
'.DS_Store'
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function getAsciiArt() {
|
|
27
|
+
const cup = `
|
|
28
|
+
( (
|
|
29
|
+
) )
|
|
30
|
+
.______.
|
|
31
|
+
| |]
|
|
32
|
+
\\ /
|
|
33
|
+
\`----'
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
// 1. Generate Plain Text
|
|
37
|
+
const vending = figlet.textSync('Vending', { horizontalLayout: 'full' });
|
|
38
|
+
const mocha = figlet.textSync('Mocha', { horizontalLayout: 'full' });
|
|
39
|
+
|
|
40
|
+
// 2. Prepare Lines
|
|
41
|
+
const vendingLines = vending.split('\n');
|
|
42
|
+
const mochaLines = mocha.split('\n');
|
|
43
|
+
const cupLines = cup.split('\n').filter(line => line.trim().length > 0);
|
|
44
|
+
|
|
45
|
+
// 3. Calculate Layout Dimensions
|
|
46
|
+
const mochaWidth = Math.max(...mochaLines.map(line => line.length));
|
|
47
|
+
|
|
48
|
+
// Combine Mocha + Cup logic to find total max width
|
|
49
|
+
// We need to see how wide the bottom section (Mocha + Cup) is vs the top section (Vending)
|
|
50
|
+
|
|
51
|
+
// Bottom Section Width
|
|
52
|
+
let maxBottomWidth = 0;
|
|
53
|
+
const bottomHeight = Math.max(mochaLines.length, cupLines.length);
|
|
54
|
+
for (let i = 0; i < bottomHeight; i++) {
|
|
55
|
+
const mLen = (mochaLines[i] || '').length;
|
|
56
|
+
const cLen = (cupLines[i] || '').length;
|
|
57
|
+
// Mocha + 3 spaces + Cup
|
|
58
|
+
const lineLen = Math.max(mLen, mochaWidth) + 3 + cLen;
|
|
59
|
+
if (lineLen > maxBottomWidth) maxBottomWidth = lineLen;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Top Section Width
|
|
63
|
+
const maxTopWidth = Math.max(...vendingLines.map(line => line.length));
|
|
64
|
+
|
|
65
|
+
// Overall Max Width
|
|
66
|
+
const contentWidth = Math.max(maxTopWidth, maxBottomWidth);
|
|
67
|
+
|
|
68
|
+
// 4. Construct Art with Border
|
|
69
|
+
const lines = [];
|
|
70
|
+
|
|
71
|
+
// Top Border
|
|
72
|
+
// Width = contentWidth + 2 spaces padding on each side = contentWidth + 4 ?
|
|
73
|
+
// Let's stick to the design: │ Content │ (2 spaces padding)
|
|
74
|
+
// So inner width = contentWidth + 4.
|
|
75
|
+
// Border line length = contentWidth + 4.
|
|
76
|
+
|
|
77
|
+
const borderLine = '─'.repeat(contentWidth + 4);
|
|
78
|
+
lines.push(chalk.cyan('╭' + borderLine + '╮'));
|
|
79
|
+
|
|
80
|
+
// Helper to push a bordered line
|
|
81
|
+
const pushLine = (str, strLength) => {
|
|
82
|
+
const padding = ' '.repeat(contentWidth - strLength);
|
|
83
|
+
// We manually construct the line: │ <str> <padding> │
|
|
84
|
+
// But <str> might contain ANSI codes, so we need to be careful not to count them in length,
|
|
85
|
+
// which is why we pass strLength explicitly.
|
|
86
|
+
lines.push(chalk.cyan('│ ') + str + padding + chalk.cyan(' │'));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Render Vending (Top)
|
|
90
|
+
for (const line of vendingLines) {
|
|
91
|
+
pushLine(chalk.cyan(line), line.length);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Gap? The user art in previous turn didn't have a gap, but it looks better with one maybe?
|
|
95
|
+
// The previous output didn't have a huge gap. Let's not add extra vertical gap to keep it compact unless needed.
|
|
96
|
+
// Actually, let's add one empty line for separation if it looks cramped.
|
|
97
|
+
// Figlet art usually has some whitespace. Let's mimic the test_border.js which didn't verify vertical spacing explicitly but looked okay.
|
|
98
|
+
// I'll skip explicit vertical gap to match previous compactness, unless I see reason to add it.
|
|
99
|
+
|
|
100
|
+
// Render Mocha + Cup (Bottom)
|
|
101
|
+
for (let i = 0; i < bottomHeight; i++) {
|
|
102
|
+
const mLine = mochaLines[i] || '';
|
|
103
|
+
const cLine = cupLines[i] || '';
|
|
104
|
+
|
|
105
|
+
// Pad mocha part to mochaWidth
|
|
106
|
+
const mPad = ' '.repeat(mochaWidth - mLine.length);
|
|
107
|
+
|
|
108
|
+
const combinedStr = chalk.cyan(mLine) + mPad + ' ' + chalk.yellow(cLine);
|
|
109
|
+
const combinedLen = mochaWidth + 3 + cLine.length;
|
|
110
|
+
|
|
111
|
+
pushLine(combinedStr, combinedLen);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Bottom Border
|
|
115
|
+
lines.push(chalk.cyan('╰' + borderLine + '╯'));
|
|
116
|
+
lines.push(''); // Final newline
|
|
117
|
+
|
|
118
|
+
return lines.join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function copyRecursiveSync(src, dest, destRoot) {
|
|
122
|
+
const exists = fs.existsSync(src);
|
|
123
|
+
const stats = exists && fs.statSync(src);
|
|
124
|
+
const isDirectory = exists && stats.isDirectory();
|
|
125
|
+
const basename = path.basename(src);
|
|
126
|
+
|
|
127
|
+
if (IGNORE_FILES.includes(basename)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Prevent infinite recursion if destination is inside source
|
|
132
|
+
if (destRoot && path.resolve(src) === path.resolve(destRoot)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (isDirectory) {
|
|
137
|
+
if (!fs.existsSync(dest)) {
|
|
138
|
+
fs.mkdirSync(dest);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fs.readdirSync(src).forEach((childItemName) => {
|
|
142
|
+
copyRecursiveSync(
|
|
143
|
+
path.join(src, childItemName),
|
|
144
|
+
path.join(dest, childItemName),
|
|
145
|
+
destRoot
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
fs.copyFileSync(src, dest);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function updateSiteConfig(projectDir, config) {
|
|
154
|
+
const configPath = path.join(projectDir, 'src', 'site.config.ts');
|
|
155
|
+
if (fs.existsSync(configPath)) {
|
|
156
|
+
let content = fs.readFileSync(configPath, 'utf8');
|
|
157
|
+
|
|
158
|
+
// Update title
|
|
159
|
+
if (config.title) {
|
|
160
|
+
content = content.replace(/title:\s*".*?"/, `title: "${config.title}"`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Update URL
|
|
164
|
+
if (config.url) {
|
|
165
|
+
content = content.replace(/url:\s*".*?"/, `url: "${config.url}"`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fs.writeFileSync(configPath, content, 'utf8');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function updatePackageJson(projectDir, config) {
|
|
173
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
174
|
+
if (fs.existsSync(pkgPath)) {
|
|
175
|
+
try {
|
|
176
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
177
|
+
pkg.name = config.projectName;
|
|
178
|
+
pkg.version = '0.0.0';
|
|
179
|
+
pkg.description = config.description || `My new blog built with vending-mocha`;
|
|
180
|
+
|
|
181
|
+
// Remove the bin entry so the new project doesn't try to be a CLI itself
|
|
182
|
+
if (pkg.bin) {
|
|
183
|
+
delete pkg.bin;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Remove CLI-specific dependencies from the new project
|
|
187
|
+
if (pkg.dependencies) {
|
|
188
|
+
delete pkg.dependencies['figlet'];
|
|
189
|
+
delete pkg.dependencies['inquirer'];
|
|
190
|
+
delete pkg.dependencies['chalk'];
|
|
191
|
+
delete pkg.dependencies['commander'];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.warn(chalk.yellow('Failed to update package.json:', e.message));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function handleNew(projectName, options) {
|
|
202
|
+
console.clear();
|
|
203
|
+
console.log(getAsciiArt());
|
|
204
|
+
|
|
205
|
+
// Interactive Prompts
|
|
206
|
+
const questions = [];
|
|
207
|
+
|
|
208
|
+
if (!projectName) {
|
|
209
|
+
questions.push({
|
|
210
|
+
type: 'input',
|
|
211
|
+
name: 'projectName',
|
|
212
|
+
message: 'What is the name of your new project?',
|
|
213
|
+
default: 'my-vending-mocha-blog',
|
|
214
|
+
validate: (input) => {
|
|
215
|
+
if (/^([a-z0-9\-\_\.]+)$/.test(input)) return true;
|
|
216
|
+
return 'Project name may only include letters, numbers, underscores and hashes.';
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
questions.push({
|
|
222
|
+
type: 'input',
|
|
223
|
+
name: 'title',
|
|
224
|
+
message: 'What is the title of your blog?',
|
|
225
|
+
default: (answers) => answers.projectName || projectName
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
questions.push({
|
|
229
|
+
type: 'input',
|
|
230
|
+
name: 'url',
|
|
231
|
+
message: 'What is the production URL of your blog?',
|
|
232
|
+
default: (answers) => `https://example.com/${answers.projectName || projectName}`
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
questions.push({
|
|
236
|
+
type: 'input',
|
|
237
|
+
name: 'description',
|
|
238
|
+
message: 'Write a short description for your blog:',
|
|
239
|
+
default: 'A personal blogging framework for developers.'
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const answers = await inquirer.prompt(questions);
|
|
243
|
+
|
|
244
|
+
// Merge args and answers
|
|
245
|
+
const config = {
|
|
246
|
+
projectName: projectName || answers.projectName,
|
|
247
|
+
title: answers.title,
|
|
248
|
+
url: answers.url,
|
|
249
|
+
description: answers.description
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const currentDir = process.cwd();
|
|
253
|
+
const projectDir = path.join(currentDir, config.projectName);
|
|
254
|
+
const templateDir = path.join(__dirname, '..');
|
|
255
|
+
|
|
256
|
+
if (fs.existsSync(projectDir)) {
|
|
257
|
+
console.error(chalk.red(`Directory ${config.projectName} already exists.`));
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(chalk.blue(`\nCreating new project in ${projectDir}...\n`));
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// 1. Copy files
|
|
265
|
+
const spinner = { start: () => console.log(chalk.gray('Copying template files...')), succeed: () => console.log(chalk.green('✔ Files copied')) }; // Simple mock spinner
|
|
266
|
+
spinner.start();
|
|
267
|
+
copyRecursiveSync(templateDir, projectDir, projectDir);
|
|
268
|
+
spinner.succeed();
|
|
269
|
+
|
|
270
|
+
// 2. Update config
|
|
271
|
+
console.log(chalk.gray('Updating configuration...'));
|
|
272
|
+
updateSiteConfig(projectDir, config);
|
|
273
|
+
console.log(chalk.green('✔ Configuration updated'));
|
|
274
|
+
|
|
275
|
+
// 3. Update package.json
|
|
276
|
+
console.log(chalk.gray('Updating package.json...'));
|
|
277
|
+
updatePackageJson(projectDir, config);
|
|
278
|
+
console.log(chalk.green('✔ package.json updated'));
|
|
279
|
+
|
|
280
|
+
// 4. Initialize Git
|
|
281
|
+
console.log(chalk.gray('Initializing git repository...'));
|
|
282
|
+
try {
|
|
283
|
+
execSync('git init', { cwd: projectDir, stdio: 'ignore' });
|
|
284
|
+
execSync('git add .', { cwd: projectDir, stdio: 'ignore' });
|
|
285
|
+
execSync('git commit -m "Initial commit from vending-mocha"', { cwd: projectDir, stdio: 'ignore' });
|
|
286
|
+
console.log(chalk.green('✔ Git initialized'));
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.warn(chalk.yellow('⚠ Failed to initialize git repository (git might not be installed).'));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log(chalk.green(`\nSuccess! Created ${config.projectName} at ${projectDir}`));
|
|
292
|
+
console.log('\nInside that directory, you can run:');
|
|
293
|
+
console.log(chalk.cyan(` cd ${config.projectName}`));
|
|
294
|
+
console.log(chalk.cyan(' npm install'));
|
|
295
|
+
console.log(chalk.cyan(' npm run dev'));
|
|
296
|
+
console.log('\nHappy blogging!');
|
|
297
|
+
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.error(chalk.red('Failed to create project:', error));
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function handleUpgrade() {
|
|
305
|
+
console.clear();
|
|
306
|
+
console.log(getAsciiArt());
|
|
307
|
+
|
|
308
|
+
const currentDir = process.cwd();
|
|
309
|
+
const templateDir = path.join(__dirname, '..');
|
|
310
|
+
|
|
311
|
+
// 1. Verify it is a vending-mocha project
|
|
312
|
+
const siteConfigPath = path.join(currentDir, 'src', 'site.config.ts');
|
|
313
|
+
const localPkgPath = path.join(currentDir, 'package.json');
|
|
314
|
+
|
|
315
|
+
if (!fs.existsSync(siteConfigPath) || !fs.existsSync(localPkgPath)) {
|
|
316
|
+
console.error(chalk.red('Error: Current directory does not appear to be a vending-mocha project.'));
|
|
317
|
+
console.error(chalk.yellow('Ensure you are in the root of your project (containing package.json and src/site.config.ts).'));
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
console.log(chalk.yellow('WARNING: This will overwrite project files to the latest version of vending-mocha.'));
|
|
322
|
+
console.log(chalk.yellow('Your content (posts/, projects/) and configuration (src/site.config.ts) will be preserved.'));
|
|
323
|
+
console.log(chalk.yellow('Please ensure you have committed your changes before proceeding.'));
|
|
324
|
+
|
|
325
|
+
const { proceed } = await inquirer.prompt([
|
|
326
|
+
{
|
|
327
|
+
type: 'confirm',
|
|
328
|
+
name: 'proceed',
|
|
329
|
+
message: 'Are you sure you want to upgrade?',
|
|
330
|
+
default: false
|
|
331
|
+
}
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
if (!proceed) {
|
|
335
|
+
console.log(chalk.blue('Upgrade cancelled.'));
|
|
336
|
+
process.exit(0);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
console.log(chalk.blue('\nUpgrading project...\n'));
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
// 2. Copy files with exclusions
|
|
343
|
+
const upgradeIgnore = [
|
|
344
|
+
...IGNORE_FILES,
|
|
345
|
+
'posts',
|
|
346
|
+
'projects',
|
|
347
|
+
'src/site.config.ts', // Important: Preserve config
|
|
348
|
+
'package.json' // We handle this separately
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
function copyUpgradeSync(src, dest) {
|
|
352
|
+
const basename = path.basename(src);
|
|
353
|
+
const relPath = path.relative(templateDir, src);
|
|
354
|
+
|
|
355
|
+
if (upgradeIgnore.includes(basename) || upgradeIgnore.includes(relPath)) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Also ignore if it is exactly the file we want to skip (normalized)
|
|
360
|
+
if (relPath === 'src/site.config.ts') return;
|
|
361
|
+
|
|
362
|
+
// Prevent infinite recursion: if src is the current directory (destination), skip it
|
|
363
|
+
if (path.resolve(src) === path.resolve(currentDir)) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const exists = fs.existsSync(src);
|
|
368
|
+
const stats = exists && fs.statSync(src);
|
|
369
|
+
const isDirectory = exists && stats.isDirectory();
|
|
370
|
+
|
|
371
|
+
if (isDirectory) {
|
|
372
|
+
if (!fs.existsSync(dest)) {
|
|
373
|
+
fs.mkdirSync(dest);
|
|
374
|
+
}
|
|
375
|
+
fs.readdirSync(src).forEach((child) => {
|
|
376
|
+
copyUpgradeSync(path.join(src, child), path.join(dest, child));
|
|
377
|
+
});
|
|
378
|
+
} else {
|
|
379
|
+
fs.copyFileSync(src, dest);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const spinner = { start: () => console.log(chalk.gray('Updating core files...')), succeed: () => console.log(chalk.green('✔ Core files updated')) };
|
|
384
|
+
spinner.start();
|
|
385
|
+
copyUpgradeSync(templateDir, currentDir);
|
|
386
|
+
spinner.succeed();
|
|
387
|
+
|
|
388
|
+
// 3. Merge package.json
|
|
389
|
+
console.log(chalk.gray('Merging package.json...'));
|
|
390
|
+
const localPkg = JSON.parse(fs.readFileSync(localPkgPath, 'utf8'));
|
|
391
|
+
const templatePkg = JSON.parse(fs.readFileSync(path.join(templateDir, 'package.json'), 'utf8'));
|
|
392
|
+
|
|
393
|
+
// Update scripts
|
|
394
|
+
localPkg.scripts = { ...localPkg.scripts, ...templatePkg.scripts };
|
|
395
|
+
|
|
396
|
+
// Update dependencies (add new ones, update versions)
|
|
397
|
+
localPkg.dependencies = { ...localPkg.dependencies, ...templatePkg.dependencies };
|
|
398
|
+
localPkg.devDependencies = { ...localPkg.devDependencies, ...templatePkg.devDependencies };
|
|
399
|
+
|
|
400
|
+
// Ensure we remove CLI deps if they somehow crept in or strictly enforce clean deps
|
|
401
|
+
if (localPkg.dependencies) {
|
|
402
|
+
delete localPkg.dependencies['figlet'];
|
|
403
|
+
delete localPkg.dependencies['inquirer'];
|
|
404
|
+
delete localPkg.dependencies['chalk'];
|
|
405
|
+
delete localPkg.dependencies['commander'];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
fs.writeFileSync(localPkgPath, JSON.stringify(localPkg, null, 2));
|
|
409
|
+
console.log(chalk.green('✔ package.json merged'));
|
|
410
|
+
|
|
411
|
+
console.log(chalk.green('\nUpgrade complete!'));
|
|
412
|
+
console.log(chalk.cyan('You may need to run `npm install` to update dependencies.'));
|
|
413
|
+
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.error(chalk.red('Failed to upgrade project:', error));
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const program = new Command();
|
|
421
|
+
|
|
422
|
+
program
|
|
423
|
+
.name('vending-mocha')
|
|
424
|
+
.description('A personal blogging framework for developers')
|
|
425
|
+
.version('0.0.0')
|
|
426
|
+
.addHelpText('before', getAsciiArt());
|
|
427
|
+
|
|
428
|
+
program.command('new')
|
|
429
|
+
.description('Create a new Vending Mocha project')
|
|
430
|
+
.argument('[project-name]', 'Name of the project directory')
|
|
431
|
+
.action(handleNew);
|
|
432
|
+
|
|
433
|
+
program.command('upgrade')
|
|
434
|
+
.description('Upgrade an existing Vending Mocha project')
|
|
435
|
+
.action(handleUpgrade);
|
|
436
|
+
|
|
437
|
+
program.parse(process.argv);
|
|
438
|
+
|
|
439
|
+
if (!process.argv.slice(2).length) {
|
|
440
|
+
program.outputHelp();
|
|
441
|
+
}
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(['dist', 'docs']),
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs.flat.recommended,
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
])
|
package/index.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
|
+
<!--app-head-->
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"><!--app-html--></div>
|
|
13
|
+
<script type="module" src="/src/entry-client.tsx"></script>
|
|
14
|
+
</body>
|
|
15
|
+
|
|
16
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vending-mocha",
|
|
3
|
+
"description": "A personal blogging framework for developers.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"vending-mocha": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "npm run build:all && vite",
|
|
12
|
+
"build:client": "tsc -b && vite build --outDir docs",
|
|
13
|
+
"build:server": "tsc -b && vite build --ssr src/entry-server.tsx --outDir dist/server",
|
|
14
|
+
"prebuild": "node scripts/generate-posts-data.js && node scripts/generate-projects-data.js",
|
|
15
|
+
"build": "npm run prebuild && npm run build:client && npm run build:server && node prerender.js && node scripts/generate-rss.js",
|
|
16
|
+
"build:all": "npm run build && for d in projects/*; do if [ -d \"$d\" ]; then (cd \"$d\" && npm run build); fi; done",
|
|
17
|
+
"preview": "npm run build:all && vite preview --outDir docs"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@types/react-syntax-highlighter": "15.5.13",
|
|
21
|
+
"chalk": "^5.6.2",
|
|
22
|
+
"commander": "^14.0.3",
|
|
23
|
+
"date-fns": "^4.1.0",
|
|
24
|
+
"figlet": "^1.10.0",
|
|
25
|
+
"gray-matter": "^4.0.3",
|
|
26
|
+
"inquirer": "^13.2.5",
|
|
27
|
+
"lucide-react": "^0.564.0",
|
|
28
|
+
"react": "^19.2.0",
|
|
29
|
+
"react-dom": "^19.2.0",
|
|
30
|
+
"react-helmet-async": "^2.0.5",
|
|
31
|
+
"react-markdown": "^10.1.0",
|
|
32
|
+
"react-router-dom": "^7.13.0",
|
|
33
|
+
"react-syntax-highlighter": "16.1.0",
|
|
34
|
+
"remark-breaks": "^4.0.0",
|
|
35
|
+
"remark-gfm": "^4.0.1"
|
|
36
|
+
},
|
|
37
|
+
"overrides": {
|
|
38
|
+
"react-helmet-async": {
|
|
39
|
+
"react": "$react"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@eslint/js": "^9.39.1",
|
|
44
|
+
"@types/node": "^24.10.1",
|
|
45
|
+
"@types/react": "^19.2.7",
|
|
46
|
+
"@types/react-dom": "^19.2.3",
|
|
47
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
48
|
+
"eslint": "^9.39.1",
|
|
49
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
50
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
51
|
+
"globals": "^16.5.0",
|
|
52
|
+
"prettier": "^3.8.1",
|
|
53
|
+
"typescript": "~5.9.3",
|
|
54
|
+
"typescript-eslint": "^8.48.0",
|
|
55
|
+
"vite": "^7.3.1"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "How to Customize Vending Mocha 🎨"
|
|
3
|
+
date: "2026-02-17 03:00:00"
|
|
4
|
+
summary: "A quick guide on how to update the configuration, styling, and content of your new blog."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Customization Guide
|
|
8
|
+
|
|
9
|
+
So you've cloned **Vending Mocha**. What's next? Here is a quick guide to making it your own.
|
|
10
|
+
|
|
11
|
+
### 1. Update Configuration
|
|
12
|
+
|
|
13
|
+
Open `src/site.config.ts` and update the following:
|
|
14
|
+
|
|
15
|
+
- **title**: Your name or site title.
|
|
16
|
+
- **description**: A short bio or site description.
|
|
17
|
+
- **url**: Your website URL (used for SEO).
|
|
18
|
+
- **theme**: Customize colors for light and dark modes.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
export const siteConfig = {
|
|
22
|
+
title: "My Awesome Blog",
|
|
23
|
+
// ...
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 2. Add Your Projects
|
|
28
|
+
|
|
29
|
+
Add new `.md` files with frontmatter to `/projects/` to add your projects.
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
---
|
|
33
|
+
title: "My Project"
|
|
34
|
+
description: "What is it?"
|
|
35
|
+
link: "https://github.com/..."
|
|
36
|
+
status: "active" // active, dead, inactive
|
|
37
|
+
weight: 5 // higher weight projects are displayed first
|
|
38
|
+
---
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Write Posts
|
|
42
|
+
|
|
43
|
+
Just add new `.md` files to `/posts/`. The filename becomes the slug (e.g., `/posts/my-post.md` -> `/post/my-post`).
|
|
44
|
+
|
|
45
|
+
That's it! You're ready to deploy. 🚀
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Deploying to GitHub Pages 🚀"
|
|
3
|
+
date: "2026-02-17 04:00:00"
|
|
4
|
+
summary: "A step-by-step guide to deploying your Vending Mocha blog to GitHub Pages using GitHub Actions."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Deploying your Vending Mocha blog to GitHub Pages is straightforward. We'll use GitHub Actions to automatically build and deploy your site whenever you push changes to the `main` branch.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
1. A GitHub account.
|
|
12
|
+
2. A Vending Mocha project pushed to a GitHub repository.
|
|
13
|
+
|
|
14
|
+
## Step 1: Configure Your Site
|
|
15
|
+
|
|
16
|
+
Open `src/site.config.ts` and ensure the `url` property matches your GitHub Pages URL.
|
|
17
|
+
|
|
18
|
+
If you are using a custom domain:
|
|
19
|
+
```typescript
|
|
20
|
+
export const siteConfig = {
|
|
21
|
+
title: "My Blog",
|
|
22
|
+
url: "https://www.example.com", // Your custom domain
|
|
23
|
+
// ...
|
|
24
|
+
};
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If you are using the default GitHub Pages URL (`username.github.io/repo-name`):
|
|
28
|
+
```typescript
|
|
29
|
+
export const siteConfig = {
|
|
30
|
+
title: "My Blog",
|
|
31
|
+
url: "https://username.github.io/repo-name", // REPLACE with your actual URL
|
|
32
|
+
// ...
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> **Important:** The `url` setting is critical for asset loading, especially if your site is hosted in a subdirectory (like `/repo-name/`).
|
|
37
|
+
|
|
38
|
+
## Step 2: Create the GitHub Actions Workflow
|
|
39
|
+
|
|
40
|
+
Create a new file in your repository at `.github/workflows/deploy.yml` and paste the following content:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
name: Deploy to GitHub Pages
|
|
44
|
+
|
|
45
|
+
on:
|
|
46
|
+
push:
|
|
47
|
+
branches:
|
|
48
|
+
- main
|
|
49
|
+
paths-ignore:
|
|
50
|
+
- 'docs/**' # Don't trigger if only the output folder changes
|
|
51
|
+
|
|
52
|
+
jobs:
|
|
53
|
+
build_and_deploy:
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
permissions:
|
|
56
|
+
contents: write
|
|
57
|
+
|
|
58
|
+
steps:
|
|
59
|
+
- name: Checkout repository
|
|
60
|
+
uses: actions/checkout@v4
|
|
61
|
+
|
|
62
|
+
- name: Setup Node.js
|
|
63
|
+
uses: actions/setup-node@v4
|
|
64
|
+
with:
|
|
65
|
+
node-version: '20'
|
|
66
|
+
cache: 'npm'
|
|
67
|
+
|
|
68
|
+
- name: Install dependencies
|
|
69
|
+
run: npm ci
|
|
70
|
+
|
|
71
|
+
- name: Build project
|
|
72
|
+
run: npm run build:all
|
|
73
|
+
|
|
74
|
+
- name: Deploy to GitHub Pages
|
|
75
|
+
uses: peaceiris/actions-gh-pages@v3
|
|
76
|
+
with:
|
|
77
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
78
|
+
publish_dir: ./docs
|
|
79
|
+
cname: your-domain.com # Optional - if you are using a custom domain
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This workflow does the following:
|
|
83
|
+
1. Triggers on every push to `main`.
|
|
84
|
+
2. Sets up Node.js.
|
|
85
|
+
3. Installs dependencies.
|
|
86
|
+
4. Builds your site (and any sub-projects).
|
|
87
|
+
5. Deploys the `docs` folder (the build output) to the `gh-pages` branch.
|
|
88
|
+
|
|
89
|
+
## Step 3: Configure GitHub Pages Settings
|
|
90
|
+
|
|
91
|
+
1. Go to your repository on GitHub.
|
|
92
|
+
2. Navigate to **Settings** > **Pages**.
|
|
93
|
+
3. Under **Build and deployment**, select **Deploy from a branch**.
|
|
94
|
+
4. Under **Branch**, select `gh-pages` and ensure the folder is `/ (root)`.
|
|
95
|
+
5. Click **Save**.
|
|
96
|
+
|
|
97
|
+
## Step 4: Push and Verify
|
|
98
|
+
|
|
99
|
+
Commit and push your changes to GitHub:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git add .
|
|
103
|
+
git commit -m "Add deployment workflow"
|
|
104
|
+
git push origin main
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Go to the **Actions** tab in your repository to watch the build progress. Once it completes (green checkmark), your site should be live at the URL you configured!
|
|
108
|
+
|
|
109
|
+
Happy blogging! ☕
|