create-linkdesk-plugin 0.1.13 → 0.1.15
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 +1 -1
- package/package.json +1 -1
- package/template/AGENTS.md +9 -0
- package/template/README.md +1 -1
- package/template/scripts/ci-verify.mjs +28 -3
- package/template/vitest.setup.ts +3 -96
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ my-cool-plugin/
|
|
|
30
30
|
├── .github/workflows/ci.yml # CI that runs `npm run verify` on every push
|
|
31
31
|
├── scripts/ci-verify.mjs # the strict tier CI runs (lint + tests + declaration self-checks)
|
|
32
32
|
├── vitest.config.ts # test config (jsdom + globals; @linkdesk/ui is inlined so its CSS import resolves)
|
|
33
|
-
├── vitest.setup.ts # test
|
|
33
|
+
├── vitest.setup.ts # one-line pointer to the shared test ground in @linkdesk/plugin-sdk (the window.linkdesk mock)
|
|
34
34
|
├── .vscode/settings.json # plugin.json is treated as jsonc (comments do not light up red)
|
|
35
35
|
├── resources/
|
|
36
36
|
│ └── icon.svg # placeholder icon — replace it with your own
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -47,6 +47,15 @@ npm run publish # ⑧ publish to your own GitHub repo (first step of listing)
|
|
|
47
47
|
- **This project is its own git repo** (the scaffold ran `git init -b main` plus one initial commit). `publish` uses `origin` to create the Release ⇒ pushing to GitHub is just two commands: `git remote add origin <your repo>` + `git push -u origin main`.
|
|
48
48
|
- **Keep the version numbers in step**: `plugin.json`'s `version` and `package.json`'s `version` **must match**; and every bump needs a matching `## v<new version>(YYYY-MM-DD)` section in `CHANGELOG.md`, or the plugin's detail page will show "no changelog provided for this version".
|
|
49
49
|
|
|
50
|
+
## 6. Tests in this repo
|
|
51
|
+
|
|
52
|
+
- **The toolchain is preinstalled** (`vitest` / `jsdom` / `@testing-library/react` are already declared) — write a `*.test.ts` file and run `npm run test`.
|
|
53
|
+
- **The shared test ground comes from the SDK**: the minimal `window.linkdesk` mock lives in `@linkdesk/plugin-sdk/vitest-setup`, and this repo's `vitest.setup.ts` is a **one-line pointer** to it. **Do not paste a second copy in here** — a copied mock drifts from the shared one and nothing would notice.
|
|
54
|
+
- **Pure logic should be tested**: a `src/**/*.ts` unit that touches neither React nor `window.linkdesk` should get a `src/__tests__/<same name>.test.ts`. Views and hooks are worth testing **when it pays off** — nothing measures that today.
|
|
55
|
+
- **Plugin-specific stubs stay in your own test files**: if your code calls something the shared mock does not cover (say `window.linkdesk.serial`), set that stub up with `vi.fn()` in the test. Do not ask the shared mock to grow a branch for your plugin.
|
|
56
|
+
- **Declarative plugins** (pure JSON / themes / language packs) have **no testable units** — their gate is the structure and declaration checks in `npm run verify`. "It has no tests" is not a defect there.
|
|
57
|
+
- **Older projects can migrate whenever they like**: replacing the full mock body with the one-line pointer plus one `npm install` is the whole move (upside: mock upgrades then ride along with the SDK).
|
|
58
|
+
|
|
50
59
|
---
|
|
51
60
|
|
|
52
61
|
> Author: {{author}} | Generated: {{date}}
|
package/template/README.md
CHANGED
|
@@ -26,7 +26,7 @@ You do not need to pre-create empty folders (git does not track them). **Create
|
|
|
26
26
|
| `src/components/` | Components reused inside this plugin | When needed |
|
|
27
27
|
| `src/services/` | Domain logic / IPC wrappers / data layer | When needed |
|
|
28
28
|
| `src/styles/` | **Multiple** CSS files — keep them together here (a single file next to the entry is fine too) | When needed |
|
|
29
|
-
| `src/__tests__/` | Unit tests — **test tooling is preinstalled** (`vitest` / `jsdom` / `@testing-library/react` are already in `devDependencies`); write tests and run `npm run test` | When needed |
|
|
29
|
+
| `src/__tests__/` | Unit tests — **test tooling is preinstalled** (`vitest` / `jsdom` / `@testing-library/react` are already in `devDependencies`); the `window.linkdesk` mock is **not** in this repo — `vitest.setup.ts` is a one-line pointer to the shared ground in `@linkdesk/plugin-sdk`, so just write tests and run `npm run test` | When needed |
|
|
30
30
|
|
|
31
31
|
> 🔴 **Shared things do not belong here** — components/hooks reused across plugins come from `@linkdesk/ui` (the public package the shell provides; it is already declared in `package.json` as `"latest"`, which resolves to the shell's current version line when you install — pin it to a specific shell version if you need a floor). The shell supplies that one instance at runtime, so **do not import its css** and **do not write a second copy inside your plugin**. Only logic that belongs to this plugin stays local.
|
|
32
32
|
> 🔴 **Assets always live in `resources/` — no loose images in the plugin root.** What gets into the install package is what is **referenced by the README** or **declared by `icon` / `marketIcon`**; the directory name itself has no magic.
|
|
@@ -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**(三档制:警告不是封锁,
|
|
13
13
|
* 作者本地不被拦——那是刻意的)。CI 要的是**拦截**,所以本段把同一份报告按
|
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
* 而包内相对路径(`resources/icon.svg`)在未装态恒 404(`linkdesk://` 只在本地已装的
|
|
30
30
|
* 插件根里找文件)。`publish` 会自动 URL 化;本段是那条纪律的机械兜底——
|
|
31
31
|
* 它看不见「谁是图标栏插件」(不看插件类型,只看字段形态,硬约束 10 零 ID 知识)。
|
|
32
|
+
* ⑥ 测试覆盖 —— 结构判据(2026-09-26 用户拍板升拦):**纯逻辑单元**(`src` 下任意深度的 `.ts`
|
|
33
|
+
* 去 `.d.ts`/测试/barrel/`use*`/含 `window.linkdesk` 或 React)应有测试,命中 =
|
|
34
|
+
* **同名测试文件 ∨ 被任一测试文件引用**——与官方覆盖尺**同一份实现**
|
|
35
|
+
* (`@linkdesk/plugin-sdk/test-audit`,⛔ 别复制判据)。🔴 零测报出 ≠ 判死:
|
|
36
|
+
* 同名判据看不见跨文件覆盖(一跳传递假红实测过)⇒ 先核覆盖再补测,别写无意义测试凑数。
|
|
37
|
+
* 百分比不上门禁(覆盖口径已定稿为结构判据);声明式/零逻辑仓按据豁免(无硬编码白名单)。
|
|
32
38
|
*
|
|
33
39
|
* ── 为什么 ③ 的覆盖度只能黄灯(不是漏做)──
|
|
34
40
|
* `t()` 的 key 可以合法地住在**应用级字典**里(`lang-defaults` 插件,运行时由它经 LanguageRegistry
|
|
@@ -37,13 +43,14 @@
|
|
|
37
43
|
* 同款理由)。所以:字典**文件本身**的问题判红(③ 上半),**跨仓才能回答**的覆盖度只报告。
|
|
38
44
|
*
|
|
39
45
|
* 用法:node scripts/ci-verify.mjs (工程根 = cwd)
|
|
40
|
-
* 退出码 0 =
|
|
46
|
+
* 退出码 0 = 六段全过;1 = 有红灯(逐条打印缺什么)
|
|
41
47
|
*/
|
|
42
48
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
43
49
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
44
50
|
import { parse as parseJsonc, printParseErrorCode } from "jsonc-parser";
|
|
45
51
|
import { runPluginLint, renderPluginLintReport } from "@linkdesk/plugin-sdk/eslint";
|
|
46
52
|
import { validateThemeJson, validateIconThemeJson } from "@linkdesk/plugin-sdk";
|
|
53
|
+
import { analyzeRepo } from "@linkdesk/plugin-sdk/test-audit";
|
|
47
54
|
|
|
48
55
|
const ROOT = process.cwd();
|
|
49
56
|
const failures = [];
|
|
@@ -541,6 +548,24 @@ if (!manifest) {
|
|
|
541
548
|
}
|
|
542
549
|
}
|
|
543
550
|
|
|
551
|
+
// ═══════════════ ⑥ 测试覆盖(结构判据——2026-09-26 用户拍板升拦)═══════════════
|
|
552
|
+
// 审计核心与官方覆盖尺**同一份实现**(@linkdesk/plugin-sdk/test-audit 单一真源)——⛔ 别在这里复制判据。
|
|
553
|
+
const audit = analyzeRepo(ROOT, { official: true });
|
|
554
|
+
if (audit.kind === "exempt") {
|
|
555
|
+
line(`⏭ ⑥ 测试覆盖:${audit.exempt}`);
|
|
556
|
+
} else if (audit.zeroTest.length > 0) {
|
|
557
|
+
fail(
|
|
558
|
+
`⑥ 测试覆盖:${audit.zeroTest.length} 个纯逻辑单元零测试(判据 = 同名测试文件 ∨ 被任一测试文件引用):\n` +
|
|
559
|
+
audit.zeroTest.map((z) => ` src/${z.unit}.ts(${z.evidence.join(" + ")})`).join("\n") +
|
|
560
|
+
`\n ⚠️ 零测 = 待裁决不是判死——同名判据看不见跨文件覆盖,先核这些单元是否已被别的测试` +
|
|
561
|
+
`间接覆盖,再补测(替身照契约不照实现;夹具不用真插件名/真文案;⛔ 不写无断言测试凑数)。`,
|
|
562
|
+
);
|
|
563
|
+
} else {
|
|
564
|
+
line(
|
|
565
|
+
`✅ ⑥ 测试覆盖:纯逻辑单元 ${audit.logicUnits} 个全部有测试(宽口径 ${audit.wideUnits},命中 = 同名 ∨ 被引用)。`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
544
569
|
// ═══════════════ 结论 ═══════════════
|
|
545
570
|
line("────────────────────────────────────────────────────────────");
|
|
546
571
|
if (failures.length > 0) {
|
|
@@ -550,5 +575,5 @@ if (failures.length > 0) {
|
|
|
550
575
|
console.error(` eslint-disable 注释 + 理由(见上面报告尾部),别把检查删了。`);
|
|
551
576
|
process.exitCode = 1;
|
|
552
577
|
} else {
|
|
553
|
-
console.log(`✅ 插件仓自检全过(${pluginId})——lint / 跨插件 / 字典 / 声明自洽 /
|
|
578
|
+
console.log(`✅ 插件仓自检全过(${pluginId})——lint / 跨插件 / 字典 / 声明自洽 / 目录条目形态 / 测试覆盖六段。`);
|
|
554
579
|
}
|
package/template/vitest.setup.ts
CHANGED
|
@@ -1,96 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* 迁移到 linkdesk.* API 后,插件代码直接依赖它——测试环境需提供最小 mock。
|
|
5
|
-
*
|
|
6
|
-
* 🔴 **本文件是壳仓 `vitest.setup.ts` 的逐字副本**(除本头注五条)。
|
|
7
|
-
* 它不是「配置」,是插件测试的**运行时地基**——下半部的六个命名空间与 `__ldkConfigStore`
|
|
8
|
-
* 少了任何一个,凡碰 `window.linkdesk` 的测试都会报错、或更糟:静默走错分支。
|
|
9
|
-
* 改壳仓那份时把这里一起改(两处同源)。「由 @linkdesk/plugin-sdk 提供共享版本、
|
|
10
|
-
* 本文件改成一行 re-export」是可预见的收敛方向——那时这五条注记一并删掉。
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// path 纯函数——直接实现,不走 IPC
|
|
14
|
-
const pathMock = {
|
|
15
|
-
normalize: (p: string) => p.replace(/\\/g, "/"),
|
|
16
|
-
join: (...parts: string[]) =>
|
|
17
|
-
parts.map((p) => String(p).replace(/\\/g, "/")).join("/").replace(/\/+/g, "/"),
|
|
18
|
-
basename: (p: string) => {
|
|
19
|
-
const s = p.replace(/\\/g, "/").split("/");
|
|
20
|
-
return s[s.length - 1] || "";
|
|
21
|
-
},
|
|
22
|
-
dirname: (p: string) => {
|
|
23
|
-
const s = p.replace(/\\/g, "/").split("/");
|
|
24
|
-
s.pop();
|
|
25
|
-
return s.join("/") || ".";
|
|
26
|
-
},
|
|
27
|
-
extname: (p: string) => {
|
|
28
|
-
const b = p.replace(/\\/g, "/").split("/").pop() || "";
|
|
29
|
-
const i = b.lastIndexOf(".");
|
|
30
|
-
return i > 0 ? b.slice(i) : "";
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// 测试全局窄类型 cast——替代 (globalThis as any)(__ldkConfigStore 由本文件声明、测试文件消费)
|
|
35
|
-
type TestGlobal = { window?: Window; __ldkConfigStore?: Map<string, unknown> };
|
|
36
|
-
const _g = globalThis as TestGlobal;
|
|
37
|
-
|
|
38
|
-
// configuration——默认返回 null,测试中按需 mock。
|
|
39
|
-
// __ldkConfigStore 暴露给测试——测试可直接设置值控制 get() 返回。
|
|
40
|
-
const _configStore = (_g.__ldkConfigStore = new Map<string, unknown>());
|
|
41
|
-
const configurationMock = {
|
|
42
|
-
get: async (key: string) => _configStore.get(key) ?? null,
|
|
43
|
-
set: async (key: string, v: unknown) => { _configStore.set(key, v); },
|
|
44
|
-
onChange: (_key: string, _cb: (v: unknown) => void) => {
|
|
45
|
-
return () => {}; // no-op unsubscribe
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// workspace——默认返回空工作区
|
|
50
|
-
const workspaceMock = {
|
|
51
|
-
getFolders: async () => [] as { uri: string; name: string }[],
|
|
52
|
-
getActive: async () => undefined as string | undefined,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// filesystem——可替换的最小 stub。测试可覆盖 lk.filesystem.xxx = vi.fn() 按需定制
|
|
56
|
-
const filesystemMock = {
|
|
57
|
-
readTextFile: async (_p: string) => "",
|
|
58
|
-
writeTextFile: async (_p: string, _d: string) => {},
|
|
59
|
-
readBinaryFile: async (_p: string) => new Uint8Array(),
|
|
60
|
-
writeBinaryFile: async (_p: string, _d: Uint8Array) => {},
|
|
61
|
-
listDir: async (_p: string) => [] as { path: string; name: string; isDirectory: boolean; isFile: boolean }[],
|
|
62
|
-
exists: async (_p: string) => false,
|
|
63
|
-
mkdir: async (_p: string) => {},
|
|
64
|
-
copy: async (_src: string, _dest: string) => {},
|
|
65
|
-
remove: async (_p: string) => {},
|
|
66
|
-
watch: async (_dirPath: string, _onEvent: (e: unknown) => void) => {
|
|
67
|
-
return () => {}; // unsubscribe
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// tabs——最小 stub
|
|
72
|
-
const tabsMock = {
|
|
73
|
-
create: async (_type: string, _opts?: Record<string, unknown>) => "tab-1",
|
|
74
|
-
openOrFocus: async (_type: string, _opts?: Record<string, unknown>) => "tab-1",
|
|
75
|
-
focus: async (_tabId: string) => {},
|
|
76
|
-
close: async (_tabId: string) => {},
|
|
77
|
-
focusBySourceId: async (_sourceId: string) => {},
|
|
78
|
-
updateLabelBySourceId: async (_sourceId: string, _label: string) => {},
|
|
79
|
-
closeBySourceId: async (_sourceId: string) => {},
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// 最小 mock——故意不满足 LinkDeskAPI 全契约(测试按需覆盖),经 linkdesk?: object 窄口赋值
|
|
83
|
-
_g.window = _g.window ?? ({} as Window);
|
|
84
|
-
(_g.window as Window & { linkdesk?: object }).linkdesk = {
|
|
85
|
-
path: pathMock,
|
|
86
|
-
configuration: configurationMock,
|
|
87
|
-
config: configurationMock,
|
|
88
|
-
workspace: workspaceMock,
|
|
89
|
-
filesystem: filesystemMock,
|
|
90
|
-
tabs: tabsMock,
|
|
91
|
-
// event stubs
|
|
92
|
-
events: {
|
|
93
|
-
on: () => () => {},
|
|
94
|
-
emit: () => {},
|
|
95
|
-
},
|
|
96
|
-
};
|
|
1
|
+
// Shared test ground: the minimal `window.linkdesk` mock ships with the SDK (`@linkdesk/plugin-sdk/vitest-setup`).
|
|
2
|
+
// One line on purpose — plugin-specific stubs belong in your own test files (`vi.fn()`), not in here.
|
|
3
|
+
import "@linkdesk/plugin-sdk/vitest-setup";
|