create-linkdesk-plugin 0.1.1 → 0.1.3
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/LICENSE +21 -0
- package/README.md +55 -55
- package/index.js +266 -136
- package/package.json +2 -2
- package/template/.github/workflows/ci.yml +45 -0
- package/template/.vscode/settings.json +8 -8
- package/template/README.md +10 -0
- package/template/gitignore +8 -8
- package/template/package.json +8 -2
- package/template/plugin.json +3 -3
- package/template/resources/icon.svg +23 -23
- package/template/scripts/ci-verify.mjs +483 -0
- package/template/src/index.css +30 -30
- package/template/src/index.tsx +35 -35
- package/template/vitest.config.ts +34 -0
- package/template/vitest.setup.ts +96 -0
package/template/src/index.tsx
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* {{displayName}}——LinkDesk 插件主视图(由 create-linkdesk-plugin 生成)。
|
|
3
|
-
*
|
|
4
|
-
* 视图插件契约(E5.8,见 docs/03-插件制造/01-API契约.md):壳以 { isActive, tabId?, sourceId? }
|
|
5
|
-
* 渲染本文件 default 导出的组件:
|
|
6
|
-
* - isActive 本标签当前是否聚焦。keep-alive 下非聚焦标签仍在渲染,isActive 只用于
|
|
7
|
-
* gate「聚焦才跑」的副作用(如自动保存),切勿用它整块 blank 掉内容。
|
|
8
|
-
* - tabId 本标签页 id。
|
|
9
|
-
* - sourceId 上下文数据(文件路径 / 数据源等),编辑器类插件用它定位内容。
|
|
10
|
-
*
|
|
11
|
-
* 样式:LinkDesk 主题色一律走 CSS 变量 var(--xxx)(见 index.css 示例),禁硬编码 hex。
|
|
12
|
-
* 文案:用 t() 读——key 就是中文原文,英文译文放 i18n/en.json(见 05-UI写法规约.md)。
|
|
13
|
-
* 壳已 external react/react-dom/react-i18next/i18next——构建不会打进包,插件工程无需 npm i 它们。
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { useTranslation } from "react-i18next";
|
|
17
|
-
import "./index.css";
|
|
18
|
-
|
|
19
|
-
export default function HelloPlugin(_props: { isActive?: boolean; tabId?: string; sourceId?: string }) {
|
|
20
|
-
const { t } = useTranslation();
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<div className="starter">
|
|
24
|
-
<h2 className="starter__title">{t("插件跑起来了 ✨")}</h2>
|
|
25
|
-
<p className="starter__text">{t("这是你的第一个 LinkDesk 插件。")}</p>
|
|
26
|
-
<p className="starter__hint">
|
|
27
|
-
<code>src/index.tsx</code> {t("是插件本体——改它,浏览器预览即时刷新。")}
|
|
28
|
-
</p>
|
|
29
|
-
<p className="starter__hint">
|
|
30
|
-
<code>npm run build</code> {t("打包出分发文件,可装进 LinkDesk 或发布到市场。")}
|
|
31
|
-
</p>
|
|
32
|
-
<p className="starter__hint">{t("目录该放哪、发布怎么做,都写在 README.md 里。")}</p>
|
|
33
|
-
</div>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* {{displayName}}——LinkDesk 插件主视图(由 create-linkdesk-plugin 生成)。
|
|
3
|
+
*
|
|
4
|
+
* 视图插件契约(E5.8,见 docs/03-插件制造/01-API契约.md):壳以 { isActive, tabId?, sourceId? }
|
|
5
|
+
* 渲染本文件 default 导出的组件:
|
|
6
|
+
* - isActive 本标签当前是否聚焦。keep-alive 下非聚焦标签仍在渲染,isActive 只用于
|
|
7
|
+
* gate「聚焦才跑」的副作用(如自动保存),切勿用它整块 blank 掉内容。
|
|
8
|
+
* - tabId 本标签页 id。
|
|
9
|
+
* - sourceId 上下文数据(文件路径 / 数据源等),编辑器类插件用它定位内容。
|
|
10
|
+
*
|
|
11
|
+
* 样式:LinkDesk 主题色一律走 CSS 变量 var(--xxx)(见 index.css 示例),禁硬编码 hex。
|
|
12
|
+
* 文案:用 t() 读——key 就是中文原文,英文译文放 i18n/en.json(见 05-UI写法规约.md)。
|
|
13
|
+
* 壳已 external react/react-dom/react-i18next/i18next——构建不会打进包,插件工程无需 npm i 它们。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useTranslation } from "react-i18next";
|
|
17
|
+
import "./index.css";
|
|
18
|
+
|
|
19
|
+
export default function HelloPlugin(_props: { isActive?: boolean; tabId?: string; sourceId?: string }) {
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="starter">
|
|
24
|
+
<h2 className="starter__title">{t("插件跑起来了 ✨")}</h2>
|
|
25
|
+
<p className="starter__text">{t("这是你的第一个 LinkDesk 插件。")}</p>
|
|
26
|
+
<p className="starter__hint">
|
|
27
|
+
<code>src/index.tsx</code> {t("是插件本体——改它,浏览器预览即时刷新。")}
|
|
28
|
+
</p>
|
|
29
|
+
<p className="starter__hint">
|
|
30
|
+
<code>npm run build</code> {t("打包出分发文件,可装进 LinkDesk 或发布到市场。")}
|
|
31
|
+
</p>
|
|
32
|
+
<p className="starter__hint">{t("目录该放哪、发布怎么做,都写在 README.md 里。")}</p>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 插件工程测试环境(E6#102 §二)。
|
|
5
|
+
*
|
|
6
|
+
* 🔴 这份配置**逐项对齐壳仓 `vitest.config.ts`**:`globals` / `environment: "jsdom"` /
|
|
7
|
+
* `setupFiles` 三条是**静默生效**的配置——缺了会报错(那还算好),配错了则是「本地绿、CI 红」,
|
|
8
|
+
* 是这类迁移最典型的坑。改这里前先看壳仓那份,别让两边环境分叉。
|
|
9
|
+
*
|
|
10
|
+
* 两条**刻意不抄**壳仓的地方(写了就是错):
|
|
11
|
+
* - **不设 `@src` / `@` 别名**:壳仓别名是给「与壳同仓的插件」用的;插件源码已外移,
|
|
12
|
+
* `@src` 在本仓物理不可达。插件代码只经 `window.linkdesk.*` 与 `@linkdesk/ui` 拿能力。
|
|
13
|
+
* - **不 include `plugins/**`**:本仓就是一只插件,源码在 `src/`。
|
|
14
|
+
*
|
|
15
|
+
* 🔴 **`server.deps.inline` 是为「仓外形态」新加的一条**(壳仓没有、也不需要):壳仓里
|
|
16
|
+
* `@linkdesk/ui` 解析到**同仓源码**,CSS 由 Vite 顺手处理;插件仓解析到**已发布的 dist**,
|
|
17
|
+
* 而 `dist/index.js` 里有 `import "./index.css"` —— Node 的外部依赖加载器读不了 `.css`,
|
|
18
|
+
* 于是凡经 `@linkdesk/ui` 的测试全部倒在
|
|
19
|
+
* `TypeError: Unknown file extension ".css"`(E6#102 实测:marketplace 6 个文件 / 12 例)。
|
|
20
|
+
* 把 `@linkdesk/ui` 交给 Vite 内联处理后即恢复。**别删这一条**——删了只会在有 UI 组件的
|
|
21
|
+
* 仓里以「莫名其妙的环境错」重现。
|
|
22
|
+
*
|
|
23
|
+
* `passWithNoTests`:还没有测试的工程跑 `vitest run` 不该红——它是「没写」不是「写错了」。
|
|
24
|
+
*/
|
|
25
|
+
export default defineConfig({
|
|
26
|
+
test: {
|
|
27
|
+
globals: true,
|
|
28
|
+
environment: "jsdom",
|
|
29
|
+
setupFiles: ["./vitest.setup.ts"],
|
|
30
|
+
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
|
31
|
+
passWithNoTests: true,
|
|
32
|
+
server: { deps: { inline: ["@linkdesk/ui"] } },
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vitest setup——mock window.linkdesk API。
|
|
3
|
+
* 测试跑在 Node.js/jsdom,没有 Electron preload 注入的 window.linkdesk。
|
|
4
|
+
* 迁移到 linkdesk.* API 后,插件代码直接依赖它——测试环境需提供最小 mock。
|
|
5
|
+
*
|
|
6
|
+
* 🔴 E6#102(L7 第 7.5 轮):**本文件是壳仓 `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
|
+
// E5.7#98:测试全局窄类型 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
|
+
};
|