@aiot-nexus/plugin-sdk 3.1.1 → 3.3.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 CHANGED
@@ -1,15 +1,20 @@
1
1
  # @aiot-nexus/plugin-sdk
2
2
 
3
- > 当前 SDK 包版本为 `3.1.1`,Framework API 为 `3.1.0`,manifest schema 保持 `1`。Framework 3 将可信插件的 `mainEntry` 直接加载到 Nexus Electron 主进程;它不是安全沙箱。PluginFrame iframe 仍保持 sandbox、无 Node/Electron 访问。宿主运行模型见 [Framework v3 架构](../docs/plugin-framework-v3-architecture.md)。
3
+ > 当前 SDK 包版本为 `3.3.0`,Framework API 为 `3.2.0`,manifest schema 保持 `1`。Framework 3 将可信插件的 `mainEntry` 直接加载到 AIoT Toolkits Electron 主进程;它不是安全沙箱。PluginFrame iframe 仍保持 sandbox、无 Node/Electron 访问。宿主运行模型见 [Framework v3 架构](../docs/plugin-framework-v3-architecture.md)。
4
4
 
5
5
  本包提供公开 TypeScript 契约、PluginFrame 浏览器客户端和 `nexus-plugin` CLI。v2 项目升级前请先阅读 [v2 到 v3 迁移说明](../docs/plugin-framework-v2-v3-migration.md)。完整开发流程见 [独立插件开发指南](../docs/standalone-plugin-development-guide.md)。
6
6
 
7
+ 宿主产品名为 AIoT Toolkits,SDK 包名和 `dev:nexus` 命令保持不变。
8
+ 本仓库 SDK 的 `--host` 目录参数只查找 `AIoT Toolkits.exe`,文件不存在时直接报错。
9
+ 尚未更新的已发布 SDK 应传完整路径,例如 `--host "C:\Tools\AIoT Toolkits.exe"`;
10
+ 目录自动识别的新行为需使用包含本次修改的 SDK 构建。
11
+
7
12
  ## 兼容基线
8
13
 
9
14
  | 项目 | 固定值 |
10
15
  | --- | --- |
11
- | SDK | `3.1.1` |
12
- | Framework API | `3.1.0` |
16
+ | SDK | `3.3.0` |
17
+ | Framework API | `3.2.0` |
13
18
  | Manifest schema | `1` |
14
19
  | Node 安装和构建 | `>=16.20.0 <17` |
15
20
  | Electron | `22.3.27` |
@@ -27,7 +32,7 @@ nexus-plugin create <plugin-id> --template minimal|sqlite --output <parent-direc
27
32
  nexus-plugin upgrade [plugin-project-directory] [--check] [--sdk-source <version-or-local-tgz>]
28
33
  nexus-plugin dev [plugin-project-directory]
29
34
  nexus-plugin dev:web [plugin-project-directory]
30
- nexus-plugin dev:nexus [plugin-project-directory] --host <nexus.exe-or-directory>
35
+ nexus-plugin dev:nexus [plugin-project-directory] --host <host-exe-or-directory>
31
36
  ```
32
37
 
33
38
  仓库内 SDK 命令:
@@ -48,15 +53,15 @@ npm run publish:sdk
48
53
  registry 升级与只读预检:
49
54
 
50
55
  ```bash
51
- npx --package=@aiot-nexus/plugin-sdk@3.1.1 nexus-plugin upgrade . --check
52
- npx --package=@aiot-nexus/plugin-sdk@3.1.1 nexus-plugin upgrade .
56
+ npx --package=@aiot-nexus/plugin-sdk@3.3.0 nexus-plugin upgrade . --check
57
+ npx --package=@aiot-nexus/plugin-sdk@3.3.0 nexus-plugin upgrade .
53
58
  ```
54
59
 
55
60
  发布前或本地 tgz:
56
61
 
57
62
  ```bash
58
- npx --package=./aiot-nexus-plugin-sdk-3.1.1.tgz nexus-plugin upgrade . \
59
- --sdk-source ./aiot-nexus-plugin-sdk-3.1.1.tgz
63
+ npx --package=./aiot-nexus-plugin-sdk-3.3.0.tgz nexus-plugin upgrade . \
64
+ --sdk-source ./aiot-nexus-plugin-sdk-3.3.0.tgz
60
65
  ```
61
66
 
62
67
  CI 可以追加 `--json` 获得 `up-to-date`、`upgrade-available`、`upgraded`、`rolled-back` 或 `blocked`。`--allow-major` 只解除 major 保护,不会改写业务源码;跨 major 必须先阅读迁移指南。`--skip-project-checks` 只跳过项目 typecheck/build,安装后的 SDK、package 和 lockfile 一致性仍会验证。
@@ -67,7 +72,7 @@ CI 可以追加 `--json` 获得 `up-to-date`、`upgrade-available`、`upgraded`
67
72
 
68
73
  安装或项目验证失败时恢复备份,并在原 lockfile 存在时运行 `npm ci`。没有原 lockfile 时会运行 `npm install` 并恢复原 SDK,但无法证明完整传递依赖树与升级前一致,因此返回 `blocked` 并保留恢复数据。标准脚手架 build 的 `dist/<pluginId>` 在回滚范围内;自定义 typecheck/build 若修改其他路径,这些副作用不属于升级事务,项目必须自行管理。
69
74
 
70
- SDK `3.0.2` 或 `3.1.0` 升级到 `3.1.1` 后,原有 `hostApiVersion: 3.0.0` 可以保持不变。只有实际使用 `showHostStatusBar` 等 3.1 专属字段时,才把 manifest 声明提升到 `3.1.0`。SDK 3.1.1 修复了 registry 升级时把裸版本号误传给 `npm install` 的问题;全局安装 3.1.0 的用户应先把全局 CLI 更新到 3.1.1。
75
+ SDK `3.0.x` / `3.1.x` 插件可以由 3.2.x 宿主继续加载。升级到 SDK `3.2.0` 只提供主题能力和新的契约;插件必须引用 `--nexus-color-*` 或读取 `context.theme` 才会使用宿主主题色。没有引用新变量的插件保持原有颜色;需要自定义主题时传入 `autoApplyHostTheme: false`。原有 `hostApiVersion: 3.0.0` 可以保持不变,只有实际使用 `showHostStatusBar` 时才需要声明 `hostApiVersion: 3.1.0` 或更高。SDK 3.1.1 修复 registry 升级命令丢失 npm 包名的问题,SDK 3.3.0 将 `dev:nexus --host` 目录查找目标改为 `AIoT Toolkits.exe`,均作为历史兼容说明保留。
71
76
 
72
77
  ## 创建插件
73
78
 
@@ -90,14 +95,14 @@ nexus-plugin create my-frame-plugin \
90
95
 
91
96
  ```bash
92
97
  npm run pack:sdk
93
- nexus-plugin create my-frame-plugin --sdk-source ./package/aiot-nexus-plugin-sdk-3.1.1.tgz --install
98
+ nexus-plugin create my-frame-plugin --sdk-source ./package/aiot-nexus-plugin-sdk-3.3.0.tgz --install
94
99
  ```
95
100
 
96
- 生成插件后,可以在 `src/manifest.json` 的业务入口中静态控制 Nexus 宿主状态栏。相关 manifest 片段如下,其余必填字段沿用脚手架生成值:
101
+ 生成插件后,可以在 `src/manifest.json` 的业务入口中静态控制 AIoT Toolkits 宿主状态栏。相关 manifest 片段如下,其余必填字段沿用脚手架生成值:
97
102
 
98
103
  ```json
99
104
  {
100
- "hostApiVersion": "3.1.0",
105
+ "hostApiVersion": "3.2.0",
101
106
  "businessEntries": [
102
107
  {
103
108
  "businessId": "my-frame-plugin",
@@ -115,7 +120,7 @@ nexus-plugin create my-frame-plugin --sdk-source ./package/aiot-nexus-plugin-sdk
115
120
 
116
121
  仅显式设置为 `false` 时隐藏。字段省略或设置为 `true` 时显示;离开该业务入口后,宿主状态栏自动恢复。该字段要求 `hostApiVersion >= 3.1.0`。
117
122
 
118
- ## Framework 3.1 mainEntry
123
+ ## Framework 3.2 mainEntry
119
124
 
120
125
  `mainEntry` 是可信 Electron 主进程代码,可直接导入 Electron、Node 内置模块和插件本地依赖:
121
126
 
@@ -183,9 +188,9 @@ function failure(code: string, message: string) {
183
188
  - `context.host.*` 直接返回 `NexusResult<T>`,不需要 `await`。
184
189
  - 注册的 operation handler 可以返回 Promise,可等待异步文件对话框、网络或其他用户交互。
185
190
  - operation 默认没有业务超时。浏览器客户端 `requestTimeoutMs` 默认是 `0`;20 秒仅用于 iframe 初始化握手。
186
- - 同步 Electron API 也能调用,但 `showOpenDialogSync()`、同步文件 I/O、长 CPU 任务会阻塞 Nexus 主进程、所有窗口和插件,应优先使用异步版本。
191
+ - 同步 Electron API 也能调用,但 `showOpenDialogSync()`、同步文件 I/O、长 CPU 任务会阻塞 AIoT Toolkits 主进程、所有窗口和插件,应优先使用异步版本。
187
192
  - Framework 不跟踪插件直接创建的 Electron/Node 资源。插件必须在 `deactivate` 中自行清理。
188
- - Node 模块图和原生模块不能安全热卸载。已加载 `mainEntry`、依赖、原生模块或相关 manifest 变化必须重启 Nexus。
193
+ - Node 模块图和原生模块不能安全热卸载。已加载 `mainEntry`、依赖、原生模块或相关 manifest 变化必须重启 AIoT Toolkits。
189
194
 
190
195
  Framework 3 不再提供 `context.host.electron` Facade,也没有 `electronHostControl` 权限。manifest 权限只描述 Host API 能力,不限制可信 `mainEntry` 直接调用 Electron/Node。SDK 保留 `createPluginElectronIpcChannel()` 和 `createPluginElectronProtocolScheme()` 作为命名约定帮助函数;宿主不会自动隔离或清理对应注册。
191
196
 
@@ -205,6 +210,12 @@ script-src 'self'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:
205
210
 
206
211
  `createPluginFrameClient()` 的首次初始化默认等待 20 秒。业务请求默认不设超时;只有插件显式传入正数 `requestTimeoutMs` 时客户端才会主动超时。进度、取消和恢复仍建议使用 `taskId + getStatus + cancel`,但这是任务 UX 和资源管理策略,不是 worker 存活要求。通信契约见 [插件 IPC 与事件通知示例](../docs/plugin-ipc-event-notification-example.md)。
207
212
 
213
+ ### 宿主主题色
214
+
215
+ Framework 3.2 在 `PluginRendererContext.theme` 中提供 `light` 和 `dark` 两套语义 palette。SDK frame client 默认把当前 palette 的固定白名单字段写入 `--nexus-color-*`,并设置 `data-nexus-theme="light|dark"`;`themeMode: "system"` 会跟随系统主题变化。SDK 不会修改插件已有的 `--bg`、`--accent`、`data-theme` 或 `color-scheme`。
216
+
217
+ SDK 升级本身只提供能力。插件需要在 CSS 中引用 `var(--nexus-color-primary, #8fc31f)` 等变量,或从 `context.theme?.dark.primary` 读取值,才会使用宿主主题色。旧宿主或缺少 `theme` 的上下文仍可正常初始化。自定义主题插件可以显式使用 `createPluginFrameClient({ autoApplyHostTheme: false })`,此时不注入变量、不设置宿主主题标记,但仍可手动读取 `context.theme`。
218
+
208
219
  ## SQLite 模板
209
220
 
210
221
  `sqlite` 模板默认打开 `pluginDataDirectory/plugin.db` 并初始化 `plugin_records`。工具栏“打开数据库”由 `mainEntry` 直接调用 `dialog.showOpenDialog()`;iframe 只接收数据库信息和参数化 operation。
@@ -228,7 +239,7 @@ script-src 'self'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:
228
239
  | `npm run dev:web` | 不执行 | 不可用 | 只调试 Renderer;`invokeMain` 返回 `PluginCapabilityUnsupported`。 |
229
240
  | `npm run dev` | Electron 调试宿主主进程直接加载 | 可用 | 调试真实生命周期、对话框、SQLite 和插件窗口。 |
230
241
  | `npm run dev:debug` | 同 `dev` | 可用 | Electron 主进程以 `--inspect-brk=9229` 启动。 |
231
- | `npm run dev:nexus -- --host ...` | 正式 Nexus 主进程直接加载 | 可用 | 最终集成和监听更新。 |
242
+ | `npm run dev:nexus -- --host ...` | 正式 AIoT Toolkits 主进程直接加载 | 可用 | 最终集成和监听更新。 |
232
243
 
233
244
  Electron 调试壳仅保留 context 工具栏;插件 iframe 无边距、无边框,填满剩余窗口,不设置最小宽高,也不根据内容上报限制尺寸。Renderer DevTools 默认打开。
234
245
 
@@ -238,16 +249,16 @@ VS Code 模板提供 `Debug Nexus plugin` 和 `Attach Nexus plugin main process
238
249
 
239
250
  | 修改范围 | 处理方式 |
240
251
  | --- | --- |
241
- | `src/renderer/**` | 只构建 Renderer 并重载 iframe,Nexus PID 不变。 |
252
+ | `src/renderer/**` | 只构建 Renderer 并重载 iframe,AIoT Toolkits PID 不变。 |
242
253
  | 无 `mainEntry` 插件的安全 manifest 展示/贡献字段 | 原子替换 manifest 并重扫,PID 不变。 |
243
- | 有 `mainEntry` 插件的 manifest 变化 | 完整构建并重启 Nexus。 |
244
- | `src/main.*`、依赖、原生模块、构建脚本 | 完整构建并重启 Nexus。 |
254
+ | 有 `mainEntry` 插件的 manifest 变化 | 完整构建并重启 AIoT Toolkits。 |
255
+ | `src/main.*`、依赖、原生模块、构建脚本 | 完整构建并重启 AIoT Toolkits。 |
245
256
 
246
- 构建失败保留旧部署;新宿主启动或就绪失败会恢复旧包和旧宿主。`--inspect` / `--inspect-brk` 调试的是 Nexus Electron 主进程,因此会同时暂停或暴露宿主代码。按 `Ctrl+C` 时 CLI 关闭自己启动的宿主。
257
+ 构建失败保留旧部署;新宿主启动或就绪失败会恢复旧包和旧宿主。`--inspect` / `--inspect-brk` 调试的是 AIoT Toolkits Electron 主进程,因此会同时暂停或暴露宿主代码。按 `Ctrl+C` 时 CLI 关闭自己启动的宿主。
247
258
 
248
259
  ## 发布前检查
249
260
 
250
- 1. `hostApiVersion` 为 `3.1.0`,manifest schema 仍为 `1`。
261
+ 1. `hostApiVersion` 使用当前 `3.2.0`(或按实际能力保持旧版本),manifest schema 仍为 `1`。
251
262
  2. 业务入口的 `showHostStatusBar` 可静态声明是否保留宿主状态栏;省略时默认保留,显式使用时要求 `hostApiVersion >= 3.1.0`。
252
263
  3. `activate/deactivate` 同步,operation handler 才使用异步。
253
264
  4. Renderer 只使用窄口径 operation,所有输入在 mainEntry 再校验。
@@ -255,4 +266,5 @@ VS Code 模板提供 `Debug Nexus plugin` 和 `Attach Nexus plugin main process
255
266
  6. IPC、protocol、快捷键、菜单和文件路径使用插件命名空间并检测冲突。
256
267
  7. `deactivate` 关闭窗口、Session 监听、IPC、protocol、快捷键、定时器、数据库和子进程。
257
268
  8. 没有同步长任务、无限载荷、任意命令/SQL/路径透传或日志敏感信息。
258
- 9. 运行 `npm test`、插件构建、正式 Nexus 集成和 Windows 7 真机验收。
269
+ 9. 主题集成明确引用 `--nexus-color-*` / `context.theme`,或显式关闭 `autoApplyHostTheme`。
270
+ 10. 运行 `npm test`、插件构建、正式 AIoT Toolkits 集成和 Windows 7 真机验收。
package/dev-host/main.js CHANGED
@@ -191,13 +191,13 @@ function activatePluginMainEntry() {
191
191
  throw new Error("Plugin activate export must be a function.");
192
192
  }
193
193
  if (isAsyncFunction(activate)) {
194
- throw new Error("Plugin activate must complete synchronously in Framework API 3.1.0.");
194
+ throw new Error("Plugin activate must complete synchronously in Framework API 3.2.0.");
195
195
  }
196
196
  activationStarted = true;
197
197
  const result = activate(createActivationContext());
198
198
  if (isPromiseLike(result)) {
199
199
  observeRejectedLifecyclePromise(result, "activate");
200
- throw new Error("Plugin activate must complete synchronously in Framework API 3.1.0.");
200
+ throw new Error("Plugin activate must complete synchronously in Framework API 3.2.0.");
201
201
  }
202
202
  }
203
203
  pluginActivated = true;
@@ -265,13 +265,13 @@ function deactivatePluginMainEntry(reason) {
265
265
  return;
266
266
  }
267
267
  if (typeof deactivate !== "function" || isAsyncFunction(deactivate)) {
268
- process.stderr.write("[nexus-plugin-dev] plugin deactivate must complete synchronously in Framework API 3.1.0.\n");
268
+ process.stderr.write("[nexus-plugin-dev] plugin deactivate must complete synchronously in Framework API 3.2.0.\n");
269
269
  return;
270
270
  }
271
271
  const result = deactivate(reason);
272
272
  if (isPromiseLike(result)) {
273
273
  observeRejectedLifecyclePromise(result, "deactivate");
274
- process.stderr.write("[nexus-plugin-dev] plugin deactivate must complete synchronously in Framework API 3.1.0.\n");
274
+ process.stderr.write("[nexus-plugin-dev] plugin deactivate must complete synchronously in Framework API 3.2.0.\n");
275
275
  }
276
276
  } catch (error) {
277
277
  process.stderr.write("[nexus-plugin-dev] plugin deactivation failed: " + errorMessage(error) + "\n");
@@ -448,6 +448,11 @@ async function verifyRendererContextSmoke() {
448
448
  || configuration.manifest.minHostVersion === undefined) {
449
449
  throw new Error("Development Host API version contract is inconsistent.");
450
450
  }
451
+ const frameSmokeToken = "nexus-plugin-frame-" + String(Date.now()) + "-" + String(Math.random());
452
+ await frame.executeJavaScript(
453
+ "window.__nexusPluginDevHostFrameToken = " + JSON.stringify(frameSmokeToken) + ";",
454
+ true
455
+ );
451
456
  const initialState = await readRendererContextState(frame);
452
457
  const targetLanguage = configuration.manifest.supportedCultures.find(function findAlternative(culture) {
453
458
  return culture !== configuration.language;
@@ -468,7 +473,11 @@ async function verifyRendererContextSmoke() {
468
473
  lastState = await readRendererContextState(frame);
469
474
  const languageChanged = lastState.language === targetLanguage
470
475
  && (targetLanguage === configuration.language || lastState.localizedText !== initialState.localizedText);
471
- if (languageChanged && lastState.theme === targetTheme) {
476
+ if (languageChanged
477
+ && lastState.theme === targetTheme
478
+ && lastState.nexusTheme === targetTheme
479
+ && lastState.primary === sdk.NEXUS_PLUGIN_RENDERER_THEME[targetTheme].primary
480
+ && lastState.frameToken === frameSmokeToken) {
472
481
  return;
473
482
  }
474
483
  await new Promise(function wait(resolve) {
@@ -477,6 +486,8 @@ async function verifyRendererContextSmoke() {
477
486
  }
478
487
  throw new Error(
479
488
  "Expected language=" + targetLanguage + " and theme=" + targetTheme
489
+ + " and primary=" + sdk.NEXUS_PLUGIN_RENDERER_THEME[targetTheme].primary
490
+ + " and unchanged frame token"
480
491
  + ", received " + JSON.stringify(lastState) + "."
481
492
  );
482
493
  }
@@ -498,6 +509,9 @@ async function readRendererContextState(frame) {
498
509
  return frame.executeJavaScript(`({
499
510
  language: document.documentElement.lang,
500
511
  theme: document.documentElement.dataset.theme || "",
512
+ nexusTheme: document.documentElement.dataset.nexusTheme || "",
513
+ primary: document.documentElement.style.getPropertyValue("--nexus-color-primary").trim(),
514
+ frameToken: window.__nexusPluginDevHostFrameToken || "",
501
515
  localizedText: document.querySelector("[data-i18n]")
502
516
  ? document.querySelector("[data-i18n]").textContent
503
517
  : ""
@@ -525,6 +539,7 @@ function createRendererConfiguration() {
525
539
  contributionId: configuration.contributionId,
526
540
  language: configuration.language,
527
541
  themeMode: configuration.themeMode,
542
+ theme: sdk.NEXUS_PLUGIN_RENDERER_THEME,
528
543
  hostApiVersion: sdk.PLUGIN_FRAMEWORK_VERSION
529
544
  }
530
545
  };
@@ -1,6 +1,7 @@
1
- export declare const PLUGIN_FRAMEWORK_VERSION = "3.1.0";
1
+ export declare const PLUGIN_FRAMEWORK_VERSION = "3.2.0";
2
2
  export declare const PLUGIN_MANIFEST_SCHEMA_VERSION = 1;
3
3
  export declare const PLUGIN_COMPATIBLE_VERSION_PATTERN = "^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))?(\\.(0|[1-9][0-9]*))?$";
4
+ export declare const PLUGIN_VERSION_PATTERN = "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$";
4
5
  export interface ParsedPluginVersion {
5
6
  major: number;
6
7
  minor: number;
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isPluginFrameworkVersionCompatible = exports.comparePluginVersions = exports.parsePluginCompatibleVersion = exports.PLUGIN_COMPATIBLE_VERSION_PATTERN = exports.PLUGIN_MANIFEST_SCHEMA_VERSION = exports.PLUGIN_FRAMEWORK_VERSION = void 0;
4
- exports.PLUGIN_FRAMEWORK_VERSION = "3.1.0";
3
+ exports.isPluginFrameworkVersionCompatible = exports.comparePluginVersions = exports.parsePluginCompatibleVersion = exports.PLUGIN_VERSION_PATTERN = exports.PLUGIN_COMPATIBLE_VERSION_PATTERN = exports.PLUGIN_MANIFEST_SCHEMA_VERSION = exports.PLUGIN_FRAMEWORK_VERSION = void 0;
4
+ exports.PLUGIN_FRAMEWORK_VERSION = "3.2.0";
5
5
  exports.PLUGIN_MANIFEST_SCHEMA_VERSION = 1;
6
6
  exports.PLUGIN_COMPATIBLE_VERSION_PATTERN = "^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))?(\\.(0|[1-9][0-9]*))?$";
7
+ exports.PLUGIN_VERSION_PATTERN = "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$";
7
8
  function parsePluginCompatibleVersion(value) {
8
9
  const match = /^(0|[1-9][0-9]*)(?:\.(0|[1-9][0-9]*))?(?:\.(0|[1-9][0-9]*))?$/.exec(value.trim());
9
10
  if (!match) {
@@ -20,7 +20,7 @@
20
20
  "pluginId": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{1,63}$" },
21
21
  "displayNameKey": { "type": "string", "minLength": 1 },
22
22
  "descriptionKey": { "type": "string" },
23
- "pluginVersion": { "type": "string", "minLength": 1 },
23
+ "pluginVersion": { "type": "string", "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$" },
24
24
  "hostApiVersion": { "type": "string", "pattern": "^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))?(\\.(0|[1-9][0-9]*))?$" },
25
25
  "minHostVersion": { "type": "string", "pattern": "^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))?(\\.(0|[1-9][0-9]*))?$" },
26
26
  "supportedCultures": {
@@ -0,0 +1,43 @@
1
+ export declare const NEXUS_THEME_BACKGROUND_COLORS: {
2
+ readonly light: "#eef1f4";
3
+ readonly dark: "#101418";
4
+ };
5
+ export declare const NEXUS_BRAND_ACCENT_KEY = "green";
6
+ export interface PluginThemeTokens {
7
+ background: string;
8
+ surface: string;
9
+ surfaceAlt: string;
10
+ surfaceRaised: string;
11
+ surfaceHover: string;
12
+ surfaceInset: string;
13
+ border: string;
14
+ borderSoft: string;
15
+ borderStrong: string;
16
+ text: string;
17
+ textMuted: string;
18
+ textSoft: string;
19
+ textFaint: string;
20
+ primary: string;
21
+ accent: string;
22
+ onPrimary: string;
23
+ primarySoft: string;
24
+ info: string;
25
+ infoSoft: string;
26
+ positive: string;
27
+ positiveSoft: string;
28
+ warning: string;
29
+ warningSoft: string;
30
+ negative: string;
31
+ negativeSoft: string;
32
+ mutedSoft: string;
33
+ onInfo: string;
34
+ onPositive: string;
35
+ onWarning: string;
36
+ onNegative: string;
37
+ }
38
+ export interface PluginRendererTheme {
39
+ light: PluginThemeTokens;
40
+ dark: PluginThemeTokens;
41
+ }
42
+ /** Public semantic colors exposed to isolated PluginFrame renderers. */
43
+ export declare const NEXUS_PLUGIN_RENDERER_THEME: PluginRendererTheme;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NEXUS_PLUGIN_RENDERER_THEME = exports.NEXUS_BRAND_ACCENT_KEY = exports.NEXUS_THEME_BACKGROUND_COLORS = void 0;
4
+ exports.NEXUS_THEME_BACKGROUND_COLORS = {
5
+ light: "#eef1f4",
6
+ dark: "#101418"
7
+ };
8
+ exports.NEXUS_BRAND_ACCENT_KEY = "green";
9
+ /** Public semantic colors exposed to isolated PluginFrame renderers. */
10
+ exports.NEXUS_PLUGIN_RENDERER_THEME = {
11
+ light: {
12
+ background: "#eef1f4",
13
+ surface: "#ffffff",
14
+ surfaceAlt: "#f7f9fa",
15
+ surfaceRaised: "#f7f9fa",
16
+ surfaceHover: "#eef3f6",
17
+ surfaceInset: "#e4e9ed",
18
+ border: "#d7dde3",
19
+ borderSoft: "#e2e7eb",
20
+ borderStrong: "#c2cbd3",
21
+ text: "#212a32",
22
+ textMuted: "#5a6772",
23
+ textSoft: "#8b97a1",
24
+ textFaint: "#8b97a1",
25
+ primary: "#8fc31f",
26
+ accent: "#6f9a15",
27
+ onPrimary: "#ffffff",
28
+ primarySoft: "rgba(143, 195, 31, 0.12)",
29
+ info: "#2277c4",
30
+ infoSoft: "rgba(34, 119, 196, 0.12)",
31
+ positive: "#1f9a4e",
32
+ positiveSoft: "rgba(31, 154, 78, 0.12)",
33
+ warning: "#e0a53a",
34
+ warningSoft: "rgba(224, 165, 58, 0.14)",
35
+ negative: "#c2402f",
36
+ negativeSoft: "rgba(194, 64, 47, 0.1)",
37
+ mutedSoft: "rgba(90, 103, 114, 0.1)",
38
+ onInfo: "#ffffff",
39
+ onPositive: "#ffffff",
40
+ onWarning: "#ffffff",
41
+ onNegative: "#ffffff"
42
+ },
43
+ dark: {
44
+ background: "#101418",
45
+ surface: "#1b222a",
46
+ surfaceAlt: "#171c22",
47
+ surfaceRaised: "#171c22",
48
+ surfaceHover: "#222a34",
49
+ surfaceInset: "#0c0f13",
50
+ border: "#2a333e",
51
+ borderSoft: "#303a45",
52
+ borderStrong: "#38434f",
53
+ text: "#e2e8ee",
54
+ textMuted: "#97a3af",
55
+ textSoft: "#64707c",
56
+ textFaint: "#64707c",
57
+ primary: "#8fc31f",
58
+ accent: "#a5d43e",
59
+ onPrimary: "#ffffff",
60
+ primarySoft: "rgba(143, 195, 31, 0.14)",
61
+ info: "#4da3e8",
62
+ infoSoft: "rgba(77, 163, 232, 0.14)",
63
+ positive: "#46b96e",
64
+ positiveSoft: "rgba(70, 185, 110, 0.14)",
65
+ warning: "#e0a53a",
66
+ warningSoft: "rgba(224, 165, 58, 0.14)",
67
+ negative: "#e2604f",
68
+ negativeSoft: "rgba(226, 96, 79, 0.14)",
69
+ mutedSoft: "rgba(151, 163, 175, 0.12)",
70
+ onInfo: "#101418",
71
+ onPositive: "#101418",
72
+ onWarning: "#101418",
73
+ onNegative: "#101418"
74
+ }
75
+ };
@@ -10,6 +10,7 @@ export interface PluginFrameClientOptions {
10
10
  initializationTimeoutMs?: number;
11
11
  requestTimeoutMs?: number;
12
12
  autoResize?: boolean;
13
+ autoApplyHostTheme?: boolean;
13
14
  resizeRoot?: HTMLElement;
14
15
  }
15
16
  export type PluginFrameContextListener = (context: PluginRendererContext) => void;
@@ -5,6 +5,40 @@ const requestMessageType = "nexus-plugin:request";
5
5
  const responseMessageType = "nexus-plugin:response";
6
6
  const resizeMessageType = "nexus-plugin:resize";
7
7
  const defaultInitializationTimeoutMs = 20000;
8
+ const systemThemeMediaQuery = "(prefers-color-scheme: dark)";
9
+ const themeTokenCssVariables = [
10
+ ["background", "--nexus-color-background"],
11
+ ["surface", "--nexus-color-surface"],
12
+ ["surfaceAlt", "--nexus-color-surface-alt"],
13
+ ["surfaceRaised", "--nexus-color-surface-raised"],
14
+ ["surfaceHover", "--nexus-color-surface-hover"],
15
+ ["surfaceInset", "--nexus-color-surface-inset"],
16
+ ["border", "--nexus-color-border"],
17
+ ["borderSoft", "--nexus-color-border-soft"],
18
+ ["borderStrong", "--nexus-color-border-strong"],
19
+ ["text", "--nexus-color-text"],
20
+ ["textMuted", "--nexus-color-text-muted"],
21
+ ["textSoft", "--nexus-color-text-soft"],
22
+ ["textFaint", "--nexus-color-text-faint"],
23
+ ["primary", "--nexus-color-primary"],
24
+ ["accent", "--nexus-color-accent"],
25
+ ["onPrimary", "--nexus-color-on-primary"],
26
+ ["primarySoft", "--nexus-color-primary-soft"],
27
+ ["info", "--nexus-color-info"],
28
+ ["infoSoft", "--nexus-color-info-soft"],
29
+ ["positive", "--nexus-color-positive"],
30
+ ["positiveSoft", "--nexus-color-positive-soft"],
31
+ ["warning", "--nexus-color-warning"],
32
+ ["warningSoft", "--nexus-color-warning-soft"],
33
+ ["negative", "--nexus-color-negative"],
34
+ ["negativeSoft", "--nexus-color-negative-soft"],
35
+ ["mutedSoft", "--nexus-color-muted-soft"],
36
+ ["onInfo", "--nexus-color-on-info"],
37
+ ["onPositive", "--nexus-color-on-positive"],
38
+ ["onWarning", "--nexus-color-on-warning"],
39
+ ["onNegative", "--nexus-color-on-negative"]
40
+ ];
41
+ const hostThemeOwners = new WeakMap();
8
42
  export class PluginFrameClientError extends Error {
9
43
  constructor(code, message) {
10
44
  super(message);
@@ -21,6 +55,7 @@ export function createPluginFrameClient(options = {}) {
21
55
  const targetWindow = options.targetWindow || frameWindow.parent;
22
56
  const initializationTimeoutMs = options.initializationTimeoutMs === undefined ? defaultInitializationTimeoutMs : options.initializationTimeoutMs;
23
57
  const requestTimeoutMs = options.requestTimeoutMs === undefined ? 0 : options.requestTimeoutMs;
58
+ const autoApplyHostTheme = options.autoApplyHostTheme !== false;
24
59
  if (!Number.isFinite(initializationTimeoutMs) || initializationTimeoutMs <= 0) {
25
60
  throw new PluginFrameClientError("ProtocolError", "initializationTimeoutMs must be a positive finite number.");
26
61
  }
@@ -35,6 +70,16 @@ export function createPluginFrameClient(options = {}) {
35
70
  let contextTimeoutId;
36
71
  let resizeTimer;
37
72
  let resizeObserver;
73
+ const hostThemeOwner = Symbol("nexus-plugin-host-theme");
74
+ let hostThemeRoot;
75
+ let hostThemeMarker;
76
+ let hostThemePreviousMarker;
77
+ let hostThemeHadPreviousMarker = false;
78
+ const hostThemeAppliedValues = new Map();
79
+ const hostThemePreviousValues = new Map();
80
+ const systemThemeMedia = autoApplyHostTheme && typeof frameWindow.matchMedia === "function"
81
+ ? frameWindow.matchMedia(systemThemeMediaQuery)
82
+ : undefined;
38
83
  const pendingRequests = new Map();
39
84
  const contextListeners = new Set();
40
85
  let resolveContext;
@@ -52,6 +97,14 @@ export function createPluginFrameClient(options = {}) {
52
97
  }
53
98
  }, initializationTimeoutMs);
54
99
  frameWindow.addEventListener("message", handleMessage);
100
+ if (systemThemeMedia) {
101
+ if (typeof systemThemeMedia.addEventListener === "function") {
102
+ systemThemeMedia.addEventListener("change", handleSystemThemeChange);
103
+ }
104
+ else {
105
+ systemThemeMedia.addListener(handleSystemThemeChange);
106
+ }
107
+ }
55
108
  if (options.autoResize !== false) {
56
109
  frameWindow.addEventListener("resize", scheduleResize);
57
110
  const ResizeObserverConstructor = frameWindow.ResizeObserver;
@@ -124,6 +177,7 @@ export function createPluginFrameClient(options = {}) {
124
177
  const hadContext = contextValue !== undefined;
125
178
  hostOrigin = event.origin;
126
179
  contextValue = event.data.context;
180
+ applyCurrentHostTheme();
127
181
  if (!contextSettled) {
128
182
  contextSettled = true;
129
183
  if (contextTimeoutId !== undefined) {
@@ -166,6 +220,99 @@ export function createPluginFrameClient(options = {}) {
166
220
  }
167
221
  }
168
222
  }
223
+ function handleSystemThemeChange() {
224
+ if (contextValue?.themeMode === "system") {
225
+ applyCurrentHostTheme();
226
+ }
227
+ }
228
+ function applyCurrentHostTheme() {
229
+ if (!autoApplyHostTheme || !contextValue) {
230
+ return;
231
+ }
232
+ const root = frameWindow.document.documentElement;
233
+ if (!root?.dataset || !root.style) {
234
+ return;
235
+ }
236
+ initializeHostThemeOwnership(root);
237
+ const resolvedMode = contextValue.themeMode === "system"
238
+ ? (systemThemeMedia?.matches ? "dark" : "light")
239
+ : contextValue.themeMode;
240
+ root.dataset.nexusTheme = resolvedMode;
241
+ hostThemeMarker = resolvedMode;
242
+ const tokens = contextValue.theme?.[resolvedMode];
243
+ for (const [tokenName, cssVariable] of themeTokenCssVariables) {
244
+ if (tokens) {
245
+ const value = tokens[tokenName];
246
+ root.style.setProperty(cssVariable, value);
247
+ hostThemeAppliedValues.set(cssVariable, value);
248
+ }
249
+ else {
250
+ restoreHostThemeVariable(root, cssVariable);
251
+ }
252
+ }
253
+ }
254
+ function initializeHostThemeOwnership(root) {
255
+ if (hostThemeRoot === root) {
256
+ hostThemeOwners.set(root, hostThemeOwner);
257
+ return;
258
+ }
259
+ if (hostThemeRoot) {
260
+ cleanupHostTheme();
261
+ }
262
+ hostThemeRoot = root;
263
+ hostThemeOwners.set(root, hostThemeOwner);
264
+ hostThemePreviousMarker = root.dataset.nexusTheme;
265
+ hostThemeHadPreviousMarker = Object.prototype.hasOwnProperty.call(root.dataset, "nexusTheme");
266
+ hostThemePreviousValues.clear();
267
+ for (const [, cssVariable] of themeTokenCssVariables) {
268
+ hostThemePreviousValues.set(cssVariable, root.style.getPropertyValue(cssVariable));
269
+ }
270
+ }
271
+ function restoreHostThemeVariable(root, cssVariable) {
272
+ const appliedValue = hostThemeAppliedValues.get(cssVariable);
273
+ if (appliedValue === undefined || root.style.getPropertyValue(cssVariable) !== appliedValue) {
274
+ hostThemeAppliedValues.delete(cssVariable);
275
+ return;
276
+ }
277
+ const previousValue = hostThemePreviousValues.get(cssVariable) || "";
278
+ if (previousValue) {
279
+ root.style.setProperty(cssVariable, previousValue);
280
+ }
281
+ else {
282
+ root.style.removeProperty(cssVariable);
283
+ }
284
+ hostThemeAppliedValues.delete(cssVariable);
285
+ }
286
+ function cleanupHostTheme() {
287
+ const root = hostThemeRoot;
288
+ if (!root) {
289
+ return;
290
+ }
291
+ if (hostThemeOwners.get(root) !== hostThemeOwner) {
292
+ hostThemeRoot = undefined;
293
+ hostThemeAppliedValues.clear();
294
+ hostThemePreviousValues.clear();
295
+ return;
296
+ }
297
+ for (const [, cssVariable] of themeTokenCssVariables) {
298
+ restoreHostThemeVariable(root, cssVariable);
299
+ }
300
+ if (hostThemeMarker !== undefined && root.dataset.nexusTheme === hostThemeMarker) {
301
+ if (hostThemeHadPreviousMarker) {
302
+ root.dataset.nexusTheme = hostThemePreviousMarker || "";
303
+ }
304
+ else {
305
+ delete root.dataset.nexusTheme;
306
+ }
307
+ }
308
+ hostThemeOwners.delete(root);
309
+ hostThemeRoot = undefined;
310
+ hostThemeMarker = undefined;
311
+ hostThemePreviousMarker = undefined;
312
+ hostThemeHadPreviousMarker = false;
313
+ hostThemeAppliedValues.clear();
314
+ hostThemePreviousValues.clear();
315
+ }
169
316
  function scheduleResize() {
170
317
  if (disposed || !contextValue || options.autoResize === false) {
171
318
  return;
@@ -200,6 +347,19 @@ export function createPluginFrameClient(options = {}) {
200
347
  disposed = true;
201
348
  frameWindow.removeEventListener("message", handleMessage);
202
349
  frameWindow.removeEventListener("resize", scheduleResize);
350
+ if (systemThemeMedia) {
351
+ if (typeof systemThemeMedia.removeEventListener === "function") {
352
+ systemThemeMedia.removeEventListener("change", handleSystemThemeChange);
353
+ }
354
+ else {
355
+ systemThemeMedia.removeListener(handleSystemThemeChange);
356
+ }
357
+ }
358
+ cleanupHostTheme();
359
+ if (contextTimeoutId !== undefined) {
360
+ frameWindow.clearTimeout(contextTimeoutId);
361
+ contextTimeoutId = undefined;
362
+ }
203
363
  resizeObserver?.disconnect();
204
364
  if (resizeTimer !== undefined) {
205
365
  frameWindow.clearTimeout(resizeTimer);
@@ -236,8 +396,45 @@ function isPluginRendererContext(value) {
236
396
  && typeof value.contributionId === "string"
237
397
  && typeof value.language === "string"
238
398
  && (value.themeMode === "light" || value.themeMode === "dark" || value.themeMode === "system")
399
+ && (value.theme === undefined || isPluginRendererTheme(value.theme))
239
400
  && typeof value.hostApiVersion === "string";
240
401
  }
402
+ function isPluginRendererTheme(value) {
403
+ if (!hasExactKeys(value, ["light", "dark"])) {
404
+ return false;
405
+ }
406
+ return isPluginThemeTokens(value.light) && isPluginThemeTokens(value.dark);
407
+ }
408
+ function isPluginThemeTokens(value) {
409
+ const tokenNames = themeTokenCssVariables.map(([tokenName]) => tokenName);
410
+ return hasExactKeys(value, tokenNames)
411
+ && tokenNames.every((tokenName) => isSafeCssColorValue(value[tokenName]));
412
+ }
413
+ function hasExactKeys(value, expectedKeys) {
414
+ if (!isRecord(value)) {
415
+ return false;
416
+ }
417
+ const keys = Object.keys(value);
418
+ return keys.length === expectedKeys.length && keys.every((key) => expectedKeys.includes(key));
419
+ }
420
+ function isSafeCssColorValue(value) {
421
+ if (typeof value !== "string" || value.length === 0 || value.length > 64 || /[;{}]/.test(value)) {
422
+ return false;
423
+ }
424
+ if (/^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(value)) {
425
+ return true;
426
+ }
427
+ const match = /^(rgb|rgba)\(([^)]+)\)$/.exec(value);
428
+ if (!match) {
429
+ return false;
430
+ }
431
+ const parts = match[2].split(",").map((part) => part.trim());
432
+ const expectedLength = match[1] === "rgba" ? 4 : 3;
433
+ if (parts.length !== expectedLength || !parts.slice(0, 3).every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)) {
434
+ return false;
435
+ }
436
+ return expectedLength === 3 || /^(?:0|1|0?\.\d+)$/.test(parts[3]) && Number(parts[3]) <= 1;
437
+ }
241
438
  function isNexusResult(value) {
242
439
  if (!isRecord(value) || typeof value.ok !== "boolean") {
243
440
  return false;
@@ -1,3 +1,3 @@
1
1
  {
2
- "nexusApplicationVersion": "0.0.1"
2
+ "nexusApplicationVersion": "0.0.3"
3
3
  }