create-linkdesk-plugin 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +266 -266
- package/package.json +1 -1
- package/template/package.json +26 -26
- package/template/plugin.json +41 -41
- package/template/scripts/ci-verify.mjs +63 -4
package/index.js
CHANGED
|
@@ -1,266 +1,266 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* create-linkdesk-plugin——LinkDesk 插件脚手架 CLI(纯 Node,零依赖,对标 yo code)。
|
|
4
|
-
*
|
|
5
|
-
* 用法:
|
|
6
|
-
* npm create linkdesk-plugin my-cool-plugin # 直接给名字(kebab-case,非交互)
|
|
7
|
-
* npm create linkdesk-plugin # 不带参数 → 交互式询问插件名
|
|
8
|
-
* npm create linkdesk-plugin my-cool-plugin --no-git # 跳过建仓
|
|
9
|
-
*
|
|
10
|
-
* 行为:把同目录 template/ 复制到 <cwd>/<name>,占位符替换成真实值,**按 `cargo new` 的语义决定
|
|
11
|
-
* 建不建 git 仓**,再打印下一步提示。
|
|
12
|
-
* 占位符:{{pluginName}} {{displayName}} {{author}} {{date}}(递归替换所有模板文件)。
|
|
13
|
-
* {{date}} 注入 CHANGELOG.md 的初始段标题——格式必须是 `## v<版本>(YYYY-MM-DD)`,
|
|
14
|
-
* 那是市场「更改日志」页签切段的解析依据(见 docs/02-Electron架构/.../插件规范化层/02)。
|
|
15
|
-
*
|
|
16
|
-
* 🔴 建仓三语义(E6#103 · L7 第 7.6 轮)——照抄 `cargo new`,**不是「一律 git init」**:
|
|
17
|
-
* ① 目标目录**已在某个 git 仓内** ⇒ 不 init(防嵌套仓——在容器目录里生成插件正是这种情形)
|
|
18
|
-
* ② 不在任何 git 仓内 ⇒ `git init -b main`
|
|
19
|
-
* ③ `--no-git` ⇒ 不建仓(逃生口,对标 `cargo new --vcs none`)
|
|
20
|
-
* 比 cargo 多一步:建完仓**顺手做一次初始提交**——模板自带 `.gitignore`,作者第一步看到的
|
|
21
|
-
* 就不是满屏 untracked,`git log` 也立刻有一笔可回退的基线。不想要仓的人用 `--no-git`。
|
|
22
|
-
*
|
|
23
|
-
* 生成产物契约:见 docs/02-Electron架构/E6_插件生态与发布/02-插件开发工具链/01-create-linkdesk-plugin脚手架.md。
|
|
24
|
-
*/
|
|
25
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
26
|
-
import { spawnSync } from "node:child_process";
|
|
27
|
-
import { createInterface } from "node:readline";
|
|
28
|
-
import { dirname, join } from "node:path";
|
|
29
|
-
import { fileURLToPath } from "node:url";
|
|
30
|
-
|
|
31
|
-
const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), "template");
|
|
32
|
-
|
|
33
|
-
/** kebab-case——同时满足插件 id / viewsContainers key / npm 包名惯例(SAFE_PLUGIN_ID 的形状子集) */
|
|
34
|
-
const NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
35
|
-
|
|
36
|
-
/** 认识的旗标——**显式白名单**:`--nogit` 这种拼错不能静默略过(作者会以为仓建好了) */
|
|
37
|
-
const KNOWN_FLAGS = ["--no-git", "--help", "-h"];
|
|
38
|
-
|
|
39
|
-
const USAGE = [
|
|
40
|
-
"用法:",
|
|
41
|
-
" npm create linkdesk-plugin <name> 生成 <name>/ 插件工程(kebab-case)",
|
|
42
|
-
" npm create linkdesk-plugin 交互式询问插件名",
|
|
43
|
-
"选项:",
|
|
44
|
-
" --no-git 不建 git 仓(对标 cargo new --vcs none)",
|
|
45
|
-
" --help, -h 显示本说明",
|
|
46
|
-
].join("\n");
|
|
47
|
-
|
|
48
|
-
/** 本地日期 YYYY-MM-DD(不用 toISOString——那是 UTC,跨时区会差一天) */
|
|
49
|
-
function todayLocal() {
|
|
50
|
-
const d = new Date();
|
|
51
|
-
const p = (n) => String(n).padStart(2, "0");
|
|
52
|
-
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** my-cool-plugin → My Cool Plugin */
|
|
56
|
-
function toDisplayName(name) {
|
|
57
|
-
return name
|
|
58
|
-
.split("-")
|
|
59
|
-
.map((s) => (s ? s[0].toUpperCase() + s.slice(1) : s))
|
|
60
|
-
.join(" ");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** 作者默认值 = git config user.name;读不到(无 git/无配置)→ "you"(作者生成后自改) */
|
|
64
|
-
function gitUserName() {
|
|
65
|
-
try {
|
|
66
|
-
const r = spawnSync("git", ["config", "user.name"], { encoding: "utf8", timeout: 3000 });
|
|
67
|
-
const v = (r.stdout || "").trim();
|
|
68
|
-
return v || "you";
|
|
69
|
-
} catch {
|
|
70
|
-
return "you";
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ─────────────────────────── git(§〇 建仓三语义) ───────────────────────────
|
|
75
|
-
|
|
76
|
-
/** 跑一条 git 命令——不抛,失败由调用方看 status(没装 git 时 status=null 且 error 有值) */
|
|
77
|
-
function git(args, cwd) {
|
|
78
|
-
try {
|
|
79
|
-
return spawnSync("git", args, { cwd, encoding: "utf8", timeout: 15000 });
|
|
80
|
-
} catch (err) {
|
|
81
|
-
return { status: null, stdout: "", stderr: String(err?.message ?? err) };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** git 在不在 PATH 上——不在就不建仓(骨架照常给,不拿「没装 git」卡住作者) */
|
|
86
|
-
function gitAvailable() {
|
|
87
|
-
return git(["--version"]).status === 0;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* 目标目录是否**已在某个 git 仓内**——`cargo new` 的「不造嵌套仓」判据。
|
|
92
|
-
*
|
|
93
|
-
* 在**目标目录里**问(不是 cwd):目录此时已建出来,向上找仓根正是 `cargo new` 的做法。
|
|
94
|
-
* `rev-parse --show-toplevel` 在仓外会 exit 128 ⇒ 那就是「不在仓内」。
|
|
95
|
-
*/
|
|
96
|
-
function insideGitRepo(dir) {
|
|
97
|
-
const r = git(["rev-parse", "--show-toplevel"], dir);
|
|
98
|
-
return r.status === 0 ? (r.stdout || "").trim() : "";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** stderr 摘要(给作者看的失败原因,最多三行) */
|
|
102
|
-
function why(r) {
|
|
103
|
-
return ((r.stderr || r.stdout || "").trim().split("\n").filter(Boolean).slice(0, 3).join(" / ")) || "(无输出)";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 建仓——返回结果对象给 CLI 输出(**作者必须知道到底建没建、为什么**,不许自己猜)。
|
|
108
|
-
*
|
|
109
|
-
* 🔴 任何一步失败都**不终止脚手架**:骨架已经生成好了,建仓是加分项,不是前置条件。
|
|
110
|
-
*/
|
|
111
|
-
function setupGit(target, { noGit }) {
|
|
112
|
-
if (noGit) return { kind: "skipped-flag" };
|
|
113
|
-
if (!gitAvailable()) return { kind: "no-git-binary" };
|
|
114
|
-
|
|
115
|
-
const top = insideGitRepo(target);
|
|
116
|
-
if (top) return { kind: "skipped-inside-repo", top };
|
|
117
|
-
|
|
118
|
-
const init = git(["init", "-b", "main"], target);
|
|
119
|
-
if (init.status !== 0) {
|
|
120
|
-
// `-b` 是 git 2.28+ 才有的旗标;更老的 git 退回 init + 显式把 HEAD 指到 main
|
|
121
|
-
// (不这么做,作者第一次 push 会撞上默认分支叫 master 的提示,与 GitHub 默认也不一致)
|
|
122
|
-
if (git(["init"], target).status !== 0) return { kind: "failed", step: "git init", detail: why(init) };
|
|
123
|
-
git(["symbolic-ref", "HEAD", "refs/heads/main"], target);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (git(["add", "-A"], target).status !== 0) return { kind: "init-only", step: "git add" };
|
|
127
|
-
|
|
128
|
-
// `--no-verify`:这是**一个新仓的初始提交**,不该被使用者全局 core.hooksPath 上的
|
|
129
|
-
// commit-msg / pre-commit 钩子审(那些钩子是给别的仓立的规矩)
|
|
130
|
-
const commit = git(["commit", "--no-verify", "-m", "chore: 初始骨架(create-linkdesk-plugin 生成)"], target);
|
|
131
|
-
if (commit.status !== 0) return { kind: "init-only", step: "git commit", detail: why(commit) };
|
|
132
|
-
|
|
133
|
-
return { kind: "created" };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** 建仓结果 → CLI 那一行(`git` 那步发生了什么,说全) */
|
|
137
|
-
function gitLine(res) {
|
|
138
|
-
switch (res.kind) {
|
|
139
|
-
case "created":
|
|
140
|
-
return " ✔ 已建 git 仓(main 分支 + 一次初始提交)";
|
|
141
|
-
case "skipped-flag":
|
|
142
|
-
return " · --no-git:未建 git 仓(发布前需要自己 git init)";
|
|
143
|
-
case "skipped-inside-repo":
|
|
144
|
-
return ` · 已在 git 仓内(${res.top})——按 cargo new 语义不建嵌套仓`;
|
|
145
|
-
case "no-git-binary":
|
|
146
|
-
return " ⚠️ 找不到 git(不在 PATH)——未建仓;装上 git 后进目录自己 git init -b main";
|
|
147
|
-
case "init-only":
|
|
148
|
-
return ` ⚠️ 仓已建,但初始提交没成(${res.step}):${res.detail}\n 多半是没配 git 身份 → git config --global user.name "你" && git config --global user.email "you@example.com",再进目录 git commit -m "初始骨架"`;
|
|
149
|
-
default:
|
|
150
|
-
return ` ⚠️ 建仓失败(${res.step}):${res.detail}——进目录自己 git init -b main`;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** 交互式单问——返回去除首尾空白的答案 */
|
|
155
|
-
function ask(question) {
|
|
156
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
157
|
-
return new Promise((resolve) => {
|
|
158
|
-
rl.question(question, (answer) => {
|
|
159
|
-
rl.close();
|
|
160
|
-
resolve(answer.trim());
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function replacePlaceholders(file, values) {
|
|
166
|
-
let text = readFileSync(file, "utf8");
|
|
167
|
-
for (const [key, value] of Object.entries(values)) {
|
|
168
|
-
text = text.split(`{{${key}}}`).join(value);
|
|
169
|
-
}
|
|
170
|
-
writeFileSync(file, text);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** 递归替换目录内全部文件(模板全是文本文件,无需跳过二进制) */
|
|
174
|
-
function walkReplace(dir, values) {
|
|
175
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
176
|
-
const full = join(dir, entry.name);
|
|
177
|
-
if (entry.isDirectory()) walkReplace(full, values);
|
|
178
|
-
else replacePlaceholders(full, values);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function main() {
|
|
183
|
-
const argv = process.argv.slice(2);
|
|
184
|
-
const flags = argv.filter((a) => a.startsWith("-"));
|
|
185
|
-
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
186
|
-
|
|
187
|
-
if (flags.includes("--help") || flags.includes("-h")) {
|
|
188
|
-
console.log(USAGE);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const unknown = flags.filter((f) => !KNOWN_FLAGS.includes(f));
|
|
192
|
-
if (unknown.length > 0) {
|
|
193
|
-
console.error(`✖ 不认识的选项:${unknown.join("、")}\n\n${USAGE}`);
|
|
194
|
-
process.exit(1);
|
|
195
|
-
}
|
|
196
|
-
if (positional.length > 1) {
|
|
197
|
-
console.error(`✖ 只接受一个插件名,收到 ${positional.length} 个:${positional.join("、")}\n\n${USAGE}`);
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
const noGit = flags.includes("--no-git");
|
|
201
|
-
|
|
202
|
-
let name = (positional[0] || "").trim();
|
|
203
|
-
if (!name) {
|
|
204
|
-
name = await ask("插件名(kebab-case,如 my-cool-plugin): ");
|
|
205
|
-
}
|
|
206
|
-
name = name.trim();
|
|
207
|
-
if (!NAME_RE.test(name)) {
|
|
208
|
-
console.error(`✖ 插件名须为 kebab-case(小写字母/数字/连字符),收到:${JSON.stringify(name)}`);
|
|
209
|
-
process.exit(1);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const target = join(process.cwd(), name);
|
|
213
|
-
if (existsSync(target) && readdirSync(target).length > 0) {
|
|
214
|
-
console.error(`✖ ${name}/ 已存在且非空——换个名字,或清空后重跑`);
|
|
215
|
-
process.exit(1);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
mkdirSync(target, { recursive: true });
|
|
219
|
-
cpSync(TEMPLATE_DIR, target, { recursive: true });
|
|
220
|
-
|
|
221
|
-
// 🔥 模板里存的是 `gitignore`(无点),生成时才改名为 `.gitignore`。
|
|
222
|
-
// 原因:**npm 打包恒定丢弃名为 `.gitignore` 的文件**(npm-packlist 排除表;实测
|
|
223
|
-
// `template/.gitignoreprobe` 与 `template/probe.txt` 都能进 tarball,唯独 `.gitignore` 不能)。
|
|
224
|
-
// 若模板里直接放 `.gitignore`,仓内生成(读模板目录)一切正常,**但发布后的
|
|
225
|
-
// `npm create linkdesk-plugin` 生成的工程会没有 .gitignore**——作者第一次 `git add .`
|
|
226
|
-
// 就把 node_modules/ 和 dist/ 全提交了。生成物契约见 check-scaffold.mjs 断言 8。
|
|
227
|
-
const tplGitignore = join(target, "gitignore");
|
|
228
|
-
if (existsSync(tplGitignore)) renameSync(tplGitignore, join(target, ".gitignore"));
|
|
229
|
-
|
|
230
|
-
const values = {
|
|
231
|
-
pluginName: name,
|
|
232
|
-
displayName: toDisplayName(name),
|
|
233
|
-
author: gitUserName(),
|
|
234
|
-
date: todayLocal(),
|
|
235
|
-
};
|
|
236
|
-
walkReplace(target, values);
|
|
237
|
-
|
|
238
|
-
// 建仓放在**最后**——此刻工作区已是终态,初始提交提交的就是作者拿到的那个骨架
|
|
239
|
-
const repo = setupGit(target, { noGit });
|
|
240
|
-
|
|
241
|
-
console.log("");
|
|
242
|
-
console.log(`✔ ${name}/ 已创建`);
|
|
243
|
-
console.log(gitLine(repo));
|
|
244
|
-
console.log("");
|
|
245
|
-
console.log(" 接下来:");
|
|
246
|
-
console.log(` cd ${name}`);
|
|
247
|
-
console.log(" npm install");
|
|
248
|
-
console.log(" npm run dev # 浏览器热重载预览(改代码即时生效)");
|
|
249
|
-
console.log(" npm run validate # 校验 plugin.json($schema / 字段 / i18n 文件)");
|
|
250
|
-
console.log(" npm run build # 打包出 <pluginId>.linkdesk-plugin,可装进 LinkDesk / 发布");
|
|
251
|
-
if (repo.kind === "created") {
|
|
252
|
-
console.log("");
|
|
253
|
-
console.log(" 要发布(npm run publish)时还需要一个 GitHub 远端:");
|
|
254
|
-
console.log(" git remote add origin git@github.com:<你>/<仓库>.git");
|
|
255
|
-
console.log(" git push -u origin main");
|
|
256
|
-
}
|
|
257
|
-
console.log("");
|
|
258
|
-
console.log(" 然后:先读 README.md —— 目录该放哪、三条纪律、怎么发布都在里面。");
|
|
259
|
-
console.log(" plugin.json 的 name / description / author 是你的身份信息,src/index.tsx 是插件本体。");
|
|
260
|
-
console.log(" 完整插件能力(侧栏视图 / 命令 / 设置 / 协议……)见 docs/03-插件制造/。");
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
main().catch((err) => {
|
|
264
|
-
console.error(err);
|
|
265
|
-
process.exit(1);
|
|
266
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* create-linkdesk-plugin——LinkDesk 插件脚手架 CLI(纯 Node,零依赖,对标 yo code)。
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* npm create linkdesk-plugin my-cool-plugin # 直接给名字(kebab-case,非交互)
|
|
7
|
+
* npm create linkdesk-plugin # 不带参数 → 交互式询问插件名
|
|
8
|
+
* npm create linkdesk-plugin my-cool-plugin --no-git # 跳过建仓
|
|
9
|
+
*
|
|
10
|
+
* 行为:把同目录 template/ 复制到 <cwd>/<name>,占位符替换成真实值,**按 `cargo new` 的语义决定
|
|
11
|
+
* 建不建 git 仓**,再打印下一步提示。
|
|
12
|
+
* 占位符:{{pluginName}} {{displayName}} {{author}} {{date}}(递归替换所有模板文件)。
|
|
13
|
+
* {{date}} 注入 CHANGELOG.md 的初始段标题——格式必须是 `## v<版本>(YYYY-MM-DD)`,
|
|
14
|
+
* 那是市场「更改日志」页签切段的解析依据(见 docs/02-Electron架构/.../插件规范化层/02)。
|
|
15
|
+
*
|
|
16
|
+
* 🔴 建仓三语义(E6#103 · L7 第 7.6 轮)——照抄 `cargo new`,**不是「一律 git init」**:
|
|
17
|
+
* ① 目标目录**已在某个 git 仓内** ⇒ 不 init(防嵌套仓——在容器目录里生成插件正是这种情形)
|
|
18
|
+
* ② 不在任何 git 仓内 ⇒ `git init -b main`
|
|
19
|
+
* ③ `--no-git` ⇒ 不建仓(逃生口,对标 `cargo new --vcs none`)
|
|
20
|
+
* 比 cargo 多一步:建完仓**顺手做一次初始提交**——模板自带 `.gitignore`,作者第一步看到的
|
|
21
|
+
* 就不是满屏 untracked,`git log` 也立刻有一笔可回退的基线。不想要仓的人用 `--no-git`。
|
|
22
|
+
*
|
|
23
|
+
* 生成产物契约:见 docs/02-Electron架构/E6_插件生态与发布/02-插件开发工具链/01-create-linkdesk-plugin脚手架.md。
|
|
24
|
+
*/
|
|
25
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { spawnSync } from "node:child_process";
|
|
27
|
+
import { createInterface } from "node:readline";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
|
|
31
|
+
const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), "template");
|
|
32
|
+
|
|
33
|
+
/** kebab-case——同时满足插件 id / viewsContainers key / npm 包名惯例(SAFE_PLUGIN_ID 的形状子集) */
|
|
34
|
+
const NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
35
|
+
|
|
36
|
+
/** 认识的旗标——**显式白名单**:`--nogit` 这种拼错不能静默略过(作者会以为仓建好了) */
|
|
37
|
+
const KNOWN_FLAGS = ["--no-git", "--help", "-h"];
|
|
38
|
+
|
|
39
|
+
const USAGE = [
|
|
40
|
+
"用法:",
|
|
41
|
+
" npm create linkdesk-plugin <name> 生成 <name>/ 插件工程(kebab-case)",
|
|
42
|
+
" npm create linkdesk-plugin 交互式询问插件名",
|
|
43
|
+
"选项:",
|
|
44
|
+
" --no-git 不建 git 仓(对标 cargo new --vcs none)",
|
|
45
|
+
" --help, -h 显示本说明",
|
|
46
|
+
].join("\n");
|
|
47
|
+
|
|
48
|
+
/** 本地日期 YYYY-MM-DD(不用 toISOString——那是 UTC,跨时区会差一天) */
|
|
49
|
+
function todayLocal() {
|
|
50
|
+
const d = new Date();
|
|
51
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
52
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** my-cool-plugin → My Cool Plugin */
|
|
56
|
+
function toDisplayName(name) {
|
|
57
|
+
return name
|
|
58
|
+
.split("-")
|
|
59
|
+
.map((s) => (s ? s[0].toUpperCase() + s.slice(1) : s))
|
|
60
|
+
.join(" ");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 作者默认值 = git config user.name;读不到(无 git/无配置)→ "you"(作者生成后自改) */
|
|
64
|
+
function gitUserName() {
|
|
65
|
+
try {
|
|
66
|
+
const r = spawnSync("git", ["config", "user.name"], { encoding: "utf8", timeout: 3000 });
|
|
67
|
+
const v = (r.stdout || "").trim();
|
|
68
|
+
return v || "you";
|
|
69
|
+
} catch {
|
|
70
|
+
return "you";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─────────────────────────── git(§〇 建仓三语义) ───────────────────────────
|
|
75
|
+
|
|
76
|
+
/** 跑一条 git 命令——不抛,失败由调用方看 status(没装 git 时 status=null 且 error 有值) */
|
|
77
|
+
function git(args, cwd) {
|
|
78
|
+
try {
|
|
79
|
+
return spawnSync("git", args, { cwd, encoding: "utf8", timeout: 15000 });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return { status: null, stdout: "", stderr: String(err?.message ?? err) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** git 在不在 PATH 上——不在就不建仓(骨架照常给,不拿「没装 git」卡住作者) */
|
|
86
|
+
function gitAvailable() {
|
|
87
|
+
return git(["--version"]).status === 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 目标目录是否**已在某个 git 仓内**——`cargo new` 的「不造嵌套仓」判据。
|
|
92
|
+
*
|
|
93
|
+
* 在**目标目录里**问(不是 cwd):目录此时已建出来,向上找仓根正是 `cargo new` 的做法。
|
|
94
|
+
* `rev-parse --show-toplevel` 在仓外会 exit 128 ⇒ 那就是「不在仓内」。
|
|
95
|
+
*/
|
|
96
|
+
function insideGitRepo(dir) {
|
|
97
|
+
const r = git(["rev-parse", "--show-toplevel"], dir);
|
|
98
|
+
return r.status === 0 ? (r.stdout || "").trim() : "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** stderr 摘要(给作者看的失败原因,最多三行) */
|
|
102
|
+
function why(r) {
|
|
103
|
+
return ((r.stderr || r.stdout || "").trim().split("\n").filter(Boolean).slice(0, 3).join(" / ")) || "(无输出)";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 建仓——返回结果对象给 CLI 输出(**作者必须知道到底建没建、为什么**,不许自己猜)。
|
|
108
|
+
*
|
|
109
|
+
* 🔴 任何一步失败都**不终止脚手架**:骨架已经生成好了,建仓是加分项,不是前置条件。
|
|
110
|
+
*/
|
|
111
|
+
function setupGit(target, { noGit }) {
|
|
112
|
+
if (noGit) return { kind: "skipped-flag" };
|
|
113
|
+
if (!gitAvailable()) return { kind: "no-git-binary" };
|
|
114
|
+
|
|
115
|
+
const top = insideGitRepo(target);
|
|
116
|
+
if (top) return { kind: "skipped-inside-repo", top };
|
|
117
|
+
|
|
118
|
+
const init = git(["init", "-b", "main"], target);
|
|
119
|
+
if (init.status !== 0) {
|
|
120
|
+
// `-b` 是 git 2.28+ 才有的旗标;更老的 git 退回 init + 显式把 HEAD 指到 main
|
|
121
|
+
// (不这么做,作者第一次 push 会撞上默认分支叫 master 的提示,与 GitHub 默认也不一致)
|
|
122
|
+
if (git(["init"], target).status !== 0) return { kind: "failed", step: "git init", detail: why(init) };
|
|
123
|
+
git(["symbolic-ref", "HEAD", "refs/heads/main"], target);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (git(["add", "-A"], target).status !== 0) return { kind: "init-only", step: "git add" };
|
|
127
|
+
|
|
128
|
+
// `--no-verify`:这是**一个新仓的初始提交**,不该被使用者全局 core.hooksPath 上的
|
|
129
|
+
// commit-msg / pre-commit 钩子审(那些钩子是给别的仓立的规矩)
|
|
130
|
+
const commit = git(["commit", "--no-verify", "-m", "chore: 初始骨架(create-linkdesk-plugin 生成)"], target);
|
|
131
|
+
if (commit.status !== 0) return { kind: "init-only", step: "git commit", detail: why(commit) };
|
|
132
|
+
|
|
133
|
+
return { kind: "created" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 建仓结果 → CLI 那一行(`git` 那步发生了什么,说全) */
|
|
137
|
+
function gitLine(res) {
|
|
138
|
+
switch (res.kind) {
|
|
139
|
+
case "created":
|
|
140
|
+
return " ✔ 已建 git 仓(main 分支 + 一次初始提交)";
|
|
141
|
+
case "skipped-flag":
|
|
142
|
+
return " · --no-git:未建 git 仓(发布前需要自己 git init)";
|
|
143
|
+
case "skipped-inside-repo":
|
|
144
|
+
return ` · 已在 git 仓内(${res.top})——按 cargo new 语义不建嵌套仓`;
|
|
145
|
+
case "no-git-binary":
|
|
146
|
+
return " ⚠️ 找不到 git(不在 PATH)——未建仓;装上 git 后进目录自己 git init -b main";
|
|
147
|
+
case "init-only":
|
|
148
|
+
return ` ⚠️ 仓已建,但初始提交没成(${res.step}):${res.detail}\n 多半是没配 git 身份 → git config --global user.name "你" && git config --global user.email "you@example.com",再进目录 git commit -m "初始骨架"`;
|
|
149
|
+
default:
|
|
150
|
+
return ` ⚠️ 建仓失败(${res.step}):${res.detail}——进目录自己 git init -b main`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 交互式单问——返回去除首尾空白的答案 */
|
|
155
|
+
function ask(question) {
|
|
156
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
rl.question(question, (answer) => {
|
|
159
|
+
rl.close();
|
|
160
|
+
resolve(answer.trim());
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function replacePlaceholders(file, values) {
|
|
166
|
+
let text = readFileSync(file, "utf8");
|
|
167
|
+
for (const [key, value] of Object.entries(values)) {
|
|
168
|
+
text = text.split(`{{${key}}}`).join(value);
|
|
169
|
+
}
|
|
170
|
+
writeFileSync(file, text);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 递归替换目录内全部文件(模板全是文本文件,无需跳过二进制) */
|
|
174
|
+
function walkReplace(dir, values) {
|
|
175
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
176
|
+
const full = join(dir, entry.name);
|
|
177
|
+
if (entry.isDirectory()) walkReplace(full, values);
|
|
178
|
+
else replacePlaceholders(full, values);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function main() {
|
|
183
|
+
const argv = process.argv.slice(2);
|
|
184
|
+
const flags = argv.filter((a) => a.startsWith("-"));
|
|
185
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
186
|
+
|
|
187
|
+
if (flags.includes("--help") || flags.includes("-h")) {
|
|
188
|
+
console.log(USAGE);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const unknown = flags.filter((f) => !KNOWN_FLAGS.includes(f));
|
|
192
|
+
if (unknown.length > 0) {
|
|
193
|
+
console.error(`✖ 不认识的选项:${unknown.join("、")}\n\n${USAGE}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
if (positional.length > 1) {
|
|
197
|
+
console.error(`✖ 只接受一个插件名,收到 ${positional.length} 个:${positional.join("、")}\n\n${USAGE}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
const noGit = flags.includes("--no-git");
|
|
201
|
+
|
|
202
|
+
let name = (positional[0] || "").trim();
|
|
203
|
+
if (!name) {
|
|
204
|
+
name = await ask("插件名(kebab-case,如 my-cool-plugin): ");
|
|
205
|
+
}
|
|
206
|
+
name = name.trim();
|
|
207
|
+
if (!NAME_RE.test(name)) {
|
|
208
|
+
console.error(`✖ 插件名须为 kebab-case(小写字母/数字/连字符),收到:${JSON.stringify(name)}`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const target = join(process.cwd(), name);
|
|
213
|
+
if (existsSync(target) && readdirSync(target).length > 0) {
|
|
214
|
+
console.error(`✖ ${name}/ 已存在且非空——换个名字,或清空后重跑`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
mkdirSync(target, { recursive: true });
|
|
219
|
+
cpSync(TEMPLATE_DIR, target, { recursive: true });
|
|
220
|
+
|
|
221
|
+
// 🔥 模板里存的是 `gitignore`(无点),生成时才改名为 `.gitignore`。
|
|
222
|
+
// 原因:**npm 打包恒定丢弃名为 `.gitignore` 的文件**(npm-packlist 排除表;实测
|
|
223
|
+
// `template/.gitignoreprobe` 与 `template/probe.txt` 都能进 tarball,唯独 `.gitignore` 不能)。
|
|
224
|
+
// 若模板里直接放 `.gitignore`,仓内生成(读模板目录)一切正常,**但发布后的
|
|
225
|
+
// `npm create linkdesk-plugin` 生成的工程会没有 .gitignore**——作者第一次 `git add .`
|
|
226
|
+
// 就把 node_modules/ 和 dist/ 全提交了。生成物契约见 check-scaffold.mjs 断言 8。
|
|
227
|
+
const tplGitignore = join(target, "gitignore");
|
|
228
|
+
if (existsSync(tplGitignore)) renameSync(tplGitignore, join(target, ".gitignore"));
|
|
229
|
+
|
|
230
|
+
const values = {
|
|
231
|
+
pluginName: name,
|
|
232
|
+
displayName: toDisplayName(name),
|
|
233
|
+
author: gitUserName(),
|
|
234
|
+
date: todayLocal(),
|
|
235
|
+
};
|
|
236
|
+
walkReplace(target, values);
|
|
237
|
+
|
|
238
|
+
// 建仓放在**最后**——此刻工作区已是终态,初始提交提交的就是作者拿到的那个骨架
|
|
239
|
+
const repo = setupGit(target, { noGit });
|
|
240
|
+
|
|
241
|
+
console.log("");
|
|
242
|
+
console.log(`✔ ${name}/ 已创建`);
|
|
243
|
+
console.log(gitLine(repo));
|
|
244
|
+
console.log("");
|
|
245
|
+
console.log(" 接下来:");
|
|
246
|
+
console.log(` cd ${name}`);
|
|
247
|
+
console.log(" npm install");
|
|
248
|
+
console.log(" npm run dev # 浏览器热重载预览(改代码即时生效)");
|
|
249
|
+
console.log(" npm run validate # 校验 plugin.json($schema / 字段 / i18n 文件)");
|
|
250
|
+
console.log(" npm run build # 打包出 <pluginId>.linkdesk-plugin,可装进 LinkDesk / 发布");
|
|
251
|
+
if (repo.kind === "created") {
|
|
252
|
+
console.log("");
|
|
253
|
+
console.log(" 要发布(npm run publish)时还需要一个 GitHub 远端:");
|
|
254
|
+
console.log(" git remote add origin git@github.com:<你>/<仓库>.git");
|
|
255
|
+
console.log(" git push -u origin main");
|
|
256
|
+
}
|
|
257
|
+
console.log("");
|
|
258
|
+
console.log(" 然后:先读 README.md —— 目录该放哪、三条纪律、怎么发布都在里面。");
|
|
259
|
+
console.log(" plugin.json 的 name / description / author 是你的身份信息,src/index.tsx 是插件本体。");
|
|
260
|
+
console.log(" 完整插件能力(侧栏视图 / 命令 / 设置 / 协议……)见 docs/03-插件制造/。");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
main().catch((err) => {
|
|
264
|
+
console.error(err);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
});
|
package/package.json
CHANGED
package/template/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "{{pluginName}}",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"description": "{{displayName}}——LinkDesk 插件(由 create-linkdesk-plugin 生成)",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"dev": "linkdesk-plugin-sdk dev",
|
|
9
|
-
"dev:real": "linkdesk-plugin-sdk dev --real",
|
|
10
|
-
"build": "linkdesk-plugin-sdk build",
|
|
11
|
-
"publish": "linkdesk-plugin-sdk publish",
|
|
12
|
-
"validate": "linkdesk-plugin-sdk validate",
|
|
13
|
-
"lint": "linkdesk-plugin-sdk lint",
|
|
14
|
-
"verify": "node scripts/ci-verify.mjs",
|
|
15
|
-
"test": "vitest run"
|
|
16
|
-
},
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"@linkdesk/plugin-sdk": "^0.1.0",
|
|
19
|
-
"@testing-library/react": "^16.3.2",
|
|
20
|
-
"@types/react": "^18.3.12",
|
|
21
|
-
"jsdom": "^29.1.1",
|
|
22
|
-
"jsonc-parser": "^3.3.1",
|
|
23
|
-
"typescript": "^5.6.3",
|
|
24
|
-
"vitest": "^4.1.10"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "{{pluginName}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "{{displayName}}——LinkDesk 插件(由 create-linkdesk-plugin 生成)",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "linkdesk-plugin-sdk dev",
|
|
9
|
+
"dev:real": "linkdesk-plugin-sdk dev --real",
|
|
10
|
+
"build": "linkdesk-plugin-sdk build",
|
|
11
|
+
"publish": "linkdesk-plugin-sdk publish",
|
|
12
|
+
"validate": "linkdesk-plugin-sdk validate",
|
|
13
|
+
"lint": "linkdesk-plugin-sdk lint",
|
|
14
|
+
"verify": "node scripts/ci-verify.mjs",
|
|
15
|
+
"test": "vitest run"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@linkdesk/plugin-sdk": "^0.1.0",
|
|
19
|
+
"@testing-library/react": "^16.3.2",
|
|
20
|
+
"@types/react": "^18.3.12",
|
|
21
|
+
"jsdom": "^29.1.1",
|
|
22
|
+
"jsonc-parser": "^3.3.1",
|
|
23
|
+
"typescript": "^5.6.3",
|
|
24
|
+
"vitest": "^4.1.10"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/template/plugin.json
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
3
|
-
// LinkDesk 插件清单 plugin.json(由 create-linkdesk-plugin 生成)
|
|
4
|
-
//
|
|
5
|
-
// · 本文件是 JSONC:可写注释、可尾逗号(LinkDesk 与 SDK 都按 jsonc 解析,对标 VS Code package.json)。
|
|
6
|
-
// · 完整字段以 plugin.schema.json 为准——VS Code 读 $schema 自动补全 + 校验。
|
|
7
|
-
// · $schema 指向本工程 node_modules/@linkdesk/plugin-sdk 随包拷贝(npm install 后生效)。
|
|
8
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
9
|
-
"$schema": "./node_modules/@linkdesk/plugin-sdk/schemas/plugin.schema.json",
|
|
10
|
-
|
|
11
|
-
// ── 插件身份 ──
|
|
12
|
-
// 🔴 插件 ID:**显式声明**。它是安装目录名 / 卸载墓碑 / 更新对账的唯一键,**发布后永不可变**。
|
|
13
|
-
// 它与插件所在的目录名**无关**(目录可以随便改)——所以别拿目录名当它的替身。
|
|
14
|
-
"pluginId": "{{pluginName}}",
|
|
15
|
-
"name": "{{displayName}}", // 显示名——标签页 / 插件详情等 UI 出现处
|
|
16
|
-
"version": "0.1.0", // 语义化版本 x.y.z——市场更新比较靠它;+1 时务必同笔补 CHANGELOG.md 的新段
|
|
17
|
-
"description": "{{displayName}}——我的第一个 LinkDesk 插件", // 一句话描述(插件详情页展示)
|
|
18
|
-
"author": "{{author}}", // 作者名
|
|
19
|
-
"icon": "resources/icon.svg", // 图标——图标栏 / 标签页 / 市场里显示的就是它(resources/icon.svg 是占位图,换成你的)
|
|
20
|
-
|
|
21
|
-
// ── 入口(视图插件 = 此文件 default 导出一个 React 组件)──
|
|
22
|
-
"entry": "src/index.tsx",
|
|
23
|
-
|
|
24
|
-
// ── 出现位置:可作为主区标签页打开 ──
|
|
25
|
-
"appearsIn": { "tabBar": true },
|
|
26
|
-
"tabBehavior": { "singleton": true }, // 全局只开一个实例,避免重复标签
|
|
27
|
-
|
|
28
|
-
// ── 贡献点(contributes:全部可选,按需增删)──
|
|
29
|
-
"contributes": {
|
|
30
|
-
// 自带翻译:key=语言码, value=相对插件根的 JSON 文件。UI 文案用 t() 读这里;无需 zh.json——中文 key 原文自带兜底。
|
|
31
|
-
"i18n": { "en": "i18n/en.json" },
|
|
32
|
-
// ── 需要「侧栏 / 底部面板 / 辅助侧栏」分区视图时:取消注释,在 src/views/ 放对应组件,
|
|
33
|
-
// 容器 key 与 view id 用你的 pluginId 做前缀防撞(对标 plugins/panel-demo)──
|
|
34
|
-
// "viewsContainers": { "{{pluginName}}-sidebar": { "title": "{{displayName}}", "location": "sidebar" } },
|
|
35
|
-
// "views": {
|
|
36
|
-
// "{{pluginName}}-sidebar": [
|
|
37
|
-
// { "id": "main", "title": "{{displayName}}", "render": "src/views/MainView.tsx", "order": 0 }
|
|
38
|
-
// ]
|
|
39
|
-
// }
|
|
40
|
-
},
|
|
41
|
-
}
|
|
1
|
+
{
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// LinkDesk 插件清单 plugin.json(由 create-linkdesk-plugin 生成)
|
|
4
|
+
//
|
|
5
|
+
// · 本文件是 JSONC:可写注释、可尾逗号(LinkDesk 与 SDK 都按 jsonc 解析,对标 VS Code package.json)。
|
|
6
|
+
// · 完整字段以 plugin.schema.json 为准——VS Code 读 $schema 自动补全 + 校验。
|
|
7
|
+
// · $schema 指向本工程 node_modules/@linkdesk/plugin-sdk 随包拷贝(npm install 后生效)。
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
"$schema": "./node_modules/@linkdesk/plugin-sdk/schemas/plugin.schema.json",
|
|
10
|
+
|
|
11
|
+
// ── 插件身份 ──
|
|
12
|
+
// 🔴 插件 ID:**显式声明**。它是安装目录名 / 卸载墓碑 / 更新对账的唯一键,**发布后永不可变**。
|
|
13
|
+
// 它与插件所在的目录名**无关**(目录可以随便改)——所以别拿目录名当它的替身。
|
|
14
|
+
"pluginId": "{{pluginName}}",
|
|
15
|
+
"name": "{{displayName}}", // 显示名——标签页 / 插件详情等 UI 出现处
|
|
16
|
+
"version": "0.1.0", // 语义化版本 x.y.z——市场更新比较靠它;+1 时务必同笔补 CHANGELOG.md 的新段
|
|
17
|
+
"description": "{{displayName}}——我的第一个 LinkDesk 插件", // 一句话描述(插件详情页展示)
|
|
18
|
+
"author": "{{author}}", // 作者名
|
|
19
|
+
"icon": "resources/icon.svg", // 图标——图标栏 / 标签页 / 市场里显示的就是它(resources/icon.svg 是占位图,换成你的)
|
|
20
|
+
|
|
21
|
+
// ── 入口(视图插件 = 此文件 default 导出一个 React 组件)──
|
|
22
|
+
"entry": "src/index.tsx",
|
|
23
|
+
|
|
24
|
+
// ── 出现位置:可作为主区标签页打开 ──
|
|
25
|
+
"appearsIn": { "tabBar": true },
|
|
26
|
+
"tabBehavior": { "singleton": true }, // 全局只开一个实例,避免重复标签
|
|
27
|
+
|
|
28
|
+
// ── 贡献点(contributes:全部可选,按需增删)──
|
|
29
|
+
"contributes": {
|
|
30
|
+
// 自带翻译:key=语言码, value=相对插件根的 JSON 文件。UI 文案用 t() 读这里;无需 zh.json——中文 key 原文自带兜底。
|
|
31
|
+
"i18n": { "en": "i18n/en.json" },
|
|
32
|
+
// ── 需要「侧栏 / 底部面板 / 辅助侧栏」分区视图时:取消注释,在 src/views/ 放对应组件,
|
|
33
|
+
// 容器 key 与 view id 用你的 pluginId 做前缀防撞(对标 plugins/panel-demo)──
|
|
34
|
+
// "viewsContainers": { "{{pluginName}}-sidebar": { "title": "{{displayName}}", "location": "sidebar" } },
|
|
35
|
+
// "views": {
|
|
36
|
+
// "{{pluginName}}-sidebar": [
|
|
37
|
+
// { "id": "main", "title": "{{displayName}}", "render": "src/views/MainView.tsx", "order": 0 }
|
|
38
|
+
// ]
|
|
39
|
+
// }
|
|
40
|
+
},
|
|
41
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 搬走的一共六类检查:编译图 / eslint(含 linkdesk/* 自定义规则)/ vitest / 体量 / i18n / 主题审计。
|
|
8
8
|
* 本脚本 + ci.yml + vitest 配置 = 给插件仓装回来的那一份,否则「独立」就是拿「质量真空」换的。
|
|
9
9
|
*
|
|
10
|
-
* ──
|
|
10
|
+
* ── 五段(每段独立判红;**没有对象也要说话**,不许静默绿)──
|
|
11
11
|
* ① lint 严格腿 —— `@linkdesk/plugin-sdk` 的 eslint 规则腿 + css/font-scale/spacing 三条扫描腿。
|
|
12
12
|
* 🔴 SDK 的 `npm run lint` 是 **WARN 级、永不 fail**(07 §六·三档:警告不是封锁,
|
|
13
13
|
* 作者本地不被拦——那是刻意的)。CI 要的是**拦截**,所以本段把同一份报告按
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
* 文件在;`contributes.themes` / `iconThemes` 的数据文件在且过各自的 schema;
|
|
25
25
|
* 主题 recipe 引用的 `linkdesk://<id>/…` 资产在(且 id 就是本插件);floatingPanel
|
|
26
26
|
* 三向自洽(viewId ↔ views[].id ↔ render)。
|
|
27
|
+
* ⑤ 目录条目形态 —— **发布产物** `marketplace.json` 里出现的图标字段(`icon` / `marketIcon`)必须是
|
|
28
|
+
* 绝对 URL + 来源标 `"url"`(E6#106)。理由:目录条目是**未装用户**看图时的唯一数据源,
|
|
29
|
+
* 而包内相对路径(`resources/icon.svg`)在未装态恒 404(`linkdesk://` 只在本地已装的
|
|
30
|
+
* 插件根里找文件)。`publish` 自 E6#106 起自动 URL 化;本段是那条纪律的机械兜底——
|
|
31
|
+
* 它看不见「谁是图标栏插件」(不看插件类型,只看字段形态,硬约束 10 零 ID 知识)。
|
|
27
32
|
*
|
|
28
33
|
* ── 为什么 ③ 的覆盖度只能黄灯(不是漏做)──
|
|
29
34
|
* `t()` 的 key 可以合法地住在**应用级字典**里(`lang-defaults` 插件,运行时由它经 LanguageRegistry
|
|
@@ -32,7 +37,7 @@
|
|
|
32
37
|
* 同款理由)。所以:字典**文件本身**的问题判红(③ 上半),**跨仓才能回答**的覆盖度只报告。
|
|
33
38
|
*
|
|
34
39
|
* 用法:node scripts/ci-verify.mjs (工程根 = cwd)
|
|
35
|
-
* 退出码 0 =
|
|
40
|
+
* 退出码 0 = 五段全过;1 = 有红灯(逐条打印缺什么)
|
|
36
41
|
*/
|
|
37
42
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
38
43
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
@@ -128,7 +133,7 @@ const legCount = (label) => report?.legs.find((l) => l.label === label)?.violati
|
|
|
128
133
|
const allRows = report?.eslintRows ?? [];
|
|
129
134
|
const ruleNotFound = allRows.filter((r) => RULE_NOT_FOUND_RE.test(r.message));
|
|
130
135
|
const strictEslintRows = allRows.filter((r) => !RULE_NOT_FOUND_RE.test(r.message));
|
|
131
|
-
/** 判红的三样:真 eslint 偏离 + css 硬编码腿(硬约束 1 的 .css 半边,eslint 到不了 .css)+ 见下
|
|
136
|
+
/** 判红的三样:真 eslint 偏离 + css 硬编码腿(硬约束 1 的 .css 半边,eslint 到不了 .css)+ 见下 ②③④⑤ */
|
|
132
137
|
const cssLegViolations = legCount("check-css-hardcode");
|
|
133
138
|
const strictLintViolations = strictEslintRows.length + cssLegViolations;
|
|
134
139
|
/** 只报告不拦的两条腿:字号度量与 4px 节奏——属「审美校准」(SDK 07 §六),存量偏离多且修它们要动插件源码 */
|
|
@@ -427,6 +432,60 @@ if (!manifest) {
|
|
|
427
432
|
}
|
|
428
433
|
if (themeAssetRefs.size > 0) ok.push(`${themeAssetRefs.size} 处 linkdesk:// 资产引用`);
|
|
429
434
|
|
|
435
|
+
/* ⑤ E6#106:目录条目的图标字段必须是**未装态可解析**的形态(绝对 URL)。
|
|
436
|
+
*
|
|
437
|
+
* 为什么这条能是纯字段断言、不需要知道「谁是图标栏插件」:无论哪种插件,**未装用户**看市场行时
|
|
438
|
+
* 目录条目是唯一数据源,而包内相对路径(`resources/icon.svg`)在未装态恒 404——「目录里存相对路径」
|
|
439
|
+
* 这件事本身就不成立,与插件类型无关。故断言只取形态,零插件 ID 知识(硬约束 10)。
|
|
440
|
+
*
|
|
441
|
+
* 为什么归 CI 而不是壳仓门禁:条目是**各仓自己的产物**,壳仓的 `npm run check` 够不着别人的仓。
|
|
442
|
+
* 与 ④ 段其它腿不同,本腿的对象是 `marketplace.json`(发布产物)——未发布过(无该文件)即跳过。 */
|
|
443
|
+
const catalogPath = resolve(ROOT, "marketplace.json");
|
|
444
|
+
if (!existsSync(catalogPath)) {
|
|
445
|
+
line(" ℹ ⑤ 目录条目图标形态:本仓无 marketplace.json(尚未发布过)——跳过。");
|
|
446
|
+
} else {
|
|
447
|
+
const catProblems = [];
|
|
448
|
+
const catOk = [];
|
|
449
|
+
try {
|
|
450
|
+
const cat = JSON.parse(readFileSync(catalogPath, "utf8"));
|
|
451
|
+
const entries = Array.isArray(cat) ? cat : (cat.plugins ?? []);
|
|
452
|
+
for (const e of entries) {
|
|
453
|
+
if (!e || typeof e !== "object") continue;
|
|
454
|
+
for (const key of ["icon", "marketIcon"]) {
|
|
455
|
+
const v = e[key];
|
|
456
|
+
if (v === undefined) continue;
|
|
457
|
+
if (/^https?:\/\//i.test(v)) {
|
|
458
|
+
// 形态对:绝对 URL。再钉一句「来源必须显式标 url」——消费端 resolvePluginIcon 对
|
|
459
|
+
// 「无 source 的绝对 URL」会当包内路径拼出 linkdesk://(两处判据不同源就会出这种错)。
|
|
460
|
+
const srcKey = key === "icon" ? "iconSource" : "marketIconSource";
|
|
461
|
+
if (e[srcKey] !== "url") {
|
|
462
|
+
catProblems.push(
|
|
463
|
+
`${key} 是绝对 URL 但 ${srcKey} ≠ "url"(现为 ${JSON.stringify(e[srcKey])})——` +
|
|
464
|
+
`未装端会把它当包内路径拼 linkdesk://`,
|
|
465
|
+
);
|
|
466
|
+
} else {
|
|
467
|
+
catOk.push(key);
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
catProblems.push(
|
|
471
|
+
`${key} = ${JSON.stringify(v)} 是**包内相对路径**——目录条目是未装用户的唯一图源,` +
|
|
472
|
+
`相对路径在未装态恒 404。跑 \`npm run publish\` 让 SDK 自动转绝对 URL(E6#106)。`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} catch (err) {
|
|
478
|
+
catProblems.push(`marketplace.json 解析失败:${err instanceof Error ? err.message : String(err)}`);
|
|
479
|
+
}
|
|
480
|
+
if (catProblems.length > 0) {
|
|
481
|
+
fail(`⑤ 目录条目图标形态:${catProblems.length} 处不可达/不合规\n${catProblems.map((p) => ` · ${p}`).join("\n")}`);
|
|
482
|
+
} else {
|
|
483
|
+
line(
|
|
484
|
+
` ✅ ⑤ 目录条目图标形态:${catOk.length > 0 ? `${catOk.join("、")} 均为绝对 URL(未装态可达)` : "本仓条目未声明图标(零图可发,允许)"}`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
430
489
|
// floatingPanel 声明自洽(壳侧 floatingPanelDeclarers.test.ts 的仓内等价物)
|
|
431
490
|
const fp = manifest.contributes?.floatingPanel;
|
|
432
491
|
if (fp && typeof fp === "object") {
|
|
@@ -479,5 +538,5 @@ if (failures.length > 0) {
|
|
|
479
538
|
console.error(` eslint-disable 注释 + 理由(见上面报告尾部),别把检查删了。`);
|
|
480
539
|
process.exitCode = 1;
|
|
481
540
|
} else {
|
|
482
|
-
console.log(`✅ 插件仓自检全过(${pluginId})——lint / 跨插件 / 字典 /
|
|
541
|
+
console.log(`✅ 插件仓自检全过(${pluginId})——lint / 跨插件 / 字典 / 声明自洽 / 目录条目形态五段。`);
|
|
483
542
|
}
|