@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 +20 -13
- package/dev-host/main.js +20 -5
- package/dist/.generated/plugin-framework-version.d.ts +1 -1
- package/dist/.generated/plugin-framework-version.js +1 -1
- package/dist/.generated/theme-contract.d.ts +43 -0
- package/dist/.generated/theme-contract.js +75 -0
- package/dist/browser/frame-client.d.ts +1 -0
- package/dist/browser/frame-client.mjs +197 -0
- package/dist/src/browser/frame-client.d.ts +1 -0
- package/dist/src/browser/frame-client.js +197 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +3 -1
- package/dist/src/sdk-info.d.ts +1 -1
- package/dist/src/sdk-info.js +1 -1
- package/dist/src/types.d.ts +37 -0
- package/dist/src/web-dev.js +2 -0
- package/package.json +1 -1
- package/templates/minimal/README.md.tmpl +4 -0
- package/templates/minimal/src/renderer/app.ts.tmpl +4 -1
- package/templates/minimal/src/renderer/plugin.css +78 -52
- package/templates/sqlite/README.md.tmpl +4 -0
- package/templates/sqlite/src/renderer/app.ts.tmpl +4 -1
- package/templates/sqlite/src/renderer/plugin.css +135 -57
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @aiot-nexus/plugin-sdk
|
|
2
2
|
|
|
3
|
-
> 当前 SDK 包版本为 `3.
|
|
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.
|
|
12
|
-
| Framework API | `3.
|
|
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.
|
|
52
|
-
npx --package=@aiot-nexus/plugin-sdk@3.
|
|
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.
|
|
59
|
-
--sdk-source ./aiot-nexus-plugin-sdk-3.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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`
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
+
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.
|
|
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;
|