@aiot-nexus/plugin-sdk 3.1.1 → 3.2.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,6 +1,6 @@
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.2.0`,Framework API 为 `3.2.0`,manifest schema 保持 `1`。Framework 3 将可信插件的 `mainEntry` 直接加载到 Nexus 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
 
@@ -8,8 +8,8 @@
8
8
 
9
9
  | 项目 | 固定值 |
10
10
  | --- | --- |
11
- | SDK | `3.1.1` |
12
- | Framework API | `3.1.0` |
11
+ | SDK | `3.2.0` |
12
+ | Framework API | `3.2.0` |
13
13
  | Manifest schema | `1` |
14
14
  | Node 安装和构建 | `>=16.20.0 <17` |
15
15
  | Electron | `22.3.27` |
@@ -48,15 +48,15 @@ npm run publish:sdk
48
48
  registry 升级与只读预检:
49
49
 
50
50
  ```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 .
51
+ npx --package=@aiot-nexus/plugin-sdk@3.2.0 nexus-plugin upgrade . --check
52
+ npx --package=@aiot-nexus/plugin-sdk@3.2.0 nexus-plugin upgrade .
53
53
  ```
54
54
 
55
55
  发布前或本地 tgz:
56
56
 
57
57
  ```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
58
+ npx --package=./aiot-nexus-plugin-sdk-3.2.0.tgz nexus-plugin upgrade . \
59
+ --sdk-source ./aiot-nexus-plugin-sdk-3.2.0.tgz
60
60
  ```
61
61
 
62
62
  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 +67,7 @@ CI 可以追加 `--json` 获得 `up-to-date`、`upgrade-available`、`upgraded`
67
67
 
68
68
  安装或项目验证失败时恢复备份,并在原 lockfile 存在时运行 `npm ci`。没有原 lockfile 时会运行 `npm install` 并恢复原 SDK,但无法证明完整传递依赖树与升级前一致,因此返回 `blocked` 并保留恢复数据。标准脚手架 build 的 `dist/<pluginId>` 在回滚范围内;自定义 typecheck/build 若修改其他路径,这些副作用不属于升级事务,项目必须自行管理。
69
69
 
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。
70
+ 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 包名的问题,作为历史兼容说明保留。
71
71
 
72
72
  ## 创建插件
73
73
 
@@ -90,14 +90,14 @@ nexus-plugin create my-frame-plugin \
90
90
 
91
91
  ```bash
92
92
  npm run pack:sdk
93
- nexus-plugin create my-frame-plugin --sdk-source ./package/aiot-nexus-plugin-sdk-3.1.1.tgz --install
93
+ nexus-plugin create my-frame-plugin --sdk-source ./package/aiot-nexus-plugin-sdk-3.2.0.tgz --install
94
94
  ```
95
95
 
96
96
  生成插件后,可以在 `src/manifest.json` 的业务入口中静态控制 Nexus 宿主状态栏。相关 manifest 片段如下,其余必填字段沿用脚手架生成值:
97
97
 
98
98
  ```json
99
99
  {
100
- "hostApiVersion": "3.1.0",
100
+ "hostApiVersion": "3.2.0",
101
101
  "businessEntries": [
102
102
  {
103
103
  "businessId": "my-frame-plugin",
@@ -115,7 +115,7 @@ nexus-plugin create my-frame-plugin --sdk-source ./package/aiot-nexus-plugin-sdk
115
115
 
116
116
  仅显式设置为 `false` 时隐藏。字段省略或设置为 `true` 时显示;离开该业务入口后,宿主状态栏自动恢复。该字段要求 `hostApiVersion >= 3.1.0`。
117
117
 
118
- ## Framework 3.1 mainEntry
118
+ ## Framework 3.2 mainEntry
119
119
 
120
120
  `mainEntry` 是可信 Electron 主进程代码,可直接导入 Electron、Node 内置模块和插件本地依赖:
121
121
 
@@ -205,6 +205,12 @@ script-src 'self'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:
205
205
 
206
206
  `createPluginFrameClient()` 的首次初始化默认等待 20 秒。业务请求默认不设超时;只有插件显式传入正数 `requestTimeoutMs` 时客户端才会主动超时。进度、取消和恢复仍建议使用 `taskId + getStatus + cancel`,但这是任务 UX 和资源管理策略,不是 worker 存活要求。通信契约见 [插件 IPC 与事件通知示例](../docs/plugin-ipc-event-notification-example.md)。
207
207
 
208
+ ### 宿主主题色
209
+
210
+ 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`。
211
+
212
+ SDK 升级本身只提供能力。插件需要在 CSS 中引用 `var(--nexus-color-primary, #8fc31f)` 等变量,或从 `context.theme?.dark.primary` 读取值,才会使用宿主主题色。旧宿主或缺少 `theme` 的上下文仍可正常初始化。自定义主题插件可以显式使用 `createPluginFrameClient({ autoApplyHostTheme: false })`,此时不注入变量、不设置宿主主题标记,但仍可手动读取 `context.theme`。
213
+
208
214
  ## SQLite 模板
209
215
 
210
216
  `sqlite` 模板默认打开 `pluginDataDirectory/plugin.db` 并初始化 `plugin_records`。工具栏“打开数据库”由 `mainEntry` 直接调用 `dialog.showOpenDialog()`;iframe 只接收数据库信息和参数化 operation。
@@ -247,7 +253,7 @@ VS Code 模板提供 `Debug Nexus plugin` 和 `Attach Nexus plugin main process
247
253
 
248
254
  ## 发布前检查
249
255
 
250
- 1. `hostApiVersion` 为 `3.1.0`,manifest schema 仍为 `1`。
256
+ 1. `hostApiVersion` 使用当前 `3.2.0`(或按实际能力保持旧版本),manifest schema 仍为 `1`。
251
257
  2. 业务入口的 `showHostStatusBar` 可静态声明是否保留宿主状态栏;省略时默认保留,显式使用时要求 `hostApiVersion >= 3.1.0`。
252
258
  3. `activate/deactivate` 同步,operation handler 才使用异步。
253
259
  4. Renderer 只使用窄口径 operation,所有输入在 mainEntry 再校验。
@@ -255,4 +261,5 @@ VS Code 模板提供 `Debug Nexus plugin` 和 `Attach Nexus plugin main process
255
261
  6. IPC、protocol、快捷键、菜单和文件路径使用插件命名空间并检测冲突。
256
262
  7. `deactivate` 关闭窗口、Session 监听、IPC、protocol、快捷键、定时器、数据库和子进程。
257
263
  8. 没有同步长任务、无限载荷、任意命令/SQL/路径透传或日志敏感信息。
258
- 9. 运行 `npm test`、插件构建、正式 Nexus 集成和 Windows 7 真机验收。
264
+ 9. 主题集成明确引用 `--nexus-color-*` / `context.theme`,或显式关闭 `autoApplyHostTheme`。
265
+ 10. 运行 `npm test`、插件构建、正式 Nexus 集成和 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,4 +1,4 @@
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
4
  export interface ParsedPluginVersion {
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
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";
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
7
  function parsePluginCompatibleVersion(value) {
@@ -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;
@@ -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;