无参数调用时脚本会打印用法并 exit 1(语义是对的:缺必填参数), 但 pnpm 会再补一句 "[ELIFECYCLE] Command failed with exit code 1", 看起来像脚本崩了而不是用法提示。 改成在用法前面先输出一行: 缺少正文内容,没有创建任何文件。 正确写法:pnpm new-thought "今天把树莓派的散热换掉了。" exit 码不变(无参数 1、--help 0),只是把原因说清楚。
143 lines
4.1 KiB
JavaScript
143 lines
4.1 KiB
JavaScript
/* 创建一篇新文章
|
|
*
|
|
* 用法:
|
|
* pnpm new-post "文章标题"
|
|
* pnpm new-post "树莓派装 Docker" --category 教程 --tags Docker,树莓派
|
|
* pnpm new-post "读《XX》有感" --category 思考 --description "摘要" --pinned
|
|
* pnpm new-post "标题" --file my-custom-slug --category 建站
|
|
*
|
|
* 不加 --category 时文件放在 src/content/posts/ 根目录;
|
|
* 加了则同时写进 frontmatter 的 category 并放进同名子目录,和现有文章结构一致。
|
|
*/
|
|
|
|
import fs from "node:fs"
|
|
import path from "node:path"
|
|
|
|
const POSTS_DIR = "./src/content/posts"
|
|
|
|
function today() {
|
|
const d = new Date()
|
|
const p = (n) => String(n).padStart(2, "0")
|
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
|
|
}
|
|
|
|
const args = process.argv.slice(2)
|
|
|
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
// 无参数进来时先说清楚原因:下面 pnpm 还会补一句
|
|
// "[ELIFECYCLE] Command failed with exit code 1",不点明的话很像脚本崩了
|
|
if (args.length === 0) {
|
|
console.log('缺少文章标题,没有创建任何文件。\n正确写法:pnpm new-post "树莓派装 Docker 全程记录" --category 教程\n')
|
|
}
|
|
console.log(`创建一篇新文章
|
|
|
|
用法:
|
|
pnpm new-post "文章标题" [选项]
|
|
|
|
选项:
|
|
--category <分类> 分类名,同时决定存放的子目录
|
|
--file <文件名> 自定义文件名(默认用标题)
|
|
--tags <标签,标签> 标签,逗号分隔
|
|
--description <摘要> 摘要,用于 SEO 和列表卡片
|
|
--image <路径> 封面图,public 下的绝对路径
|
|
--pinned 置顶
|
|
--draft 存为草稿(构建时不会输出)
|
|
|
|
示例:
|
|
pnpm new-post "树莓派装 Docker" --category 教程 --tags Docker,树莓派`)
|
|
process.exit(args.length === 0 ? 1 : 0)
|
|
}
|
|
|
|
const title = args[0]
|
|
|
|
const opts = {
|
|
category: "",
|
|
file: "",
|
|
tags: "",
|
|
description: "",
|
|
image: "",
|
|
pinned: false,
|
|
draft: false,
|
|
}
|
|
|
|
for (let i = 1; i < args.length; i++) {
|
|
const arg = args[i]
|
|
|
|
if (arg === "--pinned") {
|
|
opts.pinned = true
|
|
continue
|
|
}
|
|
if (arg === "--draft") {
|
|
opts.draft = true
|
|
continue
|
|
}
|
|
if (!arg.startsWith("--")) {
|
|
console.error(`错误:无法识别的参数 "${arg}"`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const key = arg.slice(2)
|
|
if (!(key in opts)) {
|
|
console.error(`错误:未知选项 --${key}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const value = args[i + 1]
|
|
if (value === undefined || value.startsWith("--")) {
|
|
console.error(`错误:--${key} 缺少值`)
|
|
process.exit(1)
|
|
}
|
|
opts[key] = value
|
|
i++
|
|
}
|
|
|
|
/** 文件名里不能出现 / : 等字符,统一替换掉 */
|
|
const safeName = (s) =>
|
|
s
|
|
.replace(/[/\\:*?"<>|]/g, "")
|
|
.replace(/\s+/g, "-")
|
|
.trim()
|
|
|
|
const fileName = `${safeName(opts.file || title)}.md`
|
|
const dir = opts.category ? path.join(POSTS_DIR, safeName(opts.category)) : POSTS_DIR
|
|
const fullPath = path.join(dir, fileName)
|
|
|
|
if (fs.existsSync(fullPath)) {
|
|
console.error(`错误:${fullPath} 已存在`)
|
|
process.exit(1)
|
|
}
|
|
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
|
|
const list = (v) =>
|
|
v
|
|
? v
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
: []
|
|
|
|
const tags = list(opts.tags)
|
|
|
|
/** 双引号包裹并转义,避免标题里的冒号、引号破坏 YAML */
|
|
const q = (v) => `"${String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
|
|
|
|
const frontmatter = [
|
|
`title: ${q(title)}`,
|
|
`published: ${today()}`, // 只写日期,YAML 按 UTC 零点解析,不会有时区偏移
|
|
opts.description ? `description: ${q(opts.description)}` : "",
|
|
opts.image ? `image: ${q(opts.image)}` : "",
|
|
tags.length > 0 ? `tags:\n${tags.map((t) => ` - ${q(t)}`).join("\n")}` : "tags: []",
|
|
`category: ${q(opts.category)}`,
|
|
opts.pinned ? "pinned: true" : "",
|
|
opts.draft ? "draft: true" : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n")
|
|
|
|
fs.writeFileSync(fullPath, `---\n${frontmatter}\n---\n\n在这里写正文。\n`, "utf8")
|
|
|
|
console.log(`已创建文章: ${fullPath}`)
|
|
if (opts.draft) console.log("状态:草稿(draft: true),构建时不会输出。写完记得删掉这一行。")
|
|
console.log("运行 pnpm dev 可以在浏览器里实时预览。")
|