@x-withu/page-withu 0.1.2 → 1.0.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/README.md +7 -7
- package/bin/page-withu.js +61 -357
- package/docs/develop.md +15 -17
- package/docs/setup.md +9 -24
- package/package.json +1 -2
- package/template/src/components/blog-detail.vue +14 -3
- package/template/src/styles/main.css +93 -4
package/README.md
CHANGED
|
@@ -41,18 +41,18 @@
|
|
|
41
41
|
# 安装 page-withu
|
|
42
42
|
npm install -g @x-withu/page-withu
|
|
43
43
|
|
|
44
|
-
#
|
|
45
|
-
page-withu
|
|
46
|
-
|
|
47
|
-
# 也可以通过参数直接指定
|
|
48
|
-
page-withu create my-homepage --title "My Site" --tab-title "My Blog" --favicon /favicon.svg
|
|
44
|
+
# 创建项目,默认配置会写入 config.js,后续可自行编辑
|
|
45
|
+
page-withu new my-homepage
|
|
49
46
|
|
|
50
47
|
# 安装依赖
|
|
51
48
|
cd my-homepage
|
|
52
49
|
npm install
|
|
53
50
|
|
|
54
|
-
#
|
|
55
|
-
|
|
51
|
+
# 本地预览
|
|
52
|
+
page-withu serve
|
|
53
|
+
|
|
54
|
+
# 构建分发产物
|
|
55
|
+
page-withu build
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
下一步?[配置网站](./docs/setup.md)、[部署网站](./docs/deploy.md)、[开发网站](./docs/develop.md)。
|
package/bin/page-withu.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import inquirer from 'inquirer';
|
|
5
4
|
import fs from 'fs-extra';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -13,117 +12,8 @@ const pkg = await fs.readJson(new URL('../package.json', import.meta.url));
|
|
|
13
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
13
|
const __dirname = path.dirname(__filename);
|
|
15
14
|
const templateDir = path.resolve(__dirname, '../template');
|
|
16
|
-
const manifestName = '.page-withu.json';
|
|
17
|
-
const managedRoots = ['src'];
|
|
18
|
-
const managedFiles = ['index.html', 'vite.config.js'];
|
|
19
|
-
const generatedGitignore = `node_modules/
|
|
20
|
-
dist/
|
|
21
|
-
|
|
22
|
-
.env
|
|
23
|
-
.env.*
|
|
24
|
-
!.env.example
|
|
25
|
-
*.local
|
|
26
|
-
|
|
27
|
-
.DS_Store
|
|
28
|
-
.idea/
|
|
29
|
-
`;
|
|
30
|
-
|
|
31
15
|
const program = new Command();
|
|
32
16
|
|
|
33
|
-
function hashContent(content) {
|
|
34
|
-
return createHash('sha256').update(content).digest('hex');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function hashFile(filePath) {
|
|
38
|
-
return hashContent(await fs.readFile(filePath));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function toPosixPath(value) {
|
|
42
|
-
return value.split(path.sep).join('/');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function listFiles(dir) {
|
|
46
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
47
|
-
const files = [];
|
|
48
|
-
|
|
49
|
-
for (const entry of entries) {
|
|
50
|
-
const entryPath = path.join(dir, entry.name);
|
|
51
|
-
if (entry.isDirectory()) {
|
|
52
|
-
files.push(...await listFiles(entryPath));
|
|
53
|
-
} else if (entry.isFile()) {
|
|
54
|
-
files.push(entryPath);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return files;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function listManagedTemplateFiles() {
|
|
62
|
-
const files = [];
|
|
63
|
-
|
|
64
|
-
for (const file of managedFiles) {
|
|
65
|
-
if (await fs.pathExists(path.join(templateDir, file))) files.push(file);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
for (const root of managedRoots) {
|
|
69
|
-
const rootPath = path.join(templateDir, root);
|
|
70
|
-
if (!await fs.pathExists(rootPath)) continue;
|
|
71
|
-
const rootFiles = await listFiles(rootPath);
|
|
72
|
-
files.push(...rootFiles.map((file) => toPosixPath(path.relative(templateDir, file))));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return files.sort((a, b) => a.localeCompare(b));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function createManifest(projectDir, previousManifest = null) {
|
|
79
|
-
const files = {};
|
|
80
|
-
|
|
81
|
-
for (const file of await listManagedTemplateFiles()) {
|
|
82
|
-
const projectFile = path.join(projectDir, file);
|
|
83
|
-
const templateFile = path.join(templateDir, file);
|
|
84
|
-
if (!await fs.pathExists(projectFile)) continue;
|
|
85
|
-
|
|
86
|
-
const projectHash = await hashFile(projectFile);
|
|
87
|
-
const templateHash = await hashFile(templateFile);
|
|
88
|
-
if (projectHash === templateHash) {
|
|
89
|
-
files[file] = templateHash;
|
|
90
|
-
} else if (previousManifest?.files?.[file]) {
|
|
91
|
-
files[file] = previousManifest.files[file];
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
templateVersion: pkg.version,
|
|
97
|
-
updatedAt: new Date().toISOString(),
|
|
98
|
-
files,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function writeManifest(projectDir, dryRun, previousManifest = null) {
|
|
103
|
-
const manifest = await createManifest(projectDir, previousManifest);
|
|
104
|
-
if (!dryRun) {
|
|
105
|
-
await fs.writeJson(path.join(projectDir, manifestName), manifest, { spaces: 2 });
|
|
106
|
-
}
|
|
107
|
-
return manifest;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function renderConfig({ title, tabTitle, favicon, author, footerLinks }) {
|
|
111
|
-
const links = footerLinks.map((link) => ` { label: ${JSON.stringify(link.label)}, url: ${JSON.stringify(link.url)} }`);
|
|
112
|
-
return `export default {
|
|
113
|
-
title: ${JSON.stringify(title)},
|
|
114
|
-
tabTitle: ${JSON.stringify(tabTitle)},
|
|
115
|
-
favicon: ${JSON.stringify(favicon)},
|
|
116
|
-
author: ${JSON.stringify(author)},
|
|
117
|
-
year: new Date().getFullYear(),
|
|
118
|
-
footerLinks: [
|
|
119
|
-
${links.join(',\n')}
|
|
120
|
-
],
|
|
121
|
-
pagination: {
|
|
122
|
-
pageSize: 5
|
|
123
|
-
}
|
|
124
|
-
}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
17
|
async function detectProject(projectDir) {
|
|
128
18
|
const required = ['package.json', 'config.js', 'content', 'src', 'vite.config.js'];
|
|
129
19
|
const missing = [];
|
|
@@ -135,277 +25,91 @@ async function detectProject(projectDir) {
|
|
|
135
25
|
return missing;
|
|
136
26
|
}
|
|
137
27
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (!await fs.pathExists(manifestPath)) return null;
|
|
141
|
-
return fs.readJson(manifestPath);
|
|
28
|
+
function shouldCopyTemplateItem(source) {
|
|
29
|
+
return path.relative(templateDir, source) !== '.gitignore';
|
|
142
30
|
}
|
|
143
31
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const npmignorePath = path.join(projectDir, '.npmignore');
|
|
147
|
-
const hasGitignore = await fs.pathExists(gitignorePath);
|
|
148
|
-
const hasNpmignore = await fs.pathExists(npmignorePath);
|
|
149
|
-
|
|
150
|
-
if (dryRun) return !hasGitignore || hasNpmignore;
|
|
151
|
-
if (hasNpmignore) await fs.remove(npmignorePath);
|
|
152
|
-
if (!hasGitignore) await fs.writeFile(gitignorePath, generatedGitignore);
|
|
153
|
-
|
|
154
|
-
return !hasGitignore || hasNpmignore;
|
|
32
|
+
function normalizePackageName(name) {
|
|
33
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-+|-+$/g, '') || 'page-withu-site';
|
|
155
34
|
}
|
|
156
35
|
|
|
157
|
-
async function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const answer = await inquirer.prompt([{
|
|
162
|
-
type: 'confirm',
|
|
163
|
-
name: 'overwrite',
|
|
164
|
-
message: `${file} has local changes. Overwrite it?`,
|
|
165
|
-
default: false,
|
|
166
|
-
}]);
|
|
167
|
-
|
|
168
|
-
return answer.overwrite;
|
|
169
|
-
}
|
|
36
|
+
async function createProject(projectName = 'my-homepage') {
|
|
37
|
+
const targetDir = projectName.trim() || 'my-homepage';
|
|
38
|
+
const fullPath = path.resolve(process.cwd(), targetDir);
|
|
170
39
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
added: [],
|
|
176
|
-
updated: [],
|
|
177
|
-
skipped: [],
|
|
178
|
-
unchanged: [],
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
for (const file of files) {
|
|
182
|
-
const sourceFile = path.join(templateDir, file);
|
|
183
|
-
const targetFile = path.join(projectDir, file);
|
|
184
|
-
const sourceHash = await hashFile(sourceFile);
|
|
185
|
-
const exists = await fs.pathExists(targetFile);
|
|
186
|
-
|
|
187
|
-
if (!exists) {
|
|
188
|
-
result.added.push(file);
|
|
189
|
-
if (!options.dryRun) await fs.copy(sourceFile, targetFile);
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const currentHash = await hashFile(targetFile);
|
|
194
|
-
if (currentHash === sourceHash) {
|
|
195
|
-
result.unchanged.push(file);
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
40
|
+
if (await fs.pathExists(fullPath)) {
|
|
41
|
+
console.error(chalk.red(`\nError: Directory ${targetDir} already exists.`));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
198
44
|
|
|
199
|
-
|
|
200
|
-
const safeToUpdate = previousHash && currentHash === previousHash;
|
|
201
|
-
const overwrite = safeToUpdate || await shouldOverwriteConflict(file, options);
|
|
45
|
+
const spinner = ora('Creating project...').start();
|
|
202
46
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (!options.dryRun) await fs.copy(sourceFile, targetFile);
|
|
206
|
-
} else {
|
|
207
|
-
result.skipped.push(file);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
47
|
+
try {
|
|
48
|
+
await fs.copy(templateDir, fullPath, { filter: shouldCopyTemplateItem });
|
|
210
49
|
|
|
211
|
-
|
|
212
|
-
|
|
50
|
+
const pkgPath = path.join(fullPath, 'package.json');
|
|
51
|
+
const projectPkg = await fs.readJson(pkgPath);
|
|
52
|
+
projectPkg.name = normalizePackageName(path.basename(targetDir));
|
|
53
|
+
await fs.writeJson(pkgPath, projectPkg, { spaces: 2 });
|
|
213
54
|
|
|
214
|
-
|
|
215
|
-
const next = { ...(projectPkg[key] || {}) };
|
|
216
|
-
let changed = false;
|
|
55
|
+
await fs.ensureDir(path.join(fullPath, 'content/blog'));
|
|
217
56
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
57
|
+
spinner.succeed(chalk.green(`Successfully created PageWithU project in ${targetDir}!`));
|
|
58
|
+
console.log('\nNext steps:');
|
|
59
|
+
console.log(chalk.cyan(` cd ${targetDir}`));
|
|
60
|
+
console.log(chalk.cyan(' npm install'));
|
|
61
|
+
console.log(chalk.cyan(' page-withu serve'));
|
|
62
|
+
console.log(chalk.cyan(' page-withu build\n'));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
spinner.fail(chalk.red('Failed to create project.'));
|
|
65
|
+
console.error(err);
|
|
66
|
+
process.exit(1);
|
|
223
67
|
}
|
|
224
|
-
|
|
225
|
-
if (changed) projectPkg[key] = next;
|
|
226
|
-
return changed;
|
|
227
68
|
}
|
|
228
69
|
|
|
229
|
-
async function
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
mergeSection(projectPkg, templatePkg, 'scripts');
|
|
236
|
-
mergeSection(projectPkg, templatePkg, 'dependencies');
|
|
237
|
-
mergeSection(projectPkg, templatePkg, 'devDependencies');
|
|
238
|
-
|
|
239
|
-
const changed = JSON.stringify(projectPkg) !== before;
|
|
240
|
-
if (changed && !dryRun) {
|
|
241
|
-
await fs.writeJson(projectPkgPath, projectPkg, { spaces: 2 });
|
|
70
|
+
async function runProjectScript(script, args) {
|
|
71
|
+
const missing = await detectProject(process.cwd());
|
|
72
|
+
if (missing.length) {
|
|
73
|
+
console.error(chalk.red('Current directory does not look like a PageWithU project.'));
|
|
74
|
+
console.error(chalk.yellow(`Missing: ${missing.join(', ')}`));
|
|
75
|
+
process.exit(1);
|
|
242
76
|
}
|
|
243
77
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
78
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
79
|
+
const child = spawn(npmCommand, ['run', script, '--', ...args], {
|
|
80
|
+
cwd: process.cwd(),
|
|
81
|
+
stdio: 'inherit',
|
|
82
|
+
});
|
|
249
83
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
if (!summary.added.length && !summary.updated.length && !summary.packageChanged && !summary.gitignoreChanged && !summary.skipped.length) {
|
|
259
|
-
console.log(chalk.green('Page With U project is already up to date.'));
|
|
260
|
-
}
|
|
261
|
-
if (!dryRun && (summary.added.length || summary.updated.length || summary.packageChanged || summary.gitignoreChanged)) {
|
|
262
|
-
console.log(chalk.cyan('\nNext steps:'));
|
|
263
|
-
console.log(chalk.cyan(' npm install'));
|
|
264
|
-
console.log(chalk.cyan(' npm run build'));
|
|
265
|
-
}
|
|
84
|
+
child.on('exit', (code, signal) => {
|
|
85
|
+
if (signal) {
|
|
86
|
+
process.kill(process.pid, signal);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
process.exit(code ?? 1);
|
|
90
|
+
});
|
|
266
91
|
}
|
|
267
92
|
|
|
268
93
|
program
|
|
269
94
|
.name('page-withu')
|
|
270
|
-
.description('CLI to scaffold a lightweight
|
|
95
|
+
.description('CLI to scaffold, preview, and build a lightweight personal homepage')
|
|
271
96
|
.version(pkg.version);
|
|
272
97
|
|
|
273
98
|
program
|
|
274
|
-
.command('
|
|
275
|
-
.description('create a new PageWithU project')
|
|
276
|
-
.
|
|
277
|
-
.option('--tab-title <title>', 'browser tab title')
|
|
278
|
-
.option('--favicon <path-or-url>', 'browser tab favicon path or URL')
|
|
279
|
-
.option('-a, --author <author>', 'author name')
|
|
280
|
-
.option('-g, --github <github>', 'github url')
|
|
281
|
-
.option('-e, --email <email>', 'email address')
|
|
282
|
-
.action(async (projectName, options) => {
|
|
283
|
-
let targetDir = projectName;
|
|
284
|
-
|
|
285
|
-
if (!targetDir) {
|
|
286
|
-
const answers = await inquirer.prompt([
|
|
287
|
-
{
|
|
288
|
-
type: 'input',
|
|
289
|
-
name: 'projectName',
|
|
290
|
-
message: 'What is your project named?',
|
|
291
|
-
default: 'my-homepage',
|
|
292
|
-
},
|
|
293
|
-
]);
|
|
294
|
-
targetDir = answers.projectName;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const fullPath = path.resolve(process.cwd(), targetDir);
|
|
298
|
-
|
|
299
|
-
if (fs.existsSync(fullPath)) {
|
|
300
|
-
console.error(chalk.red(`\nError: Directory ${targetDir} already exists.`));
|
|
301
|
-
process.exit(1);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const title = options.title || (await inquirer.prompt([{
|
|
305
|
-
type: 'input',
|
|
306
|
-
name: 'title',
|
|
307
|
-
message: 'Site Title:',
|
|
308
|
-
default: 'PageWithU',
|
|
309
|
-
}])).title;
|
|
310
|
-
|
|
311
|
-
const author = options.author || (await inquirer.prompt([{
|
|
312
|
-
type: 'input',
|
|
313
|
-
name: 'author',
|
|
314
|
-
message: 'Author Name:',
|
|
315
|
-
default: 'Your Name',
|
|
316
|
-
}])).author;
|
|
317
|
-
|
|
318
|
-
const tabTitle = options.tabTitle || (await inquirer.prompt([{
|
|
319
|
-
type: 'input',
|
|
320
|
-
name: 'tabTitle',
|
|
321
|
-
message: 'Browser Tab Title:',
|
|
322
|
-
default: title,
|
|
323
|
-
}])).tabTitle;
|
|
324
|
-
|
|
325
|
-
const favicon = options.favicon || (await inquirer.prompt([{
|
|
326
|
-
type: 'input',
|
|
327
|
-
name: 'favicon',
|
|
328
|
-
message: 'Favicon path or URL:',
|
|
329
|
-
default: '/src/assets/bulb.svg',
|
|
330
|
-
}])).favicon;
|
|
331
|
-
|
|
332
|
-
const github = options.github !== undefined ? options.github : (await inquirer.prompt([{
|
|
333
|
-
type: 'input',
|
|
334
|
-
name: 'github',
|
|
335
|
-
message: 'GitHub URL (optional):',
|
|
336
|
-
}])).github;
|
|
337
|
-
|
|
338
|
-
const email = options.email !== undefined ? options.email : (await inquirer.prompt([{
|
|
339
|
-
type: 'input',
|
|
340
|
-
name: 'email',
|
|
341
|
-
message: 'Email (optional):',
|
|
342
|
-
}])).email;
|
|
343
|
-
|
|
344
|
-
const spinner = ora('Creating project...').start();
|
|
345
|
-
|
|
346
|
-
try {
|
|
347
|
-
await fs.copy(templateDir, fullPath);
|
|
348
|
-
|
|
349
|
-
const footerLinks = [];
|
|
350
|
-
if (github) footerLinks.push({ label: 'GitHub', url: github });
|
|
351
|
-
if (email) footerLinks.push({ label: 'Email', url: `mailto:${email}` });
|
|
352
|
-
|
|
353
|
-
await fs.writeFile(path.join(fullPath, 'config.js'), renderConfig({ title, tabTitle, favicon, author, footerLinks }));
|
|
354
|
-
|
|
355
|
-
const pkgPath = path.join(fullPath, 'package.json');
|
|
356
|
-
const projectPkg = await fs.readJson(pkgPath);
|
|
357
|
-
projectPkg.name = targetDir.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
358
|
-
await fs.writeJson(pkgPath, projectPkg, { spaces: 2 });
|
|
359
|
-
|
|
360
|
-
await fs.ensureDir(path.join(fullPath, 'content/blog'));
|
|
361
|
-
await ensureGeneratedGitignore(fullPath, false);
|
|
362
|
-
await writeManifest(fullPath, false);
|
|
363
|
-
|
|
364
|
-
spinner.succeed(chalk.green(`Successfully created PageWithU project in ${targetDir}!`));
|
|
365
|
-
console.log('\nNext steps:');
|
|
366
|
-
console.log(chalk.cyan(` cd ${targetDir}`));
|
|
367
|
-
console.log(chalk.cyan(' npm install'));
|
|
368
|
-
console.log(chalk.cyan(' npm run dev\n'));
|
|
369
|
-
|
|
370
|
-
} catch (err) {
|
|
371
|
-
spinner.fail(chalk.red('Failed to create project.'));
|
|
372
|
-
console.error(err);
|
|
373
|
-
process.exit(1);
|
|
374
|
-
}
|
|
375
|
-
});
|
|
99
|
+
.command('new [project-name]')
|
|
100
|
+
.description('create a new PageWithU project with the default template config')
|
|
101
|
+
.action(createProject);
|
|
376
102
|
|
|
377
103
|
program
|
|
378
|
-
.command('
|
|
379
|
-
.description('
|
|
380
|
-
.
|
|
381
|
-
.
|
|
382
|
-
.option('--force', 'overwrite changed template-managed files')
|
|
383
|
-
.action(async (options) => {
|
|
384
|
-
const projectDir = process.cwd();
|
|
385
|
-
const spinner = ora(options.dryRun ? 'Checking project update...' : 'Updating project...').start();
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
const missing = await detectProject(projectDir);
|
|
389
|
-
if (missing.length) {
|
|
390
|
-
spinner.fail(chalk.red('Current directory does not look like a PageWithU project.'));
|
|
391
|
-
console.error(chalk.yellow(`Missing: ${missing.join(', ')}`));
|
|
392
|
-
process.exit(1);
|
|
393
|
-
}
|
|
104
|
+
.command('serve [args...]')
|
|
105
|
+
.description('start local preview for the current PageWithU project')
|
|
106
|
+
.allowUnknownOption(true)
|
|
107
|
+
.action((args = []) => runProjectScript('dev', args));
|
|
394
108
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (!options.dryRun) await writeManifest(projectDir, false, previousManifest);
|
|
402
|
-
|
|
403
|
-
printUpdateSummary({ ...updates, packageChanged, gitignoreChanged }, options.dryRun);
|
|
404
|
-
} catch (err) {
|
|
405
|
-
spinner.fail(chalk.red('Failed to update project.'));
|
|
406
|
-
console.error(err);
|
|
407
|
-
process.exit(1);
|
|
408
|
-
}
|
|
409
|
-
});
|
|
109
|
+
program
|
|
110
|
+
.command('build [args...]')
|
|
111
|
+
.description('build dist assets for the current PageWithU project')
|
|
112
|
+
.allowUnknownOption(true)
|
|
113
|
+
.action((args = []) => runProjectScript('build', args));
|
|
410
114
|
|
|
411
115
|
program.parse();
|
package/docs/develop.md
CHANGED
|
@@ -46,18 +46,18 @@ bin/page-withu.js
|
|
|
46
46
|
|
|
47
47
|
当前 CLI 主要负责:
|
|
48
48
|
|
|
49
|
-
1.
|
|
50
|
-
2.
|
|
51
|
-
3.
|
|
52
|
-
4. 修改目标项目 `package.json` 的 `name` 字段。
|
|
53
|
-
5. 输出下一步命令提示。
|
|
49
|
+
1. `new`:复制 `template/` 到目标目录,保留默认配置。
|
|
50
|
+
2. `serve`:在当前主页项目中启动本地预览。
|
|
51
|
+
3. `build`:在当前主页项目中构建 `dist` 产物。
|
|
54
52
|
|
|
55
53
|
本地调试 CLI:
|
|
56
54
|
|
|
57
55
|
```bash
|
|
58
56
|
node bin/page-withu.js --help
|
|
59
|
-
node bin/page-withu.js
|
|
60
|
-
|
|
57
|
+
node bin/page-withu.js new my-homepage
|
|
58
|
+
cd my-homepage
|
|
59
|
+
node /path/to/page-withu/bin/page-withu.js serve
|
|
60
|
+
node /path/to/page-withu/bin/page-withu.js build
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
如果想全局挂载本地版本:
|
|
@@ -65,14 +65,12 @@ node bin/page-withu.js update --dry-run
|
|
|
65
65
|
```bash
|
|
66
66
|
npm link
|
|
67
67
|
page-withu --help
|
|
68
|
-
page-withu
|
|
68
|
+
page-withu new my-homepage
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
调整 CLI 行为时,通常需要同步修改:
|
|
72
72
|
|
|
73
|
-
- `bin/page-withu.js` 中的 commander
|
|
74
|
-
- 交互式 prompt 的默认值。
|
|
75
|
-
- 生成 `config.js` 的逻辑。
|
|
73
|
+
- `bin/page-withu.js` 中的 commander command。
|
|
76
74
|
- 必要时修改 `template/config.js` 的默认字段。
|
|
77
75
|
- 必要时修改模板中读取配置的 Vue 组件。
|
|
78
76
|
- 必要时更新 `README.md` 和 `docs/setup.md`。
|
|
@@ -82,13 +80,13 @@ page-withu create my-homepage
|
|
|
82
80
|
```bash
|
|
83
81
|
mkdir -p /tmp/page-withu-test
|
|
84
82
|
cd /tmp/page-withu-test
|
|
85
|
-
node /path/to/page-withu/bin/page-withu.js
|
|
83
|
+
node /path/to/page-withu/bin/page-withu.js new demo-site
|
|
86
84
|
cd demo-site
|
|
87
85
|
npm install
|
|
88
|
-
|
|
86
|
+
node /path/to/page-withu/bin/page-withu.js build
|
|
89
87
|
```
|
|
90
88
|
|
|
91
|
-
|
|
89
|
+
如果构建成功,说明模板可以被正常生成和构建。
|
|
92
90
|
|
|
93
91
|
## 开发 Template
|
|
94
92
|
|
|
@@ -202,11 +200,11 @@ npm publish --access public
|
|
|
202
200
|
|
|
203
201
|
```bash
|
|
204
202
|
npm install -g @x-withu/page-withu
|
|
205
|
-
page-withu
|
|
203
|
+
page-withu new my-homepage
|
|
206
204
|
```
|
|
207
205
|
|
|
208
206
|
或在线构建无需本地安装:
|
|
209
207
|
|
|
210
208
|
```bash
|
|
211
|
-
npx @x-withu/page-withu
|
|
209
|
+
npx @x-withu/page-withu new my-homepage
|
|
212
210
|
```
|
package/docs/setup.md
CHANGED
|
@@ -83,36 +83,21 @@ index_img: https://example.com/cover.jpg
|
|
|
83
83
|
- `tags`:文章标签。
|
|
84
84
|
- `index_img`:博客列表封面图,可选;不填会保留占位布局。
|
|
85
85
|
|
|
86
|
-
##
|
|
86
|
+
## CLI 命令
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
Page With U 只提供三个常用命令:
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
page-withu
|
|
94
|
-
page-withu update
|
|
91
|
+
page-withu new my-homepage
|
|
92
|
+
page-withu serve
|
|
93
|
+
page-withu build
|
|
95
94
|
```
|
|
96
95
|
|
|
97
|
-
`page-withu
|
|
96
|
+
- `page-withu new my-homepage`:新建主页项目,所有站点配置都保持模板默认值。
|
|
97
|
+
- `page-withu serve`:在当前项目目录启动本地预览,对应模板项目的 `npm run dev`。
|
|
98
|
+
- `page-withu build`:在当前项目目录构建 `dist` 产物,对应模板项目的 `npm run build`。
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
- `config.js`
|
|
102
|
-
- `content/`
|
|
103
|
-
|
|
104
|
-
如果你修改过模板源码,更新时会跳过有冲突的文件并在终端列出来。确认要覆盖这些模板源码时,可以使用:
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
page-withu update --force
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
更新完成后建议执行:
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
npm install
|
|
114
|
-
npm run build
|
|
115
|
-
```
|
|
100
|
+
创建完成后,直接编辑 `config.js`、`content/index.md`、`content/domains.md` 和 `content/blog/` 即可定制网站。
|
|
116
101
|
|
|
117
102
|
## Markdown
|
|
118
103
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x-withu/page-withu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "A lightweight, elegant personal homepage generator.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
"chalk": "^5.3.0",
|
|
38
38
|
"commander": "^12.1.0",
|
|
39
39
|
"fs-extra": "^11.2.0",
|
|
40
|
-
"inquirer": "^9.3.2",
|
|
41
40
|
"ora": "^8.0.1"
|
|
42
41
|
}
|
|
43
42
|
}
|
|
@@ -24,10 +24,19 @@
|
|
|
24
24
|
</nav>
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
|
-
<
|
|
28
|
-
|
|
27
|
+
<button v-if="toc.length" class="blog-toc-toggle" type="button" :aria-expanded="tocOpen"
|
|
28
|
+
aria-controls="blog-toc" @click="tocOpen = true">
|
|
29
|
+
目录
|
|
30
|
+
</button>
|
|
31
|
+
<div v-if="toc.length && tocOpen" class="blog-toc-backdrop" @click="tocOpen = false"></div>
|
|
32
|
+
<aside v-if="toc.length" id="blog-toc" :class="['blog-toc', { open: tocOpen }]" aria-label="Table of contents">
|
|
33
|
+
<div class="blog-toc-header">
|
|
34
|
+
<p>目录</p>
|
|
35
|
+
<button class="blog-toc-close" type="button" aria-label="Close table of contents"
|
|
36
|
+
@click="tocOpen = false">×</button>
|
|
37
|
+
</div>
|
|
29
38
|
<a v-for="item in toc" :key="item.id" :href="`#${item.id}`"
|
|
30
|
-
:class="[`toc-level-${item.level}`, { active: activeHeading === item.id }]">
|
|
39
|
+
:class="[`toc-level-${item.level}`, { active: activeHeading === item.id }]" @click="tocOpen = false">
|
|
31
40
|
{{ item.title }}
|
|
32
41
|
</a>
|
|
33
42
|
</aside>
|
|
@@ -58,6 +67,7 @@ defineEmits(['back', 'open-post'])
|
|
|
58
67
|
const contentRef = ref(null)
|
|
59
68
|
const zoomImage = ref(null)
|
|
60
69
|
const activeHeading = ref('')
|
|
70
|
+
const tocOpen = ref(false)
|
|
61
71
|
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '')
|
|
62
72
|
const blogHref = `${basePath}/blog.html` || '/blog.html'
|
|
63
73
|
let mermaidModule
|
|
@@ -118,6 +128,7 @@ onMounted(() => {
|
|
|
118
128
|
onUnmounted(() => window.removeEventListener('scroll', updateActiveHeading))
|
|
119
129
|
|
|
120
130
|
watch(() => props.post.slug, () => {
|
|
131
|
+
tocOpen.value = false
|
|
121
132
|
activeHeading.value = props.toc[0]?.id || ''
|
|
122
133
|
nextTick(() => {
|
|
123
134
|
updateActiveHeading()
|
|
@@ -992,6 +992,14 @@ footer p {
|
|
|
992
992
|
margin-bottom: 8px;
|
|
993
993
|
}
|
|
994
994
|
|
|
995
|
+
.blog-toc-toggle {
|
|
996
|
+
display: none;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
.blog-toc-backdrop {
|
|
1000
|
+
display: none;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
995
1003
|
.blog-toc {
|
|
996
1004
|
position: sticky;
|
|
997
1005
|
top: 24px;
|
|
@@ -1005,6 +1013,17 @@ footer p {
|
|
|
1005
1013
|
font-size: 0.84rem;
|
|
1006
1014
|
}
|
|
1007
1015
|
|
|
1016
|
+
.blog-toc-header {
|
|
1017
|
+
display: flex;
|
|
1018
|
+
align-items: center;
|
|
1019
|
+
justify-content: space-between;
|
|
1020
|
+
gap: 12px;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
.blog-toc-close {
|
|
1024
|
+
display: none;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1008
1027
|
.blog-toc p {
|
|
1009
1028
|
margin: 0 0 4px;
|
|
1010
1029
|
color: var(--color-heading);
|
|
@@ -1197,7 +1216,7 @@ footer p {
|
|
|
1197
1216
|
}
|
|
1198
1217
|
|
|
1199
1218
|
/* ===== Responsive: Mobile ===== */
|
|
1200
|
-
@media (max-width:
|
|
1219
|
+
@media (max-width: 960px) {
|
|
1201
1220
|
body {
|
|
1202
1221
|
padding: 0 16px;
|
|
1203
1222
|
}
|
|
@@ -1258,10 +1277,80 @@ footer p {
|
|
|
1258
1277
|
padding-bottom: 16px;
|
|
1259
1278
|
}
|
|
1260
1279
|
|
|
1280
|
+
.blog-article-layout {
|
|
1281
|
+
grid-template-columns: 1fr;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.blog-toc-toggle {
|
|
1285
|
+
position: fixed;
|
|
1286
|
+
right: 18px;
|
|
1287
|
+
bottom: 18px;
|
|
1288
|
+
z-index: 88;
|
|
1289
|
+
display: inline-flex;
|
|
1290
|
+
align-items: center;
|
|
1291
|
+
gap: 6px;
|
|
1292
|
+
padding: 9px 14px;
|
|
1293
|
+
border: 1px solid var(--color-card-border);
|
|
1294
|
+
border-radius: 999px;
|
|
1295
|
+
color: var(--color-heading);
|
|
1296
|
+
background: var(--color-bg);
|
|
1297
|
+
box-shadow: var(--shadow-card);
|
|
1298
|
+
font: inherit;
|
|
1299
|
+
font-size: 0.84rem;
|
|
1300
|
+
cursor: pointer;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
.blog-toc-backdrop {
|
|
1304
|
+
position: fixed;
|
|
1305
|
+
inset: 0;
|
|
1306
|
+
z-index: 89;
|
|
1307
|
+
display: block;
|
|
1308
|
+
background: rgba(15, 23, 42, 0.36);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1261
1311
|
.blog-toc {
|
|
1262
|
-
position:
|
|
1263
|
-
|
|
1264
|
-
|
|
1312
|
+
position: fixed;
|
|
1313
|
+
top: 50%;
|
|
1314
|
+
left: 50%;
|
|
1315
|
+
z-index: 90;
|
|
1316
|
+
width: min(calc(100vw - 48px), 320px);
|
|
1317
|
+
max-height: min(70vh, 480px);
|
|
1318
|
+
padding: 16px;
|
|
1319
|
+
border: 1px solid var(--color-card-border);
|
|
1320
|
+
border-radius: 16px;
|
|
1321
|
+
background: var(--color-card-bg);
|
|
1322
|
+
box-shadow: var(--shadow-card);
|
|
1323
|
+
transform: translate(-50%, -50%);
|
|
1324
|
+
display: none;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
.blog-toc.open {
|
|
1328
|
+
display: flex;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
.blog-toc-header {
|
|
1332
|
+
position: sticky;
|
|
1333
|
+
top: -16px;
|
|
1334
|
+
margin: -16px -16px 4px;
|
|
1335
|
+
padding: 16px;
|
|
1336
|
+
background: var(--color-card-bg);
|
|
1337
|
+
border-bottom: 1px solid var(--color-footer-border);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
.blog-toc-close {
|
|
1341
|
+
display: inline-flex;
|
|
1342
|
+
align-items: center;
|
|
1343
|
+
justify-content: center;
|
|
1344
|
+
width: 28px;
|
|
1345
|
+
height: 28px;
|
|
1346
|
+
border: 1px solid var(--color-card-border);
|
|
1347
|
+
border-radius: 999px;
|
|
1348
|
+
color: var(--color-subtext);
|
|
1349
|
+
background: var(--color-bg);
|
|
1350
|
+
font: inherit;
|
|
1351
|
+
font-size: 1rem;
|
|
1352
|
+
line-height: 1;
|
|
1353
|
+
cursor: pointer;
|
|
1265
1354
|
}
|
|
1266
1355
|
|
|
1267
1356
|
.blog-nav {
|