@aiot-nexus/plugin-sdk 1.0.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.
Files changed (47) hide show
  1. package/README.md +74 -0
  2. package/dev-host/host.html +344 -0
  3. package/dev-host/main.js +694 -0
  4. package/dev-host/preload.js +22 -0
  5. package/dist/.generated/plugin-framework-version.d.ts +11 -0
  6. package/dist/.generated/plugin-framework-version.js +39 -0
  7. package/dist/.generated/plugin-manifest.schema.json +106 -0
  8. package/dist/browser/frame-client.d.ts +26 -0
  9. package/dist/browser/frame-client.mjs +239 -0
  10. package/dist/src/browser/frame-client.d.ts +26 -0
  11. package/dist/src/browser/frame-client.js +244 -0
  12. package/dist/src/cli/run.d.ts +7 -0
  13. package/dist/src/cli/run.js +270 -0
  14. package/dist/src/cli.d.ts +2 -0
  15. package/dist/src/cli.js +9 -0
  16. package/dist/src/dev.d.ts +36 -0
  17. package/dist/src/dev.js +224 -0
  18. package/dist/src/index.d.ts +7 -0
  19. package/dist/src/index.js +27 -0
  20. package/dist/src/scaffold.d.ts +23 -0
  21. package/dist/src/scaffold.js +306 -0
  22. package/dist/src/types.d.ts +142 -0
  23. package/dist/src/types.js +2 -0
  24. package/dist/src/validation.d.ts +9 -0
  25. package/dist/src/validation.js +160 -0
  26. package/package.json +47 -0
  27. package/templates/minimal/.gitignore.tmpl +4 -0
  28. package/templates/minimal/.vscode/launch.json +22 -0
  29. package/templates/minimal/README.md.tmpl +22 -0
  30. package/templates/minimal/scripts/build.js.tmpl +83 -0
  31. package/templates/minimal/src/main.ts.tmpl +23 -0
  32. package/templates/minimal/src/renderer/app.ts.tmpl +194 -0
  33. package/templates/minimal/src/renderer/index.html +34 -0
  34. package/templates/minimal/src/renderer/plugin.css +164 -0
  35. package/templates/minimal/tsconfig.main.json +15 -0
  36. package/templates/minimal/tsconfig.renderer.json +17 -0
  37. package/templates/sqlite/.gitignore.tmpl +4 -0
  38. package/templates/sqlite/.vscode/launch.json +22 -0
  39. package/templates/sqlite/README.md.tmpl +22 -0
  40. package/templates/sqlite/scripts/build.js.tmpl +143 -0
  41. package/templates/sqlite/src/better-sqlite3.d.ts +22 -0
  42. package/templates/sqlite/src/main.ts.tmpl +190 -0
  43. package/templates/sqlite/src/renderer/app.ts.tmpl +428 -0
  44. package/templates/sqlite/src/renderer/index.html +57 -0
  45. package/templates/sqlite/src/renderer/plugin.css +275 -0
  46. package/templates/sqlite/tsconfig.main.json +15 -0
  47. package/templates/sqlite/tsconfig.renderer.json +17 -0
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # @aiot-nexus/plugin-sdk
2
+
3
+ AIoT Nexus 独立插件开发包,包含公开 TypeScript 契约、PluginFrame 浏览器客户端和 `nexus-plugin` 命令行工具。
4
+
5
+ ## 环境
6
+
7
+ - Node.js `>=16.20.0 <17`
8
+ - npm `>=6.13.4`
9
+ - 插件框架 API `1.0.0`
10
+ - manifest schema `1`
11
+
12
+ ## 命令
13
+
14
+ ```bash
15
+ npm run build
16
+ npm test
17
+ npm run pack:sdk
18
+
19
+ nexus-plugin info
20
+ nexus-plugin validate <plugin-package-directory>
21
+ nexus-plugin create <plugin-id> --template minimal|sqlite --output <parent-directory>
22
+ nexus-plugin dev [plugin-project-directory]
23
+ ```
24
+
25
+ `npm run pack:sdk` 将离线安装包写入 `package/`。该目录是本地构建产物,不进入 Git。
26
+
27
+ ## 创建插件
28
+
29
+ ```bash
30
+ nexus-plugin create my-frame-plugin \
31
+ --template minimal \
32
+ --output . \
33
+ --display-name "My Frame Plugin" \
34
+ --sdk-source /path/to/aiot-nexus-plugin-sdk-1.0.0.tgz
35
+ ```
36
+
37
+ - `--template` 默认为 `minimal`,也可选择带 `better-sqlite3` 的 `sqlite`。
38
+ - `--output` 默认为当前目录,目标固定为 `<output>/<plugin-id>`。
39
+ - `--sdk-source` 为必填项,接受严格的 `X.Y.Z` npm 版本、`@aiot-nexus/plugin-sdk@X.Y.Z` 或本地 `.tgz`。本地包会复制到生成工程的 `vendor/`,并写入可迁移的相对 `file:` 依赖。
40
+ - 默认不安装依赖;传入 `--install` 后执行 `npm install`。SQLite 模板在每次 `npm run build` 前强制执行 Electron 22.3.27 native rebuild。
41
+ - 目标目录非空时命令以返回码 `2` 拒绝覆盖。生成或安装失败返回 `1`。
42
+ - 两套模板均读取宿主 `PluginRendererContext.language`,通过 `onContextChanged` 在同一 iframe 内响应运行时语言切换,并内置 `zh-CN` / `en-US` renderer 文案;manifest 使用相同文化资源本地化宿主入口。
43
+
44
+ 当前 SDK 不发布到远程 registry。必须先执行 `npm run pack:sdk`,再把生成的 `.tgz` 路径传给 `--sdk-source`;CLI 不会默认写入当前无法安装的远程版本,并会把本地包一并放入生成工程。未来发布后可改用严格的 `X.Y.Z` 版本。
45
+
46
+ ## 独立调试
47
+
48
+ 生成工程安装依赖后,可以完全在插件目录启动调试宿主:
49
+
50
+ ```bash
51
+ npm run dev
52
+ npm run dev:debug
53
+ ```
54
+
55
+ - `npm run dev` 先构建和校验插件,再使用工程本地固定的 `electron@22.3.27` 启动 SDK 调试宿主;Renderer DevTools 默认自动打开。
56
+ - `npm run dev:debug` 额外使用 `--inspect-brk=9229` 暂停 Electron 主进程,可在 `src/main.ts` 启动阶段命中断点。
57
+ - 生成工程包含 `.vscode/launch.json`。在 VS Code 中选择 `Debug Nexus plugin` 可直接启动;若本机关闭了子进程自动附加,则先运行 `npm run dev:debug`,再选择 `Attach Nexus plugin main (9229)`。renderer 断点在自动打开的 DevTools `Sources` 中设置。
58
+ - 调试构建会内联 `src/main.ts` 和 `src/renderer/app.ts` source map。普通 `npm run build` 不内联源码,不改变部署制品形态。
59
+ - 调试数据保存在工程的 `.nexus-plugin-dev/`,该目录默认被忽略。关闭调试窗口会结束独立 Electron 进程;修改源码后重新运行调试命令。
60
+
61
+ 可直接使用的 CLI 选项:
62
+
63
+ ```text
64
+ nexus-plugin dev [plugin-project-directory]
65
+ [--no-build]
66
+ [--inspect[=<port>] | --inspect-brk[=<port>]]
67
+ [--language <culture>]
68
+ [--theme system|light|dark]
69
+ [--surface business|tool|settings]
70
+ [--contribution-id <id>]
71
+ [--no-devtools]
72
+ ```
73
+
74
+ 调试宿主复用正式的 `nexus-plugin://` 资源协议、iframe sandbox、CSP、消息结构、operation 校验以及当前 Host API 权限语义,但不包含 Nexus 的完整页面路由、Pinia 状态和应用级功能。交付前仍需把 `dist/<plugin-id>/` 放入正式 Nexus 完成集成验证。
@@ -0,0 +1,344 @@
1
+ <!doctype html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Nexus Plugin Dev</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ font-family: "Segoe UI", Arial, sans-serif;
11
+ letter-spacing: 0;
12
+ --page: #f4f5f7;
13
+ --surface: #ffffff;
14
+ --border: #cfd4dc;
15
+ --text: #20242a;
16
+ --muted: #5d6672;
17
+ --accent: #167451;
18
+ }
19
+
20
+ :root[data-theme="dark"] {
21
+ color-scheme: dark;
22
+ --page: #181a1d;
23
+ --surface: #22252a;
24
+ --border: #3e444d;
25
+ --text: #f2f4f7;
26
+ --muted: #aeb6c2;
27
+ --accent: #5fd0a2;
28
+ }
29
+
30
+ * {
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ html,
35
+ body {
36
+ width: 100%;
37
+ min-width: 0;
38
+ min-height: 100%;
39
+ margin: 0;
40
+ background: var(--page);
41
+ color: var(--text);
42
+ }
43
+
44
+ body {
45
+ display: flex;
46
+ flex-direction: column;
47
+ }
48
+
49
+ .toolbar {
50
+ position: sticky;
51
+ top: 0;
52
+ z-index: 1;
53
+ display: flex;
54
+ align-items: center;
55
+ min-height: 48px;
56
+ gap: 12px;
57
+ padding: 7px 12px;
58
+ border-bottom: 1px solid var(--border);
59
+ background: var(--surface);
60
+ }
61
+
62
+ .plugin-name {
63
+ min-width: 120px;
64
+ margin-right: auto;
65
+ overflow: hidden;
66
+ text-overflow: ellipsis;
67
+ white-space: nowrap;
68
+ font-size: 14px;
69
+ }
70
+
71
+ .control {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 6px;
75
+ color: var(--muted);
76
+ font-size: 12px;
77
+ }
78
+
79
+ select,
80
+ button {
81
+ min-height: 30px;
82
+ border: 1px solid var(--border);
83
+ border-radius: 4px;
84
+ background: var(--surface);
85
+ color: var(--text);
86
+ font: inherit;
87
+ }
88
+
89
+ select {
90
+ min-width: 104px;
91
+ padding: 3px 26px 3px 8px;
92
+ }
93
+
94
+ button {
95
+ padding: 3px 10px;
96
+ cursor: pointer;
97
+ }
98
+
99
+ button:hover {
100
+ border-color: var(--accent);
101
+ }
102
+
103
+ .status {
104
+ width: 86px;
105
+ overflow: hidden;
106
+ color: var(--muted);
107
+ font-size: 12px;
108
+ text-align: right;
109
+ text-overflow: ellipsis;
110
+ white-space: nowrap;
111
+ }
112
+
113
+ .status[data-connected="true"] {
114
+ color: var(--accent);
115
+ }
116
+
117
+ .frame-shell {
118
+ flex: 1 1 auto;
119
+ min-width: 0;
120
+ padding: 12px;
121
+ overflow: auto;
122
+ }
123
+
124
+ iframe {
125
+ display: block;
126
+ width: 100%;
127
+ min-height: 360px;
128
+ border: 1px solid var(--border);
129
+ background: var(--surface);
130
+ }
131
+
132
+ @media (max-width: 760px) {
133
+ .toolbar {
134
+ align-items: stretch;
135
+ flex-wrap: wrap;
136
+ }
137
+
138
+ .plugin-name {
139
+ flex-basis: 100%;
140
+ width: 100%;
141
+ }
142
+
143
+ .status {
144
+ margin-left: auto;
145
+ }
146
+
147
+ .frame-shell {
148
+ padding: 8px;
149
+ }
150
+ }
151
+ </style>
152
+ </head>
153
+ <body>
154
+ <header class="toolbar">
155
+ <strong id="pluginName" class="plugin-name">Nexus Plugin Dev</strong>
156
+ <label class="control" for="languageSelect">
157
+ <span>Language</span>
158
+ <select id="languageSelect"></select>
159
+ </label>
160
+ <label class="control" for="themeSelect">
161
+ <span>Theme</span>
162
+ <select id="themeSelect">
163
+ <option value="system">System</option>
164
+ <option value="light">Light</option>
165
+ <option value="dark">Dark</option>
166
+ </select>
167
+ </label>
168
+ <button id="reloadButton" type="button">Reload frame</button>
169
+ <span id="status" class="status" data-connected="false">Waiting</span>
170
+ </header>
171
+ <main class="frame-shell">
172
+ <iframe id="pluginFrame" sandbox="allow-scripts allow-same-origin" title="Plugin renderer"></iframe>
173
+ </main>
174
+ <script>
175
+ "use strict";
176
+
177
+ var api = window.nexusPluginDevHost;
178
+ var iframe = document.getElementById("pluginFrame");
179
+ var pluginName = document.getElementById("pluginName");
180
+ var languageSelect = document.getElementById("languageSelect");
181
+ var themeSelect = document.getElementById("themeSelect");
182
+ var reloadButton = document.getElementById("reloadButton");
183
+ var status = document.getElementById("status");
184
+ var configuration;
185
+ var context;
186
+ var initialized = false;
187
+ var smokeReported = false;
188
+ var resizeSeen = false;
189
+ var pendingRequests = 0;
190
+ var smokeTimer;
191
+
192
+ window.addEventListener("message", handleMessage);
193
+ languageSelect.addEventListener("change", updateContext);
194
+ themeSelect.addEventListener("change", updateContext);
195
+ reloadButton.addEventListener("click", reloadFrame);
196
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyTheme);
197
+
198
+ api.getConfig().then(function initialize(value) {
199
+ configuration = value;
200
+ context = value.context;
201
+ document.title = value.pluginId + " - Nexus Plugin Dev";
202
+ pluginName.textContent = value.pluginId;
203
+ iframe.title = value.pluginId;
204
+ populateLanguages(value.supportedCultures, context.language);
205
+ themeSelect.value = context.themeMode;
206
+ applyTheme();
207
+ iframe.src = value.frameUrl;
208
+ }).catch(function reportInitializationFailure(error) {
209
+ setStatus("Host error", false);
210
+ console.error(error);
211
+ });
212
+
213
+ function handleMessage(event) {
214
+ if (!configuration || event.source !== iframe.contentWindow || event.origin !== configuration.frameOrigin) {
215
+ return;
216
+ }
217
+ var message = event.data;
218
+ if (!message || typeof message !== "object") {
219
+ return;
220
+ }
221
+ if (message.type === "nexus-plugin:ready") {
222
+ initialized = true;
223
+ postToFrame({ type: "nexus-plugin:init", context: context });
224
+ setStatus("Connected", true);
225
+ return;
226
+ }
227
+ if (message.type === "nexus-plugin:resize" && typeof message.height === "number") {
228
+ iframe.style.height = Math.min(6000, Math.max(160, Math.ceil(message.height))) + "px";
229
+ resizeSeen = true;
230
+ scheduleSmokeReport();
231
+ return;
232
+ }
233
+ if (message.type !== "nexus-plugin:request" || typeof message.requestId !== "string" || typeof message.method !== "string") {
234
+ return;
235
+ }
236
+ if (message.method === "getContext") {
237
+ respond(message.requestId, { ok: true, value: context });
238
+ return;
239
+ }
240
+ pendingRequests += 1;
241
+ cancelSmokeReport();
242
+ api.invoke({ method: message.method, params: message.params }).then(function sendResult(result) {
243
+ respond(message.requestId, result);
244
+ }, function sendFailure(error) {
245
+ respond(message.requestId, {
246
+ ok: false,
247
+ error: { code: "UnexpectedFailure", message: error instanceof Error ? error.message : String(error) }
248
+ });
249
+ }).then(function finishRequest() {
250
+ pendingRequests -= 1;
251
+ scheduleSmokeReport();
252
+ });
253
+ }
254
+
255
+ function updateContext() {
256
+ if (!context) {
257
+ return;
258
+ }
259
+ context = Object.assign({}, context, {
260
+ language: languageSelect.value,
261
+ themeMode: themeSelect.value
262
+ });
263
+ applyTheme();
264
+ if (initialized) {
265
+ postToFrame({ type: "nexus-plugin:context-changed", context: context });
266
+ }
267
+ }
268
+
269
+ function reloadFrame() {
270
+ if (!configuration) {
271
+ return;
272
+ }
273
+ initialized = false;
274
+ smokeReported = false;
275
+ resizeSeen = false;
276
+ pendingRequests = 0;
277
+ cancelSmokeReport();
278
+ setStatus("Waiting", false);
279
+ iframe.src = "about:blank";
280
+ window.setTimeout(function restoreFrame() {
281
+ iframe.src = configuration.frameUrl;
282
+ }, 0);
283
+ }
284
+
285
+ function populateLanguages(cultures, selected) {
286
+ languageSelect.replaceChildren();
287
+ var values = Array.isArray(cultures) && cultures.length > 0 ? cultures : [selected];
288
+ values.forEach(function appendCulture(culture) {
289
+ var option = document.createElement("option");
290
+ option.value = culture;
291
+ option.textContent = culture;
292
+ languageSelect.appendChild(option);
293
+ });
294
+ if (!values.includes(selected)) {
295
+ var option = document.createElement("option");
296
+ option.value = selected;
297
+ option.textContent = selected;
298
+ languageSelect.appendChild(option);
299
+ }
300
+ languageSelect.value = selected;
301
+ }
302
+
303
+ function applyTheme() {
304
+ var mode = context ? context.themeMode : "system";
305
+ var dark = mode === "dark" || (mode === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
306
+ document.documentElement.dataset.theme = dark ? "dark" : "light";
307
+ }
308
+
309
+ function respond(requestId, result) {
310
+ postToFrame({ type: "nexus-plugin:response", requestId: requestId, result: result });
311
+ }
312
+
313
+ function postToFrame(message) {
314
+ iframe.contentWindow && iframe.contentWindow.postMessage(message, configuration.frameOrigin);
315
+ }
316
+
317
+ function setStatus(text, connected) {
318
+ status.textContent = text;
319
+ status.dataset.connected = connected ? "true" : "false";
320
+ }
321
+
322
+ function scheduleSmokeReport() {
323
+ if (!configuration || !configuration.smoke || !initialized || !resizeSeen || smokeReported || pendingRequests !== 0) {
324
+ return;
325
+ }
326
+ cancelSmokeReport();
327
+ smokeTimer = window.setTimeout(function reportStableFrame() {
328
+ smokeTimer = undefined;
329
+ if (!smokeReported && pendingRequests === 0) {
330
+ smokeReported = true;
331
+ api.notifyFrameReady();
332
+ }
333
+ }, 300);
334
+ }
335
+
336
+ function cancelSmokeReport() {
337
+ if (smokeTimer !== undefined) {
338
+ window.clearTimeout(smokeTimer);
339
+ smokeTimer = undefined;
340
+ }
341
+ }
342
+ </script>
343
+ </body>
344
+ </html>