@aiot-nexus/plugin-sdk 1.0.1 → 1.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 +33 -5
- package/dev-host/host.html +82 -15
- package/dev-host/main.js +98 -8
- package/dist/dev-host-metadata.json +3 -0
- package/dist/src/cli/run.d.ts +1 -1
- package/dist/src/cli/run.js +197 -3
- package/dist/src/cli.js +10 -1
- package/dist/src/dev.d.ts +21 -9
- package/dist/src/dev.js +100 -19
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/nexus-dev-control.d.ts +42 -0
- package/dist/src/nexus-dev-control.js +401 -0
- package/dist/src/nexus-dev-deployment.d.ts +9 -0
- package/dist/src/nexus-dev-deployment.js +195 -0
- package/dist/src/nexus-dev.d.ts +21 -0
- package/dist/src/nexus-dev.js +569 -0
- package/dist/src/scaffold.js +3 -0
- package/dist/src/validation.d.ts +4 -1
- package/dist/src/validation.js +8 -6
- package/dist/src/web-dev.d.ts +11 -0
- package/dist/src/web-dev.js +379 -0
- package/package.json +6 -3
- package/templates/minimal/README.md.tmpl +8 -2
- package/templates/minimal/scripts/build.js.tmpl +21 -4
- package/templates/minimal/src/renderer/app.ts.tmpl +37 -4
- package/templates/minimal/src/renderer/index.html +2 -2
- package/templates/minimal/src/renderer/plugin.css +34 -27
- package/templates/sqlite/README.md.tmpl +8 -2
- package/templates/sqlite/scripts/build.js.tmpl +28 -7
- package/templates/sqlite/src/renderer/app.ts.tmpl +37 -4
- package/templates/sqlite/src/renderer/index.html +2 -2
- package/templates/sqlite/src/renderer/plugin.css +52 -38
package/README.md
CHANGED
|
@@ -15,14 +15,17 @@ AIoT Nexus 独立插件开发包,包含公开 TypeScript 契约、PluginFrame
|
|
|
15
15
|
npm run build
|
|
16
16
|
npm test
|
|
17
17
|
npm run pack:sdk
|
|
18
|
+
npm run publish:sdk
|
|
18
19
|
|
|
19
20
|
nexus-plugin info
|
|
20
21
|
nexus-plugin validate <plugin-package-directory>
|
|
21
22
|
nexus-plugin create <plugin-id> --template minimal|sqlite --output <parent-directory>
|
|
22
23
|
nexus-plugin dev [plugin-project-directory]
|
|
24
|
+
nexus-plugin dev:web [plugin-project-directory]
|
|
25
|
+
nexus-plugin dev:nexus [plugin-project-directory] --host <nexus.exe-or-directory>
|
|
23
26
|
```
|
|
24
27
|
|
|
25
|
-
`npm run pack:sdk` 将离线安装包写入 `package/`。该目录是本地构建产物,不进入 Git
|
|
28
|
+
`npm run pack:sdk` 将离线安装包写入 `package/`。该目录是本地构建产物,不进入 Git。`npm run publish:sdk` 发布到官方 npm registry;`prepack` 会强制运行测试、构建并核对实际 npm 文件清单,缺少任一公开入口时拒绝发布。
|
|
26
29
|
|
|
27
30
|
## 创建插件
|
|
28
31
|
|
|
@@ -39,23 +42,27 @@ nexus-plugin create my-frame-plugin \
|
|
|
39
42
|
- `--sdk-source` 默认为当前 CLI 的 SDK 版本。也可传入严格的 `X.Y.Z` npm 版本、`@aiot-nexus/plugin-sdk@X.Y.Z` 或本地 `.tgz` 进行覆盖;本地包会复制到生成工程的 `vendor/`,并写入可迁移的相对 `file:` 依赖。
|
|
40
43
|
- 默认不安装依赖;传入 `--install` 后执行 `npm install`。SQLite 模板在每次 `npm run build` 前强制执行 Electron 22.3.27 native rebuild。
|
|
41
44
|
- 目标目录非空时命令以返回码 `2` 拒绝覆盖。生成或安装失败返回 `1`。
|
|
42
|
-
- 两套模板均读取宿主 `PluginRendererContext.language`,通过 `onContextChanged` 在同一 iframe
|
|
45
|
+
- 两套模板均读取宿主 `PluginRendererContext.language` 和 `themeMode`,通过 `onContextChanged` 在同一 iframe 内响应运行时语言与主题切换,并内置 `zh-CN` / `en-US` renderer 文案;manifest 使用相同文化资源本地化宿主入口。模板页面自身不提供语言或主题选择控件。
|
|
43
46
|
|
|
44
|
-
默认来源从 npm registry 安装。离线开发时先执行 `npm run pack:sdk`,再通过 `--sdk-source /path/to/aiot-nexus-plugin-sdk-1.0.
|
|
47
|
+
默认来源从 npm registry 安装。离线开发时先执行 `npm run pack:sdk`,再通过 `--sdk-source /path/to/aiot-nexus-plugin-sdk-1.1.0.tgz` 指定本地包;CLI 会把本地包一并放入生成工程。
|
|
45
48
|
|
|
46
49
|
## 独立调试
|
|
47
50
|
|
|
48
51
|
生成工程安装依赖后,可以完全在插件目录启动调试宿主:
|
|
49
52
|
|
|
50
53
|
```bash
|
|
54
|
+
npm run dev:web
|
|
51
55
|
npm run dev
|
|
52
56
|
npm run dev:debug
|
|
53
57
|
```
|
|
54
58
|
|
|
59
|
+
- `npm run dev:web` 先运行模板独立的 `build:web`,只编译 Renderer 到 `.nexus-plugin-dev/web-package/<plugin-id>/`,再启动仅绑定 `127.0.0.1:4173` 的本地 HTTP 宿主并打开系统浏览器;它不执行 native rebuild、不复制原生运行时、不覆盖正式 `dist`,支持浏览器 DevTools 与源码映射。
|
|
60
|
+
- 浏览器模式不加载 `mainEntry`,`invokeMain` 会返回 `PluginCapabilityUnsupported`;设置仅在本次服务进程内保存,日志输出到 CLI。涉及主进程、SQLite 或 Host API 时使用 `npm run dev`。
|
|
55
61
|
- `npm run dev` 先构建和校验插件,再使用工程本地固定的 `electron@22.3.27` 启动 SDK 调试宿主;Renderer DevTools 默认自动打开。
|
|
62
|
+
- Electron 调试宿主的 `host.hostVersion` 默认是 SDK 构建时记录的 Nexus 应用版本;使用 `nexus-plugin dev . --host-version <X.Y.Z>` 可模拟其他应用版本,并按 manifest `minHostVersion` 拒绝不兼容组合。
|
|
56
63
|
- `npm run dev:debug` 额外使用 `--inspect-brk=9229` 暂停 Electron 主进程,可在 `src/main.ts` 启动阶段命中断点。
|
|
57
64
|
- 生成工程包含 `.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`
|
|
65
|
+
- Electron 调试构建会内联 `src/main.ts` 和 `src/renderer/app.ts` source map;浏览器 `build:web` 只内联 Renderer source map。普通 `npm run build` 不内联源码,不改变部署制品形态。模板在收到首个 Host context 前隐藏正文,应用语言和主题后再显示。
|
|
59
66
|
- 调试数据保存在工程的 `.nexus-plugin-dev/`,该目录默认被忽略。关闭调试窗口会结束独立 Electron 进程;修改源码后重新运行调试命令。
|
|
60
67
|
|
|
61
68
|
可直接使用的 CLI 选项:
|
|
@@ -64,6 +71,7 @@ npm run dev:debug
|
|
|
64
71
|
nexus-plugin dev [plugin-project-directory]
|
|
65
72
|
[--no-build]
|
|
66
73
|
[--inspect[=<port>] | --inspect-brk[=<port>]]
|
|
74
|
+
[--host-version <X.Y.Z>]
|
|
67
75
|
[--language <culture>]
|
|
68
76
|
[--theme system|light|dark]
|
|
69
77
|
[--surface business|tool|settings]
|
|
@@ -71,4 +79,24 @@ nexus-plugin dev [plugin-project-directory]
|
|
|
71
79
|
[--no-devtools]
|
|
72
80
|
```
|
|
73
81
|
|
|
74
|
-
|
|
82
|
+
调试宿主工具栏中的语言和主题选择器用于模拟 Nexus 主框架 context,不属于插件页面。切换后调试宿主向同一 iframe 重发框架 `1.0.0` 已有的 `init` 消息;SDK 将后续 `init` 作为 context 更新并原地刷新语言和主题,同时仍接受可选的 `context-changed` 消息。调试宿主还复用正式的 `nexus-plugin://` 资源协议、iframe sandbox、CSP、消息结构、operation 校验以及当前 Host API 权限语义,但不包含 Nexus 的完整页面路由、Pinia 状态和应用级功能。交付前仍需把 `dist/<plugin-id>/` 放入正式 Nexus 完成集成验证。
|
|
83
|
+
|
|
84
|
+
浏览器 UI 命令支持 `--no-build`、`--no-open`、`--port <port>`,以及与 Electron 调试宿主相同的 `--language`、`--theme`、`--surface` 和 `--contribution-id` 参数。它使用普通 `http://127.0.0.1` iframe,不模拟 `nexus-plugin://` 协议或 Electron sandbox,因此只作为 UI 开发入口。Renderer-only 输出不会覆盖正式插件制品。
|
|
85
|
+
|
|
86
|
+
## 使用正式 Nexus 调试
|
|
87
|
+
|
|
88
|
+
已经安装并构建插件后,可直接把编译好的正式 Nexus 作为宿主:
|
|
89
|
+
|
|
90
|
+
```powershell
|
|
91
|
+
npm run dev:nexus -- --host D:\prj\tmp\aiot-nexus\dist\electron\nexus-win32-x64\nexus.exe
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`dev:nexus` 首次启动会完成正式构建,通过 staging 原子部署到 `<exeDir>/Plugins/<pluginId>/`,再由 CLI 以 `--plugin-dev`、随机控制管道和会话令牌启动 Nexus。开发通道只在这次由 CLI 管理的进程中启用;普通双击或不带 `--plugin-dev` 启动时不会创建控制管道,也不会打开 DevTools。
|
|
95
|
+
|
|
96
|
+
| 修改范围 | 行为 |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `src/renderer/**` | 只运行 `build:web` 并替换 renderer;宿主 PID 不变,iframe 重新完成 `ready/init`。 |
|
|
99
|
+
| `src/manifest.json` 的展示、贡献点等安全字段 | 构建 renderer、原子替换 manifest 后重扫插件目录;宿主 PID 不变。 |
|
|
100
|
+
| `src/main.ts`、依赖、原生模块或主进程相关 manifest 字段 | 完整构建成功后请求旧宿主退出,替换完整插件包并启动新宿主;宿主 PID 变化。 |
|
|
101
|
+
|
|
102
|
+
更新构建或 manifest 重扫失败时保留上一个可运行部署;新宿主启动失败时回滚插件包并恢复旧版本。`mainEntry` 不通过清理 `require.cache` 热替换,主进程变化始终使用完整重启。默认打开 Renderer DevTools;可传 `--no-devtools`。主进程调试支持 `--inspect[=<port>]` 和 `--inspect-brk[=<port>]`,贡献点选择支持 `--surface` 与 `--contribution-id`。按 `Ctrl+C` 停止 CLI 时,CLI 会关闭其启动的正式宿主。
|
package/dev-host/host.html
CHANGED
|
@@ -154,18 +154,18 @@
|
|
|
154
154
|
<header class="toolbar">
|
|
155
155
|
<strong id="pluginName" class="plugin-name">Nexus Plugin Dev</strong>
|
|
156
156
|
<label class="control" for="languageSelect">
|
|
157
|
-
<span>Language</span>
|
|
157
|
+
<span data-host-i18n="control.language">Language</span>
|
|
158
158
|
<select id="languageSelect"></select>
|
|
159
159
|
</label>
|
|
160
160
|
<label class="control" for="themeSelect">
|
|
161
|
-
<span>Theme</span>
|
|
161
|
+
<span data-host-i18n="control.theme">Theme</span>
|
|
162
162
|
<select id="themeSelect">
|
|
163
|
-
<option value="system">System</option>
|
|
164
|
-
<option value="light">Light</option>
|
|
165
|
-
<option value="dark">Dark</option>
|
|
163
|
+
<option value="system" data-host-i18n="theme.system">System</option>
|
|
164
|
+
<option value="light" data-host-i18n="theme.light">Light</option>
|
|
165
|
+
<option value="dark" data-host-i18n="theme.dark">Dark</option>
|
|
166
166
|
</select>
|
|
167
167
|
</label>
|
|
168
|
-
<button id="reloadButton" type="button">Reload frame</button>
|
|
168
|
+
<button id="reloadButton" type="button" data-host-i18n="action.reload">Reload frame</button>
|
|
169
169
|
<span id="status" class="status" data-connected="false">Waiting</span>
|
|
170
170
|
</header>
|
|
171
171
|
<main class="frame-shell">
|
|
@@ -174,13 +174,13 @@
|
|
|
174
174
|
<script>
|
|
175
175
|
"use strict";
|
|
176
176
|
|
|
177
|
-
var api = window.nexusPluginDevHost;
|
|
177
|
+
var api = window.nexusPluginDevHost || createBrowserHostApi();
|
|
178
178
|
var iframe = document.getElementById("pluginFrame");
|
|
179
179
|
var pluginName = document.getElementById("pluginName");
|
|
180
180
|
var languageSelect = document.getElementById("languageSelect");
|
|
181
181
|
var themeSelect = document.getElementById("themeSelect");
|
|
182
182
|
var reloadButton = document.getElementById("reloadButton");
|
|
183
|
-
var
|
|
183
|
+
var statusOutput = document.getElementById("status");
|
|
184
184
|
var configuration;
|
|
185
185
|
var context;
|
|
186
186
|
var initialized = false;
|
|
@@ -188,6 +188,32 @@
|
|
|
188
188
|
var resizeSeen = false;
|
|
189
189
|
var pendingRequests = 0;
|
|
190
190
|
var smokeTimer;
|
|
191
|
+
var statusKey = "status.waiting";
|
|
192
|
+
var statusConnected = false;
|
|
193
|
+
var hostMessages = {
|
|
194
|
+
"en-US": {
|
|
195
|
+
"control.language": "Language",
|
|
196
|
+
"control.theme": "Theme",
|
|
197
|
+
"theme.system": "System",
|
|
198
|
+
"theme.light": "Light",
|
|
199
|
+
"theme.dark": "Dark",
|
|
200
|
+
"action.reload": "Reload frame",
|
|
201
|
+
"status.waiting": "Waiting",
|
|
202
|
+
"status.connected": "Connected",
|
|
203
|
+
"status.hostError": "Host error"
|
|
204
|
+
},
|
|
205
|
+
"zh-CN": {
|
|
206
|
+
"control.language": "语言",
|
|
207
|
+
"control.theme": "主题",
|
|
208
|
+
"theme.system": "跟随系统",
|
|
209
|
+
"theme.light": "浅色",
|
|
210
|
+
"theme.dark": "深色",
|
|
211
|
+
"action.reload": "重新加载",
|
|
212
|
+
"status.waiting": "等待中",
|
|
213
|
+
"status.connected": "已连接",
|
|
214
|
+
"status.hostError": "宿主错误"
|
|
215
|
+
}
|
|
216
|
+
};
|
|
191
217
|
|
|
192
218
|
window.addEventListener("message", handleMessage);
|
|
193
219
|
languageSelect.addEventListener("change", updateContext);
|
|
@@ -203,10 +229,11 @@
|
|
|
203
229
|
iframe.title = value.pluginId;
|
|
204
230
|
populateLanguages(value.supportedCultures, context.language);
|
|
205
231
|
themeSelect.value = context.themeMode;
|
|
232
|
+
applyHostLanguage();
|
|
206
233
|
applyTheme();
|
|
207
234
|
iframe.src = value.frameUrl;
|
|
208
235
|
}).catch(function reportInitializationFailure(error) {
|
|
209
|
-
setStatus("
|
|
236
|
+
setStatus("status.hostError", false);
|
|
210
237
|
console.error(error);
|
|
211
238
|
});
|
|
212
239
|
|
|
@@ -221,7 +248,7 @@
|
|
|
221
248
|
if (message.type === "nexus-plugin:ready") {
|
|
222
249
|
initialized = true;
|
|
223
250
|
postToFrame({ type: "nexus-plugin:init", context: context });
|
|
224
|
-
setStatus("
|
|
251
|
+
setStatus("status.connected", true);
|
|
225
252
|
return;
|
|
226
253
|
}
|
|
227
254
|
if (message.type === "nexus-plugin:resize" && typeof message.height === "number") {
|
|
@@ -260,9 +287,10 @@
|
|
|
260
287
|
language: languageSelect.value,
|
|
261
288
|
themeMode: themeSelect.value
|
|
262
289
|
});
|
|
290
|
+
applyHostLanguage();
|
|
263
291
|
applyTheme();
|
|
264
292
|
if (initialized) {
|
|
265
|
-
postToFrame({ type: "nexus-plugin:
|
|
293
|
+
postToFrame({ type: "nexus-plugin:init", context: context });
|
|
266
294
|
}
|
|
267
295
|
}
|
|
268
296
|
|
|
@@ -275,7 +303,7 @@
|
|
|
275
303
|
resizeSeen = false;
|
|
276
304
|
pendingRequests = 0;
|
|
277
305
|
cancelSmokeReport();
|
|
278
|
-
setStatus("
|
|
306
|
+
setStatus("status.waiting", false);
|
|
279
307
|
iframe.src = "about:blank";
|
|
280
308
|
window.setTimeout(function restoreFrame() {
|
|
281
309
|
iframe.src = configuration.frameUrl;
|
|
@@ -306,6 +334,15 @@
|
|
|
306
334
|
document.documentElement.dataset.theme = dark ? "dark" : "light";
|
|
307
335
|
}
|
|
308
336
|
|
|
337
|
+
function applyHostLanguage() {
|
|
338
|
+
var language = context && String(context.language).toLowerCase().startsWith("zh") ? "zh-CN" : "en-US";
|
|
339
|
+
document.documentElement.lang = language;
|
|
340
|
+
document.querySelectorAll("[data-host-i18n]").forEach(function localize(element) {
|
|
341
|
+
element.textContent = hostMessages[language][element.dataset.hostI18n] || element.textContent;
|
|
342
|
+
});
|
|
343
|
+
statusOutput.textContent = hostMessages[language][statusKey];
|
|
344
|
+
}
|
|
345
|
+
|
|
309
346
|
function respond(requestId, result) {
|
|
310
347
|
postToFrame({ type: "nexus-plugin:response", requestId: requestId, result: result });
|
|
311
348
|
}
|
|
@@ -314,9 +351,39 @@
|
|
|
314
351
|
iframe.contentWindow && iframe.contentWindow.postMessage(message, configuration.frameOrigin);
|
|
315
352
|
}
|
|
316
353
|
|
|
317
|
-
function setStatus(
|
|
318
|
-
|
|
319
|
-
|
|
354
|
+
function setStatus(key, connected) {
|
|
355
|
+
statusKey = key;
|
|
356
|
+
statusConnected = connected;
|
|
357
|
+
var language = context && String(context.language).toLowerCase().startsWith("zh") ? "zh-CN" : "en-US";
|
|
358
|
+
statusOutput.textContent = hostMessages[language][statusKey];
|
|
359
|
+
statusOutput.dataset.connected = statusConnected ? "true" : "false";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function createBrowserHostApi() {
|
|
363
|
+
return {
|
|
364
|
+
getConfig: function getConfig() {
|
|
365
|
+
return requestBrowserHost("/__nexus_plugin_dev/config");
|
|
366
|
+
},
|
|
367
|
+
invoke: function invoke(request) {
|
|
368
|
+
return requestBrowserHost("/__nexus_plugin_dev/invoke", {
|
|
369
|
+
method: "POST",
|
|
370
|
+
headers: { "Content-Type": "application/json" },
|
|
371
|
+
body: JSON.stringify(request)
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
notifyFrameReady: function notifyFrameReady() {}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function requestBrowserHost(url, options) {
|
|
379
|
+
return window.fetch(url, Object.assign({ credentials: "same-origin" }, options)).then(function parseResponse(response) {
|
|
380
|
+
return response.json().then(function handleBody(body) {
|
|
381
|
+
if (!response.ok) {
|
|
382
|
+
throw new Error(body && body.error ? body.error : "Browser host request failed.");
|
|
383
|
+
}
|
|
384
|
+
return body;
|
|
385
|
+
});
|
|
386
|
+
});
|
|
320
387
|
}
|
|
321
388
|
|
|
322
389
|
function scheduleSmokeReport() {
|
package/dev-host/main.js
CHANGED
|
@@ -43,6 +43,7 @@ app.setName("Nexus Plugin Dev: " + configuration.manifest.pluginId);
|
|
|
43
43
|
|
|
44
44
|
let mainWindow;
|
|
45
45
|
let smokeTimeout;
|
|
46
|
+
let smokeVerificationRunning = false;
|
|
46
47
|
|
|
47
48
|
app.whenReady().then(function startDevHost() {
|
|
48
49
|
registerPluginRendererProtocol();
|
|
@@ -69,6 +70,10 @@ function loadConfiguration(args) {
|
|
|
69
70
|
const contributionId = requiredArgument(args, "--contribution-id");
|
|
70
71
|
const language = requiredArgument(args, "--language");
|
|
71
72
|
const themeMode = requiredArgument(args, "--theme");
|
|
73
|
+
const hostVersion = requiredArgument(args, "--host-version");
|
|
74
|
+
if (!isStrictVersion(hostVersion)) {
|
|
75
|
+
throw new Error("--host-version must use X.Y.Z format.");
|
|
76
|
+
}
|
|
72
77
|
if (!hasContribution(manifest, surface, contributionId)) {
|
|
73
78
|
throw new Error("Selected PluginFrame contribution is not declared: " + surface + ":" + contributionId);
|
|
74
79
|
}
|
|
@@ -84,6 +89,7 @@ function loadConfiguration(args) {
|
|
|
84
89
|
contributionId,
|
|
85
90
|
language,
|
|
86
91
|
themeMode,
|
|
92
|
+
hostVersion,
|
|
87
93
|
openDevTools: !args.includes("--no-devtools"),
|
|
88
94
|
smoke: args.includes("--smoke"),
|
|
89
95
|
devDataDirectory,
|
|
@@ -178,7 +184,7 @@ function activatePluginMain() {
|
|
|
178
184
|
function createHostApi() {
|
|
179
185
|
const manifest = configuration.manifest;
|
|
180
186
|
return {
|
|
181
|
-
hostVersion:
|
|
187
|
+
hostVersion: configuration.hostVersion,
|
|
182
188
|
hostApiVersion: sdk.PLUGIN_FRAMEWORK_VERSION,
|
|
183
189
|
pluginId: manifest.pluginId,
|
|
184
190
|
pluginDataDirectory: configuration.pluginDataDirectory,
|
|
@@ -291,16 +297,93 @@ function registerIpcHandlers() {
|
|
|
291
297
|
});
|
|
292
298
|
ipcMain.on(ipcChannels.frameReady, function frameReady(event) {
|
|
293
299
|
assertMainWindowSender(event);
|
|
294
|
-
if (configuration.smoke) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
+
if (configuration.smoke && !smokeVerificationRunning) {
|
|
301
|
+
smokeVerificationRunning = true;
|
|
302
|
+
void verifyRendererContextSmoke().then(function reportSmokeSuccess() {
|
|
303
|
+
clearTimeout(smokeTimeout);
|
|
304
|
+
process.stdout.write("[nexus-plugin-dev] renderer context synchronization smoke passed.\n");
|
|
305
|
+
setTimeout(function exitAfterSmokeSettles() {
|
|
306
|
+
app.exit(0);
|
|
307
|
+
}, 100);
|
|
308
|
+
}).catch(function reportSmokeFailure(error) {
|
|
309
|
+
clearTimeout(smokeTimeout);
|
|
310
|
+
process.stderr.write("[nexus-plugin-dev] renderer context synchronization smoke failed: " + errorMessage(error) + "\n");
|
|
311
|
+
app.exit(1);
|
|
312
|
+
});
|
|
300
313
|
}
|
|
301
314
|
});
|
|
302
315
|
}
|
|
303
316
|
|
|
317
|
+
async function verifyRendererContextSmoke() {
|
|
318
|
+
if (!mainWindow || mainWindow.isDestroyed()) {
|
|
319
|
+
throw new Error("Development host window is unavailable.");
|
|
320
|
+
}
|
|
321
|
+
const frame = findPluginRendererFrame(mainWindow.webContents.mainFrame);
|
|
322
|
+
if (!frame) {
|
|
323
|
+
throw new Error("Plugin renderer frame was not found.");
|
|
324
|
+
}
|
|
325
|
+
const hostApi = createHostApi();
|
|
326
|
+
if (hostApi.hostVersion !== configuration.hostVersion
|
|
327
|
+
|| hostApi.hostApiVersion !== sdk.PLUGIN_FRAMEWORK_VERSION
|
|
328
|
+
|| configuration.manifest.minHostVersion === undefined) {
|
|
329
|
+
throw new Error("Development Host API version contract is inconsistent.");
|
|
330
|
+
}
|
|
331
|
+
const initialState = await readRendererContextState(frame);
|
|
332
|
+
const targetLanguage = configuration.manifest.supportedCultures.find(function findAlternative(culture) {
|
|
333
|
+
return culture !== configuration.language;
|
|
334
|
+
}) || configuration.language;
|
|
335
|
+
const targetTheme = configuration.themeMode === "dark" ? "light" : "dark";
|
|
336
|
+
await mainWindow.webContents.executeJavaScript(`(() => {
|
|
337
|
+
const languageSelect = document.getElementById("languageSelect");
|
|
338
|
+
const themeSelect = document.getElementById("themeSelect");
|
|
339
|
+
languageSelect.value = ${JSON.stringify(targetLanguage)};
|
|
340
|
+
languageSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
|
341
|
+
themeSelect.value = ${JSON.stringify(targetTheme)};
|
|
342
|
+
themeSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
|
343
|
+
})()`, true);
|
|
344
|
+
|
|
345
|
+
const deadline = Date.now() + 5000;
|
|
346
|
+
let lastState = initialState;
|
|
347
|
+
while (Date.now() < deadline) {
|
|
348
|
+
lastState = await readRendererContextState(frame);
|
|
349
|
+
const languageChanged = lastState.language === targetLanguage
|
|
350
|
+
&& (targetLanguage === configuration.language || lastState.localizedText !== initialState.localizedText);
|
|
351
|
+
if (languageChanged && lastState.theme === targetTheme) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
await new Promise(function wait(resolve) {
|
|
355
|
+
setTimeout(resolve, 100);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
throw new Error(
|
|
359
|
+
"Expected language=" + targetLanguage + " and theme=" + targetTheme
|
|
360
|
+
+ ", received " + JSON.stringify(lastState) + "."
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function findPluginRendererFrame(frame) {
|
|
365
|
+
for (const childFrame of frame.frames) {
|
|
366
|
+
if (childFrame.url.startsWith(pluginRendererProtocol + "://" + configuration.manifest.pluginId + "/")) {
|
|
367
|
+
return childFrame;
|
|
368
|
+
}
|
|
369
|
+
const nested = findPluginRendererFrame(childFrame);
|
|
370
|
+
if (nested) {
|
|
371
|
+
return nested;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function readRendererContextState(frame) {
|
|
378
|
+
return frame.executeJavaScript(`({
|
|
379
|
+
language: document.documentElement.lang,
|
|
380
|
+
theme: document.documentElement.dataset.theme || "",
|
|
381
|
+
localizedText: document.querySelector("[data-i18n]")
|
|
382
|
+
? document.querySelector("[data-i18n]").textContent
|
|
383
|
+
: ""
|
|
384
|
+
})`, true);
|
|
385
|
+
}
|
|
386
|
+
|
|
304
387
|
function createRendererConfiguration() {
|
|
305
388
|
const manifest = configuration.manifest;
|
|
306
389
|
const frameOrigin = pluginRendererProtocol + "://" + manifest.pluginId;
|
|
@@ -457,7 +540,7 @@ function createWindow() {
|
|
|
457
540
|
}
|
|
458
541
|
if (configuration.smoke) {
|
|
459
542
|
smokeTimeout = setTimeout(function failSmokeTimeout() {
|
|
460
|
-
process.stderr.write("[nexus-plugin-dev] renderer
|
|
543
|
+
process.stderr.write("[nexus-plugin-dev] renderer context synchronization smoke timed out.\n");
|
|
461
544
|
app.exit(1);
|
|
462
545
|
}, 15000);
|
|
463
546
|
}
|
|
@@ -660,6 +743,13 @@ function requiredPathArgument(args, name) {
|
|
|
660
743
|
return resolved;
|
|
661
744
|
}
|
|
662
745
|
|
|
746
|
+
function isStrictVersion(value) {
|
|
747
|
+
const match = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.exec(value);
|
|
748
|
+
return Boolean(match && [match[1], match[2], match[3]].every(function isSafe(segment) {
|
|
749
|
+
return Number.isSafeInteger(Number(segment));
|
|
750
|
+
}));
|
|
751
|
+
}
|
|
752
|
+
|
|
663
753
|
function requiredArgument(args, name) {
|
|
664
754
|
const prefix = name + "=";
|
|
665
755
|
const argument = args.find(function findArgument(value) {
|
package/dist/src/cli/run.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export interface CliIo {
|
|
|
3
3
|
stdout(message: string): void;
|
|
4
4
|
stderr(message: string): void;
|
|
5
5
|
}
|
|
6
|
-
export declare function runCli(args: string[], io: CliIo): number
|
|
6
|
+
export declare function runCli(args: string[], io: CliIo): number | Promise<number>;
|
|
7
7
|
export declare function usage(): string;
|
package/dist/src/cli/run.js
CHANGED
|
@@ -28,6 +28,8 @@ const path = __importStar(require("path"));
|
|
|
28
28
|
const index_1 = require("../index");
|
|
29
29
|
const scaffold_1 = require("../scaffold");
|
|
30
30
|
const dev_1 = require("../dev");
|
|
31
|
+
const web_dev_1 = require("../web-dev");
|
|
32
|
+
const nexus_dev_1 = require("../nexus-dev");
|
|
31
33
|
const nodeEngine = ">=16.20.0 <17";
|
|
32
34
|
function runCli(args, io) {
|
|
33
35
|
const command = args[0];
|
|
@@ -66,6 +68,12 @@ function runCli(args, io) {
|
|
|
66
68
|
if (command === "dev") {
|
|
67
69
|
return runDev(args.slice(1), io);
|
|
68
70
|
}
|
|
71
|
+
if (command === "dev:web") {
|
|
72
|
+
return runWebDev(args.slice(1), io);
|
|
73
|
+
}
|
|
74
|
+
if (command === "dev:nexus") {
|
|
75
|
+
return runNexusDev(args.slice(1), io);
|
|
76
|
+
}
|
|
69
77
|
return usageError(io, command ? "Unknown command: " + command : "A command is required.");
|
|
70
78
|
}
|
|
71
79
|
exports.runCli = runCli;
|
|
@@ -77,11 +85,187 @@ function usage() {
|
|
|
77
85
|
" nexus-plugin create <plugin-id> [--template minimal|sqlite] [--output <parent-directory>]",
|
|
78
86
|
" [--display-name <name>] [--sdk-source <version-or-local-tgz>] [--install]",
|
|
79
87
|
" nexus-plugin dev [plugin-project-directory] [--no-build] [--inspect[=<port>] | --inspect-brk[=<port>]]",
|
|
88
|
+
" [--host-version <X.Y.Z>] [--language <culture>] [--theme system|light|dark] [--surface business|tool|settings]",
|
|
89
|
+
" [--contribution-id <id>] [--no-devtools]",
|
|
90
|
+
" nexus-plugin dev:web [plugin-project-directory] [--no-build] [--no-open] [--port <port>]",
|
|
91
|
+
" [--language <culture>] [--theme system|light|dark] [--surface business|tool|settings]",
|
|
92
|
+
" [--contribution-id <id>]",
|
|
93
|
+
" nexus-plugin dev:nexus [plugin-project-directory] --host <nexus.exe-or-directory>",
|
|
94
|
+
" [--inspect[=<port>] | --inspect-brk[=<port>]] [--no-devtools]",
|
|
80
95
|
" [--language <culture>] [--theme system|light|dark] [--surface business|tool|settings]",
|
|
81
|
-
" [--contribution-id <id>]
|
|
96
|
+
" [--contribution-id <id>]"
|
|
82
97
|
].join("\n");
|
|
83
98
|
}
|
|
84
99
|
exports.usage = usage;
|
|
100
|
+
function runNexusDev(args, io) {
|
|
101
|
+
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
102
|
+
io.stdout(usage());
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
let projectDirectory = io.cwd();
|
|
106
|
+
let index = 0;
|
|
107
|
+
if (args[0] && !args[0].startsWith("--")) {
|
|
108
|
+
projectDirectory = path.resolve(io.cwd(), args[0]);
|
|
109
|
+
index = 1;
|
|
110
|
+
}
|
|
111
|
+
const options = {
|
|
112
|
+
projectDirectory,
|
|
113
|
+
openDevTools: true
|
|
114
|
+
};
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (; index < args.length; index += 1) {
|
|
117
|
+
const option = args[index];
|
|
118
|
+
if (option === "--no-devtools") {
|
|
119
|
+
if (seen.has(option)) {
|
|
120
|
+
return usageError(io, "Duplicate option: " + option);
|
|
121
|
+
}
|
|
122
|
+
seen.add(option);
|
|
123
|
+
options.openDevTools = false;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const inspector = parseInspectorOption(option);
|
|
127
|
+
if (inspector) {
|
|
128
|
+
if (options.inspectorMode) {
|
|
129
|
+
return usageError(io, "Only one main-process inspector option can be used.");
|
|
130
|
+
}
|
|
131
|
+
const key = "--" + inspector.mode;
|
|
132
|
+
if (seen.has(key)) {
|
|
133
|
+
return usageError(io, "Duplicate option: " + key);
|
|
134
|
+
}
|
|
135
|
+
seen.add(key);
|
|
136
|
+
options.inspectorMode = inspector.mode;
|
|
137
|
+
options.inspectorPort = inspector.port;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!["--host", "--language", "--theme", "--surface", "--contribution-id"].includes(option)) {
|
|
141
|
+
return usageError(io, "Unknown dev:nexus option: " + option);
|
|
142
|
+
}
|
|
143
|
+
if (seen.has(option)) {
|
|
144
|
+
return usageError(io, "Duplicate option: " + option);
|
|
145
|
+
}
|
|
146
|
+
seen.add(option);
|
|
147
|
+
const value = args[index + 1];
|
|
148
|
+
if (value === undefined || value.startsWith("--")) {
|
|
149
|
+
return usageError(io, option + " requires a value.");
|
|
150
|
+
}
|
|
151
|
+
index += 1;
|
|
152
|
+
if (option === "--host") {
|
|
153
|
+
options.hostPath = path.resolve(io.cwd(), value);
|
|
154
|
+
}
|
|
155
|
+
else if (option === "--language") {
|
|
156
|
+
options.language = value;
|
|
157
|
+
}
|
|
158
|
+
else if (option === "--theme") {
|
|
159
|
+
if (value !== "system" && value !== "light" && value !== "dark") {
|
|
160
|
+
return usageError(io, "theme must be system, light, or dark.");
|
|
161
|
+
}
|
|
162
|
+
options.themeMode = value;
|
|
163
|
+
}
|
|
164
|
+
else if (option === "--surface") {
|
|
165
|
+
if (value !== "business" && value !== "tool" && value !== "settings") {
|
|
166
|
+
return usageError(io, "surface must be business, tool, or settings.");
|
|
167
|
+
}
|
|
168
|
+
options.surface = value;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
options.contributionId = value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!options.hostPath) {
|
|
175
|
+
return usageError(io, "dev:nexus requires --host <nexus.exe-or-directory>.");
|
|
176
|
+
}
|
|
177
|
+
return (0, nexus_dev_1.runPluginNexusDev)(options, io).catch((error) => {
|
|
178
|
+
if (error instanceof dev_1.PluginDevError) {
|
|
179
|
+
io.stderr(error.message);
|
|
180
|
+
return error.exitCode;
|
|
181
|
+
}
|
|
182
|
+
io.stderr("Packaged Nexus plugin development failed: " + (error instanceof Error ? error.message : String(error)));
|
|
183
|
+
return 1;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function runWebDev(args, io) {
|
|
187
|
+
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
188
|
+
io.stdout(usage());
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
let projectDirectory = io.cwd();
|
|
192
|
+
let index = 0;
|
|
193
|
+
if (args[0] && !args[0].startsWith("--")) {
|
|
194
|
+
projectDirectory = path.resolve(io.cwd(), args[0]);
|
|
195
|
+
index = 1;
|
|
196
|
+
}
|
|
197
|
+
const options = {
|
|
198
|
+
projectDirectory,
|
|
199
|
+
build: true,
|
|
200
|
+
openBrowser: true,
|
|
201
|
+
port: 4173
|
|
202
|
+
};
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
for (; index < args.length; index += 1) {
|
|
205
|
+
const option = args[index];
|
|
206
|
+
if (option === "--no-build" || option === "--no-open") {
|
|
207
|
+
if (seen.has(option)) {
|
|
208
|
+
return usageError(io, "Duplicate option: " + option);
|
|
209
|
+
}
|
|
210
|
+
seen.add(option);
|
|
211
|
+
if (option === "--no-build") {
|
|
212
|
+
options.build = false;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
options.openBrowser = false;
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!["--port", "--language", "--theme", "--surface", "--contribution-id"].includes(option)) {
|
|
220
|
+
return usageError(io, "Unknown dev:web option: " + option);
|
|
221
|
+
}
|
|
222
|
+
if (seen.has(option)) {
|
|
223
|
+
return usageError(io, "Duplicate option: " + option);
|
|
224
|
+
}
|
|
225
|
+
seen.add(option);
|
|
226
|
+
const value = args[index + 1];
|
|
227
|
+
if (value === undefined || value.startsWith("--")) {
|
|
228
|
+
return usageError(io, option + " requires a value.");
|
|
229
|
+
}
|
|
230
|
+
index += 1;
|
|
231
|
+
if (option === "--port") {
|
|
232
|
+
const port = Number(value);
|
|
233
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
234
|
+
return usageError(io, "port must be an integer from 1 to 65535.");
|
|
235
|
+
}
|
|
236
|
+
options.port = port;
|
|
237
|
+
}
|
|
238
|
+
else if (option === "--language") {
|
|
239
|
+
options.language = value;
|
|
240
|
+
}
|
|
241
|
+
else if (option === "--theme") {
|
|
242
|
+
if (value !== "system" && value !== "light" && value !== "dark") {
|
|
243
|
+
return usageError(io, "theme must be system, light, or dark.");
|
|
244
|
+
}
|
|
245
|
+
options.themeMode = value;
|
|
246
|
+
}
|
|
247
|
+
else if (option === "--surface") {
|
|
248
|
+
if (value !== "business" && value !== "tool" && value !== "settings") {
|
|
249
|
+
return usageError(io, "surface must be business, tool, or settings.");
|
|
250
|
+
}
|
|
251
|
+
options.surface = value;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
options.contributionId = value;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
return (0, web_dev_1.runPluginWebDev)(options, io);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (error instanceof dev_1.PluginDevError) {
|
|
262
|
+
io.stderr(error.message);
|
|
263
|
+
return error.exitCode;
|
|
264
|
+
}
|
|
265
|
+
io.stderr("Plugin browser dev host failed: " + (error instanceof Error ? error.message : String(error)));
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
85
269
|
function runDev(args, io) {
|
|
86
270
|
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
87
271
|
io.stdout(usage());
|
|
@@ -131,7 +315,7 @@ function runDev(args, io) {
|
|
|
131
315
|
options.inspectorPort = inspector.port;
|
|
132
316
|
continue;
|
|
133
317
|
}
|
|
134
|
-
if (!["--language", "--theme", "--surface", "--contribution-id"].includes(option)) {
|
|
318
|
+
if (!["--host-version", "--language", "--theme", "--surface", "--contribution-id"].includes(option)) {
|
|
135
319
|
return usageError(io, "Unknown dev option: " + option);
|
|
136
320
|
}
|
|
137
321
|
if (seen.has(option)) {
|
|
@@ -143,7 +327,13 @@ function runDev(args, io) {
|
|
|
143
327
|
return usageError(io, option + " requires a value.");
|
|
144
328
|
}
|
|
145
329
|
index += 1;
|
|
146
|
-
if (option === "--
|
|
330
|
+
if (option === "--host-version") {
|
|
331
|
+
if (!isStrictVersion(value)) {
|
|
332
|
+
return usageError(io, "host-version must use X.Y.Z format.");
|
|
333
|
+
}
|
|
334
|
+
options.hostVersion = value;
|
|
335
|
+
}
|
|
336
|
+
else if (option === "--language") {
|
|
147
337
|
options.language = value;
|
|
148
338
|
}
|
|
149
339
|
else if (option === "--theme") {
|
|
@@ -185,6 +375,10 @@ function parseInspectorOption(option) {
|
|
|
185
375
|
}
|
|
186
376
|
return { mode: match[1], port };
|
|
187
377
|
}
|
|
378
|
+
function isStrictVersion(value) {
|
|
379
|
+
const match = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.exec(value);
|
|
380
|
+
return Boolean(match && [match[1], match[2], match[3]].every((segment) => Number.isSafeInteger(Number(segment))));
|
|
381
|
+
}
|
|
188
382
|
function runCreate(args, io) {
|
|
189
383
|
if (args.length === 0 || args[0].startsWith("--")) {
|
|
190
384
|
return usageError(io, "create requires a plugin-id.");
|