create-linkdesk-plugin 0.1.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 ADDED
@@ -0,0 +1,39 @@
1
+ # create-linkdesk-plugin
2
+
3
+ LinkDesk 插件脚手架——一行命令生成你的第一个插件项目(对标 `yo code`)。
4
+
5
+ ```bash
6
+ npm create linkdesk-plugin my-cool-plugin
7
+ ```
8
+
9
+ 不带名字则交互式询问:
10
+
11
+ ```bash
12
+ npm create linkdesk-plugin
13
+ ```
14
+
15
+ ## 生成什么
16
+
17
+ ```
18
+ my-cool-plugin/
19
+ ├── plugin.json # 插件清单(JSONC:可注释/尾逗号,字段分节示范,VS Code $schema 校验)
20
+ ├── package.json # scripts: dev / build / validate;依赖 @linkdesk/plugin-sdk
21
+ ├── tsconfig.json # jsx: react-jsx + window.linkdesk.* 类型(@linkdesk/plugin-sdk)
22
+ ├── .vscode/settings.json # plugin.json 按 jsonc 识别(注释不标红)
23
+ ├── src/
24
+ │ ├── index.tsx # 视图组件 default 导出——壳以 { isActive, tabId?, sourceId? } 渲染
25
+ │ └── index.css # 样式示例——主题色走 var(--xxx)
26
+ └── i18n/
27
+ └── en.json
28
+ ```
29
+
30
+ 然后:
31
+
32
+ ```bash
33
+ cd my-cool-plugin
34
+ npm install
35
+ npm run build # 产出 <pluginId>.linkdesk-plugin——可装进 LinkDesk / 发布
36
+ ```
37
+
38
+ > 说明:插件作者工作流(dev 热预览 / build / 发布全链路)的完整文档见
39
+ > [00-第三方作者旅程](../../docs/02-Electron架构/E6_插件生态与发布/05-文档与发布/00-第三方作者旅程.md)。
package/index.js ADDED
@@ -0,0 +1,111 @@
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
+ *
9
+ * 行为:把同目录 template/ 复制到 <cwd>/<name>,把占位符替换成真实值,打印下一步提示。
10
+ * 占位符:{{pluginName}} {{displayName}} {{author}}(递归替换所有模板文件)。
11
+ *
12
+ * 生成产物契约:见 docs/02-Electron架构/E6_插件生态与发布/02-插件开发工具链/01-create-linkdesk-plugin脚手架.md。
13
+ */
14
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { spawnSync } from "node:child_process";
16
+ import { createInterface } from "node:readline";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), "template");
21
+
22
+ /** kebab-case——同时满足插件 id / viewsContainers key / npm 包名惯例(SAFE_PLUGIN_ID 的形状子集) */
23
+ const NAME_RE = /^[a-z][a-z0-9-]*$/;
24
+
25
+ /** my-cool-plugin → My Cool Plugin */
26
+ function toDisplayName(name) {
27
+ return name
28
+ .split("-")
29
+ .map((s) => (s ? s[0].toUpperCase() + s.slice(1) : s))
30
+ .join(" ");
31
+ }
32
+
33
+ /** 作者默认值 = git config user.name;读不到(无 git/无配置)→ "you"(作者生成后自改) */
34
+ function gitUserName() {
35
+ try {
36
+ const r = spawnSync("git", ["config", "user.name"], { encoding: "utf8", timeout: 3000 });
37
+ const v = (r.stdout || "").trim();
38
+ return v || "you";
39
+ } catch {
40
+ return "you";
41
+ }
42
+ }
43
+
44
+ /** 交互式单问——返回去除首尾空白的答案 */
45
+ function ask(question) {
46
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
47
+ return new Promise((resolve) => {
48
+ rl.question(question, (answer) => {
49
+ rl.close();
50
+ resolve(answer.trim());
51
+ });
52
+ });
53
+ }
54
+
55
+ function replacePlaceholders(file, values) {
56
+ let text = readFileSync(file, "utf8");
57
+ for (const [key, value] of Object.entries(values)) {
58
+ text = text.split(`{{${key}}}`).join(value);
59
+ }
60
+ writeFileSync(file, text);
61
+ }
62
+
63
+ /** 递归替换目录内全部文件(模板全是文本文件,无需跳过二进制) */
64
+ function walkReplace(dir, values) {
65
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
66
+ const full = join(dir, entry.name);
67
+ if (entry.isDirectory()) walkReplace(full, values);
68
+ else replacePlaceholders(full, values);
69
+ }
70
+ }
71
+
72
+ async function main() {
73
+ let name = (process.argv[2] || "").trim();
74
+ if (!name) {
75
+ name = await ask("插件名(kebab-case,如 my-cool-plugin): ");
76
+ }
77
+ name = name.trim();
78
+ if (!NAME_RE.test(name)) {
79
+ console.error(`✖ 插件名须为 kebab-case(小写字母/数字/连字符),收到:${JSON.stringify(name)}`);
80
+ process.exit(1);
81
+ }
82
+
83
+ const target = join(process.cwd(), name);
84
+ if (existsSync(target) && readdirSync(target).length > 0) {
85
+ console.error(`✖ ${name}/ 已存在且非空——换个名字,或清空后重跑`);
86
+ process.exit(1);
87
+ }
88
+
89
+ mkdirSync(target, { recursive: true });
90
+ cpSync(TEMPLATE_DIR, target, { recursive: true });
91
+
92
+ const values = { pluginName: name, displayName: toDisplayName(name), author: gitUserName() };
93
+ walkReplace(target, values);
94
+
95
+ console.log("");
96
+ console.log(`✔ ${name}/ 已创建`);
97
+ console.log("");
98
+ console.log(" 接下来:");
99
+ console.log(` cd ${name}`);
100
+ console.log(" npm install");
101
+ console.log(" npm run validate # 校验 plugin.json($schema / 字段 / i18n 文件)");
102
+ console.log(" npm run build # 产出 <pluginId>.linkdesk-plugin,可装进 LinkDesk / 发布");
103
+ console.log("");
104
+ console.log(" 编辑 plugin.json 的 name / description / author,src/index.tsx 是你的插件本体。");
105
+ console.log(" 更多插件能力(侧栏视图 / 命令 / 设置 / 协议……)见 docs/03-插件制造/ 与 plugin.schema.json。");
106
+ }
107
+
108
+ main().catch((err) => {
109
+ console.error(err);
110
+ process.exit(1);
111
+ });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "create-linkdesk-plugin",
3
+ "version": "0.1.0",
4
+ "description": "LinkDesk 插件脚手架——`npm create linkdesk-plugin my-cool-plugin` 一行生成你的第一个插件项目(对标 yo code)。",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-linkdesk-plugin": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "template",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ },
17
+ "keywords": [
18
+ "linkdesk",
19
+ "plugin",
20
+ "scaffold",
21
+ "create"
22
+ ],
23
+ "license": "MIT"
24
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "files.associations": {
3
+ "plugin.json": "jsonc"
4
+ }
5
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "hello": "Hello from LinkDesk!"
3
+ }
@@ -0,0 +1,17 @@
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
+ "build": "linkdesk-plugin-sdk build",
10
+ "validate": "linkdesk-plugin-sdk validate"
11
+ },
12
+ "devDependencies": {
13
+ "@linkdesk/plugin-sdk": "^0.1.0",
14
+ "@types/react": "^18.3.12",
15
+ "typescript": "^5.6.3"
16
+ }
17
+ }
@@ -0,0 +1,38 @@
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
+ "pluginId": "{{pluginName}}", // 全局唯一 ID(kebab-case)——安装目录 / 寻址 / 命令前缀都按它
13
+ "name": "{{displayName}}", // 显示名——标签页 / 插件详情等 UI 出现处
14
+ "version": "0.1.0", // 语义化版本 x.y.z——发布更新比较靠它,改了要 +1
15
+ "description": "{{displayName}}——我的第一个 LinkDesk 插件", // 一句话描述(插件详情页展示)
16
+ "author": "{{author}}", // 作者名
17
+
18
+ // ── 入口(视图插件 = 此文件 default 导出一个 React 组件)──
19
+ "entry": "src/index.tsx",
20
+
21
+ // ── 出现位置:可作为主区标签页打开 ──
22
+ "appearsIn": { "tabBar": true },
23
+ "tabBehavior": { "singleton": true }, // 全局只开一个实例,避免重复标签
24
+
25
+ // ── 贡献点(contributes:全部可选,按需增删)──
26
+ "contributes": {
27
+ // 自带翻译:key=语言码, value=相对插件根的 JSON 文件。UI 文案用 t() 读这里;无需 zh.json——中文 key 原文自带兜底。
28
+ "i18n": { "en": "i18n/en.json" },
29
+ // ── 需要「侧栏 / 底部面板 / 辅助侧栏」分区视图时:取消注释,在 src/views/ 放对应组件,
30
+ // 容器 key 与 view id 用你的 pluginId 做前缀防撞(对标 plugins/panel-demo)──
31
+ // "viewsContainers": { "{{pluginName}}-sidebar": { "title": "{{displayName}}", "location": "sidebar" } },
32
+ // "views": {
33
+ // "{{pluginName}}-sidebar": [
34
+ // { "id": "main", "title": "{{displayName}}", "render": "src/views/MainView.tsx", "order": 0 }
35
+ // ]
36
+ // }
37
+ },
38
+ }
@@ -0,0 +1,23 @@
1
+ /* {{displayName}} 样式示例——LinkDesk 主题色一律走 var(--xxx),禁硬编码 hex。
2
+ 布局/字号这类结构值用普通 px(不违反);颜色/语义类才必须走主题变量。 */
3
+
4
+ .starter {
5
+ padding: 24px;
6
+ font-family: inherit;
7
+ }
8
+
9
+ .starter__title {
10
+ margin: 0 0 8px;
11
+ color: var(--text);
12
+ }
13
+
14
+ .starter__text {
15
+ margin: 0 0 4px;
16
+ color: var(--text-muted);
17
+ }
18
+
19
+ .starter__hint {
20
+ margin: 0;
21
+ color: var(--text-muted);
22
+ font-size: 12px;
23
+ }
@@ -0,0 +1,29 @@
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
+ * UI 文案规范化后用 t()(react-i18next,壳提供)读 i18n/en.json——见 05-UI写法规约.md。
13
+ * 壳已 external react/react-dom/react-i18next/i18next——构建不会打进包,插件工程无需 npm i 它们。
14
+ */
15
+
16
+ import "./index.css";
17
+
18
+ export default function HelloPlugin(_props: { isActive?: boolean; tabId?: string; sourceId?: string }) {
19
+ return (
20
+ <div className="starter">
21
+ <h2 className="starter__title">{{displayName}} 跑起来了 ✨</h2>
22
+ <p className="starter__text">这是你的第一个 LinkDesk 插件。</p>
23
+ <p className="starter__hint">
24
+ 编辑 <code>src/index.tsx</code> 即可看到变化;<code>npm run build</code> 打包出{" "}
25
+ <code>.linkdesk-plugin</code> 分发文件。
26
+ </p>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "jsx": "react-jsx",
9
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
10
+ "types": ["@linkdesk/plugin-sdk"],
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src"]
14
+ }