@aiot-nexus/plugin-sdk 2.1.1 → 3.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.
Files changed (44) hide show
  1. package/README.md +197 -90
  2. package/dev-host/main.js +366 -505
  3. package/dist/.generated/plugin-framework-version.d.ts +1 -1
  4. package/dist/.generated/plugin-framework-version.js +1 -1
  5. package/dist/.generated/plugin-manifest.schema.json +3 -2
  6. package/dist/browser/frame-client.d.ts +1 -0
  7. package/dist/browser/frame-client.mjs +121 -13
  8. package/dist/src/browser/frame-client.d.ts +1 -0
  9. package/dist/src/browser/frame-client.js +121 -13
  10. package/dist/src/cli/run.js +81 -6
  11. package/dist/src/cli/update-notice.js +1 -1
  12. package/dist/src/dev.js +3 -3
  13. package/dist/src/{electron-facade.js → electron-namespaces.js} +2 -3
  14. package/dist/src/index.d.ts +2 -2
  15. package/dist/src/index.js +3 -3
  16. package/dist/src/nexus-dev-control.d.ts +1 -1
  17. package/dist/src/nexus-dev-control.js +2 -2
  18. package/dist/src/nexus-dev.d.ts +0 -3
  19. package/dist/src/nexus-dev.js +118 -53
  20. package/dist/src/scaffold.js +6 -5
  21. package/dist/src/sdk-info.d.ts +17 -0
  22. package/dist/src/sdk-info.js +23 -0
  23. package/dist/src/types.d.ts +15 -98
  24. package/dist/src/upgrade.d.ts +44 -0
  25. package/dist/src/upgrade.js +1189 -0
  26. package/dist/src/validation.js +9 -0
  27. package/package.json +2 -2
  28. package/scripts/sdk-update-check.js +1 -1
  29. package/templates/minimal/.vscode/launch.json +1 -1
  30. package/templates/minimal/README.md.tmpl +18 -17
  31. package/templates/minimal/src/main.ts.tmpl +5 -4
  32. package/templates/sqlite/.vscode/launch.json +1 -1
  33. package/templates/sqlite/README.md.tmpl +15 -13
  34. package/templates/sqlite/src/main.ts.tmpl +24 -20
  35. package/dev-host/plugin-host.js +0 -4
  36. package/dist/internal/src-electron/main/core/runtime-result.js +0 -42
  37. package/dist/internal/src-electron/main/plugins/plugin-electron-facade-broker.js +0 -890
  38. package/dist/internal/src-electron/plugin-host-bootstrap.js +0 -417
  39. package/dist/internal/src-electron/shared/contracts.js +0 -2
  40. package/dist/internal/src-electron/shared/plugin-electron-facade-client.js +0 -181
  41. package/dist/internal/src-electron/shared/plugin-electron-facade-descriptors.js +0 -70
  42. package/dist/internal/src-electron/shared/plugin-electron-facade.js +0 -243
  43. package/dist/internal/src-electron/shared/plugin-rpc.js +0 -245
  44. /package/dist/src/{electron-facade.d.ts → electron-namespaces.d.ts} +0 -0
@@ -34,6 +34,7 @@ const plugin_manifest_schema_json_1 = __importDefault(require("../.generated/plu
34
34
  const plugin_framework_version_1 = require("../.generated/plugin-framework-version");
35
35
  const manifestValidator = new _2020_1.default({ allErrors: true, strict: true })
36
36
  .compile(plugin_manifest_schema_json_1.default);
37
+ const hostStatusBarFrameworkVersion = "3.1.0";
37
38
  function validatePluginPackage(packageDirectory, options = {}) {
38
39
  const resolvedDirectory = path.resolve(packageDirectory);
39
40
  try {
@@ -85,6 +86,11 @@ function validateResolvedPluginPackage(resolvedDirectory, options) {
85
86
  if (!(0, plugin_framework_version_1.isPluginFrameworkVersionCompatible)(manifest.hostApiVersion, plugin_framework_version_1.PLUGIN_FRAMEWORK_VERSION)) {
86
87
  errors.push("Plugin Framework API version is incompatible with " + plugin_framework_version_1.PLUGIN_FRAMEWORK_VERSION + ".");
87
88
  }
89
+ const hostStatusBarVersionComparison = (0, plugin_framework_version_1.comparePluginVersions)(manifest.hostApiVersion, hostStatusBarFrameworkVersion);
90
+ if (usesHostStatusBarDeclaration(manifest)
91
+ && (hostStatusBarVersionComparison === undefined || hostStatusBarVersionComparison < 0)) {
92
+ errors.push("businessEntries.showHostStatusBar requires Plugin Framework API 3.1.0 or newer.");
93
+ }
88
94
  if (!(0, plugin_framework_version_1.parsePluginCompatibleVersion)(manifest.minHostVersion)) {
89
95
  errors.push("minHostVersion contains an unsupported numeric version.");
90
96
  }
@@ -141,6 +147,9 @@ function usesPluginFrame(manifest) {
141
147
  || (manifest.tools || []).some((entry) => entry.component === "PluginFrame")
142
148
  || (manifest.settingsSections || []).some((entry) => entry.component === "PluginFrame");
143
149
  }
150
+ function usesHostStatusBarDeclaration(manifest) {
151
+ return (manifest.businessEntries || []).some((entry) => (Object.prototype.hasOwnProperty.call(entry, "showHostStatusBar")));
152
+ }
144
153
  function isPathInside(rootDirectory, childPath) {
145
154
  const relative = path.relative(path.resolve(rootDirectory), path.resolve(childPath));
146
155
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiot-nexus/plugin-sdk",
3
- "version": "2.1.1",
3
+ "version": "3.1.0",
4
4
  "description": "Types, PluginFrame client, and CLI for AIoT Nexus independent plugins.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -38,7 +38,7 @@
38
38
  "postinstall": "node scripts/sdk-update-check.js --install",
39
39
  "verify:package": "node scripts/verify-package.js",
40
40
  "test:types": "tsc -p tsconfig.types-test.json",
41
- "test": "npm run build && npm run test:types && node tests/package.test.js && node tests/sdk-update-check.test.js && node tests/cli.test.js && node tests/cli-entry.test.js && node tests/update-notice.test.js && node tests/dev.test.js && node tests/nexus-dev.test.js && node tests/frame-client.test.js && node tests/web-dev.test.js && npm run verify:package",
41
+ "test": "npm run build && npm run test:types && node tests/package.test.js && node tests/sdk-update-check.test.js && node tests/cli.test.js && node tests/cli-entry.test.js && node tests/update-notice.test.js && node tests/upgrade.test.js && node tests/dev.test.js && node tests/nexus-dev.test.js && node tests/frame-client.test.js && node tests/web-dev.test.js && npm run verify:package",
42
42
  "prepack": "node scripts/prepack.js",
43
43
  "pack:sdk": "npm run build && npm run verify:package && node scripts/pack.js",
44
44
  "publish:sdk": "npm publish"
@@ -348,7 +348,7 @@ function formatUpdateNotice(result) {
348
348
  const lines = [
349
349
  "npm notice",
350
350
  "npm notice Update available for " + result.packageName + ": " + result.currentVersion + " -> " + result.latestVersion,
351
- "npm notice Run npm install --save-dev " + result.packageName + "@latest"
351
+ "npm notice Run npx --package=" + result.packageName + "@" + result.latestVersion + " nexus-plugin upgrade ."
352
352
  ];
353
353
  if (result.isMajorUpdate) {
354
354
  lines.push("npm notice Major update: first confirm Nexus Framework API compatibility.");
@@ -9,7 +9,7 @@
9
9
  "cwd": "${workspaceFolder}"
10
10
  },
11
11
  {
12
- "name": "Attach Nexus plugin worker (9229)",
12
+ "name": "Attach Nexus plugin main process (9229)",
13
13
  "type": "node",
14
14
  "request": "attach",
15
15
  "address": "127.0.0.1",
@@ -13,24 +13,25 @@ npm run dev:nexus -- --host C:\path\to\nexus.exe
13
13
 
14
14
  SDK checks for a newer `@aiot-nexus/plugin-sdk` in two steps. The package `postinstall` refreshes a user cache; npm 7+ hides dependency lifecycle output by default, so use `npm install --foreground-scripts` to see it. The `nexus-plugin` CLI reads that cache and prints npm-style notices when it is fresh; missing or older-than-24-hours cache data is checked in the background without blocking the command. `npm install --ignore-scripts` skips only the install check, while CLI checks remain available.
15
15
 
16
- The check follows the npm-configured registry, falls back to the official npm registry when needed, and silently ignores offline, timeout, malformed-response, or corrupt-cache failures. Use `NEXUS_PLUGIN_SDK_UPDATE_CHECK=0` or `NO_UPDATE_NOTIFIER=1` to disable both checks. A major SDK update requires confirming Nexus Framework API compatibility; upgrades are never automatic. When a notice appears, review compatibility and run `npm install --save-dev @aiot-nexus/plugin-sdk@latest` manually.
16
+ The check follows the npm-configured registry, falls back to the official npm registry when needed, and silently ignores offline, timeout, malformed-response, or corrupt-cache failures. Use `NEXUS_PLUGIN_SDK_UPDATE_CHECK=0` or `NO_UPDATE_NOTIFIER=1` to disable both checks. Notices never modify the project. When one appears, substitute the exact version printed by the notice for `X.Y.Z`: `npx --package=@aiot-nexus/plugin-sdk@X.Y.Z nexus-plugin upgrade . --check`, then repeat without `--check`. The transaction synchronizes package.json, package-lock.json, and node_modules, runs project checks, and rolls back on failure. A major update additionally requires `--allow-major` and the migration guide linked by the CLI; source and manifest files are never rewritten.
17
17
 
18
- The deployable plugin package is `dist/__PLUGIN_ID__/`. For a manual installation, stop Nexus before replacing `Plugins/__PLUGIN_ID__/`, then start it again. During development, use `npm run dev:nexus` so the host coordinates worker shutdown, atomic replacement, and worker reactivation without changing the Nexus PID.
18
+ The deployable plugin package is `dist/__PLUGIN_ID__/`. For a manual installation, stop Nexus before replacing `Plugins/__PLUGIN_ID__/`, then start it again. During development, use `npm run dev:nexus` so the CLI coordinates Nexus shutdown, atomic replacement, and restart for trusted `mainEntry` changes.
19
19
 
20
- ## Electron Facade file dialogs
20
+ ## Native Electron file dialogs
21
21
 
22
- `mainEntry` receives an asynchronous Electron 22 Facade through `context.host.electron`. A regular plugin can open native file, folder, and save dialogs without declaring `electronHostControl` or the legacy `dialog` permission:
22
+ Framework API 3.0 loads trusted `mainEntry` code in the Electron main process. Import Electron 22 directly to open native file, folder, and save dialogs; no Facade or dialog permission is required:
23
23
 
24
24
  ```ts
25
25
  import * as fs from "fs/promises";
26
+ import { dialog } from "electron";
26
27
  import type { NexusResult, PluginMainActivationContext } from "@aiot-nexus/plugin-sdk";
27
28
 
28
29
  const maximumTextFileBytes = 1024 * 1024;
29
30
 
30
31
  export async function chooseAndReadText(
31
- context: PluginMainActivationContext
32
+ _context: PluginMainActivationContext
32
33
  ): Promise<NexusResult<string | undefined>> {
33
- const result = await context.host.electron.dialog.showOpenDialog({
34
+ const result = await dialog.showOpenDialog({
34
35
  properties: ["openFile"],
35
36
  filters: [{ name: "Text", extensions: ["txt", "md", "json"] }]
36
37
  });
@@ -83,35 +84,35 @@ function failure(code: string, message: string): NexusResult<never> {
83
84
  return { ok: false, error: { code, message } };
84
85
  }
85
86
 
86
- export async function chooseFolder(context: PluginMainActivationContext): Promise<string | undefined> {
87
- const result = await context.host.electron.dialog.showOpenDialog({
87
+ export async function chooseFolder(): Promise<string | undefined> {
88
+ const result = await dialog.showOpenDialog({
88
89
  properties: ["openDirectory", "createDirectory"]
89
90
  });
90
91
  return result.canceled ? undefined : result.filePaths[0];
91
92
  }
92
93
 
93
- export async function chooseSavePath(context: PluginMainActivationContext): Promise<string | undefined> {
94
- const result = await context.host.electron.dialog.showSaveDialog({ defaultPath: "document.txt" });
94
+ export async function chooseSavePath(): Promise<string | undefined> {
95
+ const result = await dialog.showSaveDialog({ defaultPath: "document.txt" });
95
96
  return result.canceled ? undefined : result.filePath;
96
97
  }
97
98
  ```
98
99
 
99
- Call these helpers from an operation registered with `context.host.registerRendererMethod`, then let the renderer invoke that narrow operation through `client.invokeMain()`. Return the `NexusResult` from `chooseAndReadText` directly so the renderer receives the stable `FileNotReadable`, `FileTooLarge`, or `FileReadFailed` code. Do not expose the Facade or unrestricted file paths to the iframe. For larger files, stream or paginate in `mainEntry` and return only bounded chunks or summaries instead of the whole file. `npm run dev:web` is renderer-only and deliberately has no `mainEntry`, Node filesystem, or Electron Facade; use `npm run dev` or a formal Nexus host to test these dialogs.
100
+ Call these helpers from an operation registered with `context.host.registerRendererMethod`, then let the renderer invoke that narrow operation through `client.invokeMain()`. Return the `NexusResult` from `chooseAndReadText` directly so the renderer receives the stable `FileNotReadable`, `FileTooLarge`, or `FileReadFailed` code. Do not expose Electron objects or unrestricted file paths to the iframe. For larger files, stream or paginate in `mainEntry` and return only bounded chunks or summaries instead of the whole file. `npm run dev:web` is renderer-only and deliberately has no `mainEntry`, Node filesystem, or Electron main-process API; use `npm run dev` or a formal Nexus host to test these dialogs.
100
101
 
101
- The renderer calls the plugin-worker `demo.echo` operation through `@aiot-nexus/plugin-sdk`. The worker also calls `context.host.electron.app.getName()` during activation, so `npm run dev` verifies the real asynchronous Electron Facade path. Run `npm run validate` after changing the manifest or output layout.
102
+ The renderer calls the trusted-main `demo.echo` operation through `@aiot-nexus/plugin-sdk`. The main entry also calls the directly imported `app.getName()` during activation, so `npm run dev` verifies the real Electron main-process path. Run `npm run validate` after changing the manifest or output layout.
102
103
 
103
104
  The renderer follows `context.language` and `context.themeMode` from Nexus, subscribes to in-place context updates without reloading the iframe, and includes `en-US` and `zh-CN` message catalogs. The plugin page has no language or theme selector; the controls shown by `npm run dev` belong to the debug host and simulate Nexus context changes. Below that toolbar, the iframe has no host padding, border, minimum size, or content-driven height and fills the remaining host viewport. Add or edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
104
105
 
105
106
  The current SDK has no generic business-event subscription API. For a long-running operation, register a fast `job.start` operation that returns a `taskId`, then expose `job.getStatus` and optional `job.cancel` for renderer polling. See the Nexus repository's plugin IPC and event notification example for the full contract and code.
106
107
 
107
- Use `npm run dev:web` for renderer-only UI work in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. It does not execute `mainEntry`, create a worker, provide Electron Facade, or overwrite the deployable `dist` package. Language and theme controls still simulate Nexus context, while `invokeMain` is intentionally unavailable. Pass `-- --no-open` or `-- --port <port>` when needed; use `npm run dev` for Host API and Electron integration.
108
+ Use `npm run dev:web` for renderer-only UI work in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. It does not execute `mainEntry`, provide Node or Electron main-process APIs, or overwrite the deployable `dist` package. Language and theme controls still simulate Nexus context, while `invokeMain` is intentionally unavailable. Pass `-- --no-open` or `-- --port <port>` when needed; use `npm run dev` for Host API and Electron integration.
108
109
 
109
- Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` for live development against a compiled, packaged Nexus. The CLI atomically deploys this plugin to `<exeDir>/Plugins/__PLUGIN_ID__/`. Renderer changes rebuild and reload the iframe without changing the Nexus PID; safe manifest changes rescan contributions in the same process; `mainEntry`, dependency, native-module, or worker-sensitive manifest changes rebuild the full package, stop the old plugin worker, replace the package, and start a new worker while keeping the Nexus PID. Failed builds keep the previous deployment; a failed worker reload rolls the package back and restores the previous worker. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
110
+ Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` for live development against a compiled, packaged Nexus. The CLI atomically deploys this plugin to `<exeDir>/Plugins/__PLUGIN_ID__/`. Renderer changes rebuild and reload the iframe without changing the Nexus PID; safe manifest changes rescan contributions in the same process; `mainEntry`, dependency, native-module, or restart-sensitive manifest changes stop Nexus, replace the complete package, and start a new Nexus process because trusted modules cannot be unloaded safely from Node's module cache. Failed builds keep the previous deployment; launch or readiness failures restore the previous package and host. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
110
111
 
111
- When this project was created from a local SDK archive, `vendor/aiot-nexus-plugin-sdk.tgz` is part of the source project and must be kept when the project is moved or committed.
112
+ When this project was created from a local SDK archive, `vendor/aiot-nexus-plugin-sdk.tgz` is part of the source project and must be kept when the project is moved or committed. An explicit local upgrade copies the target archive to `vendor/aiot-nexus-plugin-sdk-<version>.tgz` and updates the exact file dependency.
112
113
 
113
- For a local SDK archive, the tgz version remains the plugin's current SDK version for comparison. It is not replaced automatically by a newer npm package; fully offline work can disable checks with the environment variables above.
114
+ For a local SDK archive, the tgz version remains the plugin's current SDK version for comparison. It is not replaced automatically by a newer npm package; pass the target archive to both npx and `nexus-plugin upgrade --sdk-source <archive>`. Fully offline work can disable checks with the environment variables above.
114
115
 
115
- For plugin-worker breakpoints, open this plugin directory in VS Code, set breakpoints in `src/main.ts`, and run `Debug Nexus plugin`. If child-process auto attach is disabled, start `npm run dev:debug` and select `Attach Nexus plugin worker (9229)`. Renderer DevTools opens automatically, with `src/renderer/app.ts` available through the development source map. `npm run dev` rebuilds before launch; use `npm run dev -- --no-build` only when intentionally reusing the current `dist` output.
116
+ For trusted-main breakpoints, open this plugin directory in VS Code, set breakpoints in `src/main.ts`, and run `Debug Nexus plugin`. To attach manually, start `npm run dev:debug` and select `Attach Nexus plugin main process (9229)`. Renderer DevTools opens automatically, with `src/renderer/app.ts` available through the development source map. `npm run dev` rebuilds before launch; use `npm run dev -- --no-build` only when intentionally reusing the current `dist` output.
116
117
 
117
118
  The independent host stores data under `.nexus-plugin-dev/` and mirrors the PluginFrame protocol and current Host API surface. Its simulated Nexus application version defaults to the version recorded by the SDK build; pass `npm run dev -- --host-version <X.Y.Z>` to exercise another compatibility branch. Run the deployable `dist/__PLUGIN_ID__/` in Nexus before delivery because the standalone host does not reproduce the full application shell or routing state.
@@ -1,8 +1,9 @@
1
+ import { app } from "electron";
1
2
  import type { PluginMainActivationContext } from "@aiot-nexus/plugin-sdk";
2
3
 
3
- export async function activate(context: PluginMainActivationContext): Promise<void> {
4
- const applicationName = await context.host.electron.app.getName();
5
- const registration = await context.host.registerRendererMethod("demo.echo", (request) => {
4
+ export function activate(context: PluginMainActivationContext): void {
5
+ const applicationName = app.getName();
6
+ const registration = context.host.registerRendererMethod("demo.echo", (request) => {
6
7
  const value = request.input && typeof request.input.value === "string"
7
8
  ? request.input.value.trim()
8
9
  : "";
@@ -20,7 +21,7 @@ export async function activate(context: PluginMainActivationContext): Promise<vo
20
21
  if (!registration.ok) {
21
22
  throw new Error(registration.error.message);
22
23
  }
23
- const logResult = await context.host.log("info", "__PLUGIN_ID__ activated in " + applicationName + ".");
24
+ const logResult = context.host.log("info", "__PLUGIN_ID__ activated in " + applicationName + ".");
24
25
  if (!logResult.ok) {
25
26
  throw new Error(logResult.error.message);
26
27
  }
@@ -9,7 +9,7 @@
9
9
  "cwd": "${workspaceFolder}"
10
10
  },
11
11
  {
12
- "name": "Attach Nexus plugin worker (9229)",
12
+ "name": "Attach Nexus plugin main process (9229)",
13
13
  "type": "node",
14
14
  "request": "attach",
15
15
  "address": "127.0.0.1",
@@ -13,20 +13,22 @@ npm run dev:nexus -- --host C:\path\to\nexus.exe
13
13
 
14
14
  SDK checks for a newer `@aiot-nexus/plugin-sdk` in two steps. The package `postinstall` refreshes a user cache; npm 7+ hides dependency lifecycle output by default, so use `npm install --foreground-scripts` to see it. The `nexus-plugin` CLI reads that cache and prints npm-style notices when it is fresh; missing or older-than-24-hours cache data is checked in the background without blocking the command. `npm install --ignore-scripts` skips only the install check, while CLI checks remain available.
15
15
 
16
- The check follows the npm-configured registry, falls back to the official npm registry when needed, and silently ignores offline, timeout, malformed-response, or corrupt-cache failures. Use `NEXUS_PLUGIN_SDK_UPDATE_CHECK=0` or `NO_UPDATE_NOTIFIER=1` to disable both checks. A major SDK update requires confirming Nexus Framework API compatibility; upgrades are never automatic. When a notice appears, review compatibility and run `npm install --save-dev @aiot-nexus/plugin-sdk@latest` manually.
16
+ The check follows the npm-configured registry, falls back to the official npm registry when needed, and silently ignores offline, timeout, malformed-response, or corrupt-cache failures. Use `NEXUS_PLUGIN_SDK_UPDATE_CHECK=0` or `NO_UPDATE_NOTIFIER=1` to disable both checks. Notices never modify the project. When one appears, substitute the exact version printed by the notice for `X.Y.Z`: `npx --package=@aiot-nexus/plugin-sdk@X.Y.Z nexus-plugin upgrade . --check`, then repeat without `--check`. The transaction synchronizes package.json, package-lock.json, and node_modules, runs project checks (including the Electron 22 native rebuild), and rolls back on failure. A major update additionally requires `--allow-major` and the migration guide linked by the CLI; source and manifest files are never rewritten.
17
17
 
18
18
  Every `npm run build` rebuilds `better-sqlite3@8.7.0` for Electron 22.3.27 with `@electron/rebuild@3.7.2` before packaging. `npm run rebuild:native` remains available as a standalone diagnostic command.
19
19
 
20
- The deployable plugin package is `dist/__PLUGIN_ID__/`. For a manual installation, stop Nexus before replacing `Plugins/__PLUGIN_ID__/`, then start it again. During development, use `npm run dev:nexus` so the host coordinates worker shutdown, atomic replacement, and worker reactivation without changing the Nexus PID. Runtime data is stored in the host-provided plugin data directory and is not written into the plugin package.
20
+ The deployable plugin package is `dist/__PLUGIN_ID__/`. For a manual installation, stop Nexus before replacing `Plugins/__PLUGIN_ID__/`, then start it again. During development, use `npm run dev:nexus` so the CLI coordinates Nexus shutdown, atomic replacement, and restart for trusted `mainEntry` changes. Runtime data is stored in the host-provided plugin data directory and is not written into the plugin package.
21
21
 
22
- The template is a schema-aware SQLite database browser. It opens `plugin.db` in the plugin data directory by default and initializes the `plugin_records` sample table there. **Open Database** calls `context.host.electron.dialog.showOpenDialog()` from `mainEntry`; it accepts a single existing SQLite file, including files without an extension. A regular plugin can use this dialog Facade without `electronHostControl`. The renderer only displays the selected path and invokes parameterized operations. It never receives an Electron object and does not expose a path input, arbitrary SQL, DDL, `ATTACH`, or a SQL console.
22
+ The template is a schema-aware SQLite database browser. It opens `plugin.db` in the plugin data directory by default and initializes the `plugin_records` sample table there. **Open Database** calls Electron's directly imported `dialog.showOpenDialog()` from trusted `mainEntry`; it accepts a single existing SQLite file, including files without an extension. The renderer only displays the selected path and invokes parameterized operations. It never receives an Electron object and does not expose a path input, arbitrary SQL, DDL, `ATTACH`, or a SQL console.
23
23
 
24
- ## Electron Facade database dialog
24
+ ## Native Electron database dialog
25
25
 
26
- The template's `sqlite.chooseDatabase` operation uses the same asynchronous Electron Facade available to other v2 plugins. The essential call in `src/main.ts` is:
26
+ Framework API 3.0 loads trusted `mainEntry` code in the Electron main process. The `sqlite.chooseDatabase` operation imports Electron 22 directly; the essential call in `src/main.ts` is:
27
27
 
28
28
  ```ts
29
- const result = await context.host.electron.dialog.showOpenDialog({
29
+ import { dialog } from "electron";
30
+
31
+ const result = await dialog.showOpenDialog({
30
32
  title: "Open SQLite database",
31
33
  properties: ["openFile"],
32
34
  filters: [
@@ -41,24 +43,24 @@ if (!result.canceled && result.filePaths[0]) {
41
43
  }
42
44
  ```
43
45
 
44
- For related workflows, use `properties: ["openDirectory", "createDirectory"]` to choose a folder or `context.host.electron.dialog.showSaveDialog()` to choose a save path. These calls belong in `mainEntry`; the renderer should invoke a registered business operation such as `sqlite.chooseDatabase` through `client.invokeMain()` and receive only the result it needs. No `electronHostControl` or legacy `dialog` permission is required. Native dialogs are unavailable in `npm run dev:web`; use `npm run dev` or a formal Nexus host.
46
+ For related workflows, use `properties: ["openDirectory", "createDirectory"]` to choose a folder or `dialog.showSaveDialog()` to choose a save path. These calls belong in trusted `mainEntry`; the renderer should invoke a registered business operation such as `sqlite.chooseDatabase` through `client.invokeMain()` and receive only the result it needs. Native dialogs are unavailable in `npm run dev:web`; use `npm run dev` or a formal Nexus host.
45
47
 
46
48
  External files are opened read/write only after a separate candidate connection can query `sqlite_schema`. Canceling the dialog or selecting a corrupt, locked, or inaccessible file keeps the current connection. The latest external path is atomically stored as `sqlite-browser-state.json` in the plugin data directory. Startup restores it when valid; otherwise the template clears the saved selection, falls back to `plugin.db`, and returns a visible notice. External databases do not receive the sample schema or journal pragma.
47
49
 
48
- The table selector lists ordinary user tables from the `main` schema. Dynamic headers show declared type, SQLite affinity, primary-key order, NULL, default, and generated-column metadata. The editor supports default/NULL/value modes for inserts and unchanged/NULL/value modes for updates. INTEGER input uses exact decimal integers, REAL must be finite, NUMERIC accepts integer, real, or text values, and BLOB input uses even-length hexadecimal text. The 1 MiB field and 4 MiB row limits count decoded BLOB bytes rather than hexadecimal characters. Ordinary rowid tables prefer an unshadowed built-in rowid; `WITHOUT ROWID` tables, or tables that shadow every rowid alias, fall back only to primary-key columns that are all non-NULL. A table without a unique locator remains browsable and insertable, but update and delete are unavailable. Primary-key locators larger than 16 KiB are also treated as browse-only so a page cannot copy unbounded keys into the plugin worker and iframe. Update and delete run in a transaction and must affect exactly one row. TEXT previews are capped at 16 KiB of UTF-8 data; BLOB previews show the byte length and first 32 bytes.
50
+ The table selector lists ordinary user tables from the `main` schema. Dynamic headers show declared type, SQLite affinity, primary-key order, NULL, default, and generated-column metadata. The editor supports default/NULL/value modes for inserts and unchanged/NULL/value modes for updates. INTEGER input uses exact decimal integers, REAL must be finite, NUMERIC accepts integer, real, or text values, and BLOB input uses even-length hexadecimal text. The 1 MiB field and 4 MiB row limits count decoded BLOB bytes rather than hexadecimal characters. Ordinary rowid tables prefer an unshadowed built-in rowid; `WITHOUT ROWID` tables, or tables that shadow every rowid alias, fall back only to primary-key columns that are all non-NULL. A table without a unique locator remains browsable and insertable, but update and delete are unavailable. Primary-key locators larger than 16 KiB are also treated as browse-only so a page cannot copy unbounded keys into the trusted main process and iframe. Update and delete run in a transaction and must affect exactly one row. TEXT previews are capped at 16 KiB of UTF-8 data; BLOB previews show the byte length and first 32 bytes.
49
51
 
50
52
  The renderer follows `context.language` and `context.themeMode` from Nexus and preserves unfinished editor values during in-place context updates. The plugin page has no language or theme selector; the controls shown by `npm run dev` belong to the debug host. Below that toolbar, the iframe has no host padding, border, minimum size, or content-driven height and fills the remaining host viewport. This SDK template deliberately has no Quasar dependency, so its data grid uses a native `<table>` as a dependency-minimized template exception. It still provides pagination, contained horizontal scrolling, light/dark themes, `en-US` / `zh-CN`, and busy/empty/error states. Edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
51
53
 
52
54
  The current SDK has no generic business-event subscription API. For a long-running operation, register a fast `job.start` operation that returns a `taskId`, then expose `job.getStatus` and optional `job.cancel` for renderer polling. See the Nexus repository's plugin IPC and event notification example for the full contract and code.
53
55
 
54
- Use `npm run dev:web` for renderer-only layout, styling, localization, and responsive checks in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, and does not rebuild or load `better-sqlite3`, execute `mainEntry`, create a worker, provide Electron Facade, copy native runtime files, or overwrite the deployable `dist` package. It builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. The page explicitly reports `PluginCapabilityUnsupported`; file selection, table loading, and CRUD require `npm run dev` or a formal Nexus host. Pass `-- --no-open` or `-- --port <port>` when needed.
56
+ Use `npm run dev:web` for renderer-only layout, styling, localization, and responsive checks in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, and does not rebuild or load `better-sqlite3`, execute `mainEntry`, provide Node or Electron main-process APIs, copy native runtime files, or overwrite the deployable `dist` package. It builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. The page explicitly reports `PluginCapabilityUnsupported`; file selection, table loading, and CRUD require `npm run dev` or a formal Nexus host. Pass `-- --no-open` or `-- --port <port>` when needed.
55
57
 
56
- Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` to run the plugin inside a compiled, packaged Nexus. The initial deployment and every full package replacement are atomic. Renderer-only and safe manifest updates keep the Nexus PID; `mainEntry`, dependency, native-module, or worker-sensitive manifest changes run the complete build (including the Electron 22.3.27 native rebuild), stop the old plugin worker, replace the package, and start a new worker in the same Nexus process. The database remains under the host-provided plugin data directory across worker replacements. Failed builds keep the previous deployment, and a failed worker reload rolls the package back and restores the previous worker. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
58
+ Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` to run the plugin inside a compiled, packaged Nexus. The initial deployment and every full package replacement are atomic. Renderer-only and safe manifest updates keep the Nexus PID; `mainEntry`, dependency, native-module, or restart-sensitive manifest changes run the complete build (including the Electron 22.3.27 native rebuild), stop Nexus, replace the package, and start a new Nexus process. The database remains under the host-provided plugin data directory across host restarts. Failed builds keep the previous deployment, and launch or readiness failures roll the package back and restore the previous host. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
57
59
 
58
- When this project was created from a local SDK archive, `vendor/aiot-nexus-plugin-sdk.tgz` is part of the source project and must be kept when the project is moved or committed.
60
+ When this project was created from a local SDK archive, `vendor/aiot-nexus-plugin-sdk.tgz` is part of the source project and must be kept when the project is moved or committed. An explicit local upgrade copies the target archive to `vendor/aiot-nexus-plugin-sdk-<version>.tgz` and updates the exact file dependency.
59
61
 
60
- For a local SDK archive, the tgz version remains the plugin's current SDK version for comparison. It is not replaced automatically by a newer npm package; fully offline work can disable checks with the environment variables above.
62
+ For a local SDK archive, the tgz version remains the plugin's current SDK version for comparison. It is not replaced automatically by a newer npm package; pass the target archive to both npx and `nexus-plugin upgrade --sdk-source <archive>`. Fully offline work can disable checks with the environment variables above.
61
63
 
62
- For plugin-worker breakpoints, open this plugin directory in VS Code, set breakpoints in `src/main.ts`, and run `Debug Nexus plugin`. If child-process auto attach is disabled, start `npm run dev:debug` and select `Attach Nexus plugin worker (9229)`. Renderer DevTools opens automatically, with `src/renderer/app.ts` available through the development source map. The default debug build also runs the Electron 22.3.27 native rebuild before launch. Use `npm run dev -- --no-build` only when intentionally reusing a compatible current `dist` output.
64
+ For trusted-main breakpoints, open this plugin directory in VS Code, set breakpoints in `src/main.ts`, and run `Debug Nexus plugin`. To attach manually, start `npm run dev:debug` and select `Attach Nexus plugin main process (9229)`. Renderer DevTools opens automatically, with `src/renderer/app.ts` available through the development source map. The default debug build also runs the Electron 22.3.27 native rebuild before launch. Use `npm run dev -- --no-build` only when intentionally reusing a compatible current `dist` output.
63
65
 
64
66
  The independent host stores the SQLite database and settings under `.nexus-plugin-dev/data/` and mirrors the PluginFrame protocol and current Host API surface. Its simulated Nexus application version defaults to the version recorded by the SDK build; pass `npm run dev -- --host-version <X.Y.Z>` to exercise another compatibility branch. Run the deployable `dist/__PLUGIN_ID__/` in Nexus before delivery because the standalone host does not reproduce the full application shell or routing state.
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import Database = require("better-sqlite3");
4
+ import { app, dialog } from "electron";
4
5
  import type {
5
6
  NexusResult,
6
7
  PluginMainActivationContext,
@@ -149,24 +150,24 @@ let stateFilePath = "";
149
150
  let databaseNotice: string | undefined;
150
151
  let databaseSelectionPending = false;
151
152
 
152
- export async function activate(context: PluginMainActivationContext): Promise<void> {
153
+ export function activate(context: PluginMainActivationContext): void {
153
154
  activationContext = context;
154
155
  fs.mkdirSync(context.host.pluginDataDirectory, { recursive: true });
155
156
  defaultDatabasePath = path.join(context.host.pluginDataDirectory, defaultDatabaseName);
156
157
  stateFilePath = path.join(context.host.pluginDataDirectory, stateFileName);
157
158
  activeDatabase = openInitialDatabase();
158
159
 
159
- await register(context, "sqlite.getDatabaseInfo", handled(getDatabaseInfo));
160
- await register(context, "sqlite.chooseDatabase", handledAsync(chooseDatabase));
161
- await register(context, "sqlite.useDefaultDatabase", handledAsync(useDefaultDatabase));
162
- await register(context, "sqlite.listTables", handled(listTables));
163
- await register(context, "sqlite.readTable", handled(readTable));
164
- await register(context, "sqlite.insertRow", handled(insertRow));
165
- await register(context, "sqlite.updateRow", handled(updateRow));
166
- await register(context, "sqlite.deleteRow", handled(deleteRow));
167
-
168
- const applicationName = await context.host.electron.app.getName();
169
- const logResult = await context.host.log(
160
+ register(context, "sqlite.getDatabaseInfo", handled(getDatabaseInfo));
161
+ register(context, "sqlite.chooseDatabase", handledAsync(chooseDatabase));
162
+ register(context, "sqlite.useDefaultDatabase", handledAsync(useDefaultDatabase));
163
+ register(context, "sqlite.listTables", handled(listTables));
164
+ register(context, "sqlite.readTable", handled(readTable));
165
+ register(context, "sqlite.insertRow", handled(insertRow));
166
+ register(context, "sqlite.updateRow", handled(updateRow));
167
+ register(context, "sqlite.deleteRow", handled(deleteRow));
168
+
169
+ const applicationName = app.getName();
170
+ const logResult = context.host.log(
170
171
  "info",
171
172
  "__PLUGIN_ID__ database " + (activeDatabase.created ? "created" : "loaded") + " in " + applicationName + ": " + activeDatabase.databasePath
172
173
  );
@@ -182,8 +183,8 @@ export function deactivate(): void {
182
183
  databaseSelectionPending = false;
183
184
  }
184
185
 
185
- async function register(context: PluginMainActivationContext, name: string, handler: PluginRendererOperationHandler): Promise<void> {
186
- const result = await context.host.registerRendererMethod(name, handler);
186
+ function register(context: PluginMainActivationContext, name: string, handler: PluginRendererOperationHandler): void {
187
+ const result = context.host.registerRendererMethod(name, handler);
187
188
  if (!result.ok) {
188
189
  throw new Error(result.error.message);
189
190
  }
@@ -286,11 +287,11 @@ async function chooseDatabase(): Promise<{ canceled: boolean; database: Database
286
287
  if (databaseSelectionPending) {
287
288
  throw codedError("Busy", "A database selection dialog is already open.");
288
289
  }
289
- const context = requireActivationContext();
290
+ requireActivationContext();
290
291
  databaseSelectionPending = true;
291
292
  try {
292
293
  const current = requireDatabase();
293
- const result = await context.host.electron.dialog.showOpenDialog({
294
+ const result = await dialog.showOpenDialog({
294
295
  title: "Open SQLite database",
295
296
  defaultPath: path.dirname(current.databasePath),
296
297
  properties: ["openFile"],
@@ -311,7 +312,7 @@ async function chooseDatabase(): Promise<{ canceled: boolean; database: Database
311
312
  candidate.connection.close();
312
313
  throw error;
313
314
  }
314
- await logDatabaseSwitch("external", path.basename(candidate.databasePath));
315
+ logDatabaseSwitch("external", path.basename(candidate.databasePath));
315
316
  return { canceled: false, database: getDatabaseInfo() };
316
317
  } finally {
317
318
  databaseSelectionPending = false;
@@ -328,16 +329,19 @@ async function useDefaultDatabase(): Promise<{ database: DatabaseInfo }> {
328
329
  candidate.connection.close();
329
330
  throw error;
330
331
  }
331
- await logDatabaseSwitch("default", defaultDatabaseName);
332
+ logDatabaseSwitch("default", defaultDatabaseName);
332
333
  return { database: getDatabaseInfo() };
333
334
  }
334
335
 
335
- async function logDatabaseSwitch(source: DatabaseSource, databaseName: string): Promise<void> {
336
+ function logDatabaseSwitch(source: DatabaseSource, databaseName: string): void {
336
337
  const context = activationContext;
337
338
  if (!context) {
338
339
  return;
339
340
  }
340
- await context.host.log("info", "__PLUGIN_ID__ switched to " + source + " database: " + databaseName);
341
+ const result = context.host.log("info", "__PLUGIN_ID__ switched to " + source + " database: " + databaseName);
342
+ if (!result.ok) {
343
+ throw new Error(result.error.message);
344
+ }
341
345
  }
342
346
 
343
347
  function replaceDatabase(candidate: ActiveDatabase): void {
@@ -1,4 +0,0 @@
1
- "use strict";
2
- /* eslint-disable @typescript-eslint/no-var-requires */
3
-
4
- require("../dist/internal/src-electron/plugin-host-bootstrap.js");
@@ -1,42 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fail = exports.ok = exports.StatusCodes = void 0;
4
- exports.StatusCodes = {
5
- InvalidRequest: "InvalidRequest",
6
- NotFound: "NotFound",
7
- UnexpectedFailure: "UnexpectedFailure",
8
- ExternalAppDefinitionMissing: "ExternalAppDefinitionMissing",
9
- ExternalAppDisabled: "ExternalAppDisabled",
10
- ExternalAppLaunchSettingsMissing: "ExternalAppLaunchSettingsMissing",
11
- ExternalAppExecutableNotConfigured: "ExternalAppExecutableNotConfigured",
12
- ExternalAppExecutableInvalid: "ExternalAppExecutableInvalid",
13
- ExternalAppExecutableMissing: "ExternalAppExecutableMissing",
14
- ExternalAppWorkingDirectoryInvalid: "ExternalAppWorkingDirectoryInvalid",
15
- ExternalAppWorkingDirectoryMissing: "ExternalAppWorkingDirectoryMissing",
16
- ExternalAppStartFailed: "ExternalAppStartFailed",
17
- DirectoryMissing: "DirectoryMissing",
18
- PluginBlocked: "PluginBlocked",
19
- PluginManifestInvalid: "PluginManifestInvalid",
20
- PluginIncompatible: "PluginIncompatible",
21
- PluginPermissionDenied: "PluginPermissionDenied",
22
- PluginCapabilityUnsupported: "PluginCapabilityUnsupported",
23
- PluginMainEntryFailed: "PluginMainEntryFailed",
24
- ToolFileReadFailed: "ToolFileReadFailed",
25
- XxbpDeferred: "XxbpDeferred",
26
- XxbpInvalidRemotePath: "XxbpInvalidRemotePath",
27
- XxbpToolUnavailable: "XxbpToolUnavailable",
28
- XxbpCommandFailed: "XxbpCommandFailed",
29
- XxbpCommandTimeout: "XxbpCommandTimeout",
30
- XxbpOperationCancelled: "XxbpOperationCancelled",
31
- XxbpDevicesSessionMissing: "XxbpDevicesSessionMissing",
32
- XxbpResultFileMissing: "XxbpResultFileMissing",
33
- XxbpNonTextFile: "XxbpNonTextFile"
34
- };
35
- function ok(value) {
36
- return { ok: true, value };
37
- }
38
- exports.ok = ok;
39
- function fail(code, message, details) {
40
- return { ok: false, error: { code, message, details } };
41
- }
42
- exports.fail = fail;