@aiot-nexus/plugin-sdk 2.0.2 → 2.1.1

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 (32) hide show
  1. package/README.md +18 -8
  2. package/dev-host/host.html +13 -12
  3. package/dev-host/main.js +134 -9
  4. package/dev-host/plugin-host.js +1 -427
  5. package/dist/.generated/plugin-framework-version.d.ts +1 -1
  6. package/dist/.generated/plugin-framework-version.js +1 -1
  7. package/dist/.generated/plugin-manifest.schema.json +1 -1
  8. package/dist/internal/src-electron/main/core/runtime-result.js +42 -0
  9. package/dist/internal/src-electron/main/plugins/plugin-electron-facade-broker.js +890 -0
  10. package/dist/internal/src-electron/plugin-host-bootstrap.js +417 -0
  11. package/dist/internal/src-electron/shared/contracts.js +2 -0
  12. package/dist/internal/src-electron/shared/plugin-electron-facade-client.js +181 -0
  13. package/dist/internal/src-electron/shared/plugin-electron-facade-descriptors.js +70 -0
  14. package/dist/internal/src-electron/shared/plugin-electron-facade.js +243 -0
  15. package/dist/internal/src-electron/shared/plugin-rpc.js +245 -0
  16. package/dist/src/electron-facade.d.ts +4 -0
  17. package/dist/src/electron-facade.js +56 -0
  18. package/dist/src/index.d.ts +2 -1
  19. package/dist/src/index.js +2 -1
  20. package/dist/src/nexus-dev.js +5 -4
  21. package/dist/src/scaffold.js +2 -2
  22. package/dist/src/types.d.ts +86 -2
  23. package/dist/src/web-dev.js +2 -2
  24. package/package.json +7 -2
  25. package/templates/minimal/README.md.tmpl +84 -3
  26. package/templates/minimal/src/main.ts.tmpl +2 -1
  27. package/templates/sqlite/README.md.tmpl +30 -2
  28. package/templates/sqlite/src/better-sqlite3.d.ts +12 -1
  29. package/templates/sqlite/src/main.ts.tmpl +1034 -124
  30. package/templates/sqlite/src/renderer/app.ts.tmpl +691 -180
  31. package/templates/sqlite/src/renderer/index.html +40 -22
  32. package/templates/sqlite/src/renderer/plugin.css +302 -90
@@ -1,3 +1,5 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
1
3
  export interface NexusError {
2
4
  code: string;
3
5
  message: string;
@@ -12,8 +14,8 @@ export type NexusResult<T> = {
12
14
  };
13
15
  export type ThemeMode = "light" | "dark" | "system";
14
16
  export type LaunchPageParams = Record<string, string>;
15
- export type PluginPermission = "businessLog" | "settings" | "filesystem" | "udp" | "serial" | "externalProcess" | "dialog";
16
- export type PluginPermissionSupportState = "implemented" | "routeGuard" | "reserved";
17
+ export type PluginPermission = "businessLog" | "settings" | "filesystem" | "udp" | "serial" | "externalProcess" | "dialog" | "electronHostControl";
18
+ export type PluginPermissionSupportState = "implemented" | "hostControl" | "routeGuard" | "reserved";
17
19
  export interface PluginPermissionSummary {
18
20
  permission: PluginPermission;
19
21
  supportState: PluginPermissionSupportState;
@@ -111,6 +113,86 @@ export interface PluginUnsupportedCapabilityApi {
111
113
  unsupported(operation?: unknown): Promise<NexusResult<never>>;
112
114
  [operation: string]: unknown;
113
115
  }
116
+ type ElectronExports = typeof import("electron");
117
+ type PluginElectronFacadeScalar = string | number | boolean | bigint | null | undefined | Date | Buffer | Error;
118
+ type PluginElectronFacadeRemoteObject = import("electron").BrowserView | import("electron").BrowserWindow | import("electron").ClientRequest | import("electron").CommandLine | import("electron").Cookies | import("electron").Debugger | import("electron").DownloadItem | import("electron").Dock | import("electron").Event | import("electron").IncomingMessage | import("electron").IpcMainEvent | import("electron").IpcMainInvokeEvent | import("electron").Menu | import("electron").MenuItem | import("electron").MessageChannelMain | import("electron").MessagePortMain | import("electron").NativeImage | import("electron").Notification | import("electron").Session | import("electron").ServiceWorkers | import("electron").ShareMenu | import("electron").TouchBar | import("electron").TouchBarButton | import("electron").TouchBarColorPicker | import("electron").TouchBarGroup | import("electron").TouchBarLabel | import("electron").TouchBarOtherItemsProxy | import("electron").TouchBarPopover | import("electron").TouchBarScrubber | import("electron").TouchBarSegmentedControl | import("electron").TouchBarSlider | import("electron").TouchBarSpacer | import("electron").Tray | import("electron").UtilityProcess | import("electron").WebContents | import("electron").WebFrameMain | import("electron").WebRequest;
119
+ type PluginElectronFacadeArguments<T extends readonly unknown[]> = {
120
+ [K in keyof T]: PluginElectronFacadeArgument<T[K]>;
121
+ };
122
+ type PluginElectronFacadeArgument<T> = T extends PluginElectronFacadeScalar ? T : T extends PluginElectronFacadeRemoteObject ? PluginElectronFacadeModule<T> : T extends (...args: infer TArgs) => infer TResult ? (...args: PluginElectronFacadeArguments<TArgs>) => PluginElectronFacadeValue<TResult> | Promise<PluginElectronFacadeValue<TResult>> : T extends readonly unknown[] ? PluginElectronFacadeArguments<T> : T extends object ? {
123
+ [K in keyof T]: PluginElectronFacadeArgument<T[K]>;
124
+ } : T;
125
+ type PluginElectronFacadeValue<T> = T extends PromiseLike<infer TValue> ? PluginElectronFacadeValue<TValue> : T extends PluginElectronFacadeScalar ? T : T extends PluginElectronFacadeRemoteObject ? PluginElectronFacadeModule<T> : T extends readonly unknown[] ? {
126
+ [K in keyof T]: PluginElectronFacadeValue<T[K]>;
127
+ } : T extends object ? {
128
+ readonly [K in keyof T]: PluginElectronFacadeValue<T[K]>;
129
+ } : T;
130
+ type PluginElectronFacadeAsyncMembers<T> = {
131
+ readonly [K in keyof T as T[K] extends (...args: never[]) => unknown ? K : never]: T[K] extends (...args: infer TArgs) => infer TResult ? (...args: PluginElectronFacadeArguments<TArgs>) => Promise<PluginElectronFacadeValue<TResult>> : never;
132
+ };
133
+ type PluginElectronFacadeConstructed<T> = T extends abstract new (...args: never[]) => infer TInstance ? TInstance : Record<string, unknown>;
134
+ type PluginElectronFacadeConstructorArguments<T> = T extends abstract new (...args: infer TArgs) => unknown ? PluginElectronFacadeArguments<TArgs> : unknown[];
135
+ export type PluginElectronFacadeMethod<TArgs extends readonly unknown[] = unknown[], TResult = unknown> = (...args: PluginElectronFacadeArguments<TArgs>) => Promise<PluginElectronFacadeValue<TResult>>;
136
+ export interface PluginElectronFacadeSubscription {
137
+ readonly $typeName?: "ElectronEventSubscription" | string;
138
+ $release(): Promise<void>;
139
+ }
140
+ export interface PluginElectronFacadeEventSnapshot {
141
+ readonly defaultPrevented: boolean;
142
+ readonly preventDefaultSupported: false;
143
+ readonly senderId?: number | string;
144
+ }
145
+ export type PluginElectronFacadeModule<TModule = Record<string, unknown>> = PluginElectronFacadeAsyncMembers<TModule> & {
146
+ readonly $typeName?: string;
147
+ $construct(...args: PluginElectronFacadeConstructorArguments<TModule>): Promise<PluginElectronFacadeModule<PluginElectronFacadeConstructed<TModule>>>;
148
+ $get<TKey extends keyof TModule>(member: TKey): Promise<PluginElectronFacadeValue<TModule[TKey]>>;
149
+ $get(member: string): Promise<unknown>;
150
+ $set<TKey extends keyof TModule>(member: TKey, value: PluginElectronFacadeArgument<TModule[TKey]>): Promise<void>;
151
+ $set(member: string, value: unknown): Promise<void>;
152
+ $on(event: string, listener: (...args: unknown[]) => unknown): Promise<PluginElectronFacadeSubscription>;
153
+ $once(event: string, listener: (...args: unknown[]) => unknown): Promise<PluginElectronFacadeSubscription>;
154
+ $release(): Promise<void>;
155
+ };
156
+ export interface PluginElectronFacade {
157
+ readonly app: PluginElectronFacadeModule<ElectronExports["app"]>;
158
+ readonly autoUpdater: PluginElectronFacadeModule<ElectronExports["autoUpdater"]>;
159
+ readonly BrowserView: PluginElectronFacadeModule<ElectronExports["BrowserView"]>;
160
+ readonly BrowserWindow: PluginElectronFacadeModule<ElectronExports["BrowserWindow"]>;
161
+ readonly clipboard: PluginElectronFacadeModule<ElectronExports["clipboard"]>;
162
+ readonly contentTracing: PluginElectronFacadeModule<ElectronExports["contentTracing"]>;
163
+ readonly contextBridge: PluginElectronFacadeModule<ElectronExports["contextBridge"]>;
164
+ readonly crashReporter: PluginElectronFacadeModule<ElectronExports["crashReporter"]>;
165
+ readonly desktopCapturer: PluginElectronFacadeModule<ElectronExports["desktopCapturer"]>;
166
+ readonly dialog: PluginElectronFacadeModule<ElectronExports["dialog"]>;
167
+ readonly globalShortcut: PluginElectronFacadeModule<ElectronExports["globalShortcut"]>;
168
+ readonly inAppPurchase: PluginElectronFacadeModule<ElectronExports["inAppPurchase"]>;
169
+ readonly ipcMain: PluginElectronFacadeModule<ElectronExports["ipcMain"]>;
170
+ readonly ipcRenderer: PluginElectronFacadeModule<ElectronExports["ipcRenderer"]>;
171
+ readonly Menu: PluginElectronFacadeModule<ElectronExports["Menu"]>;
172
+ readonly MenuItem: PluginElectronFacadeModule<ElectronExports["MenuItem"]>;
173
+ readonly MessageChannelMain: PluginElectronFacadeModule<ElectronExports["MessageChannelMain"]>;
174
+ readonly nativeImage: PluginElectronFacadeModule<ElectronExports["nativeImage"]>;
175
+ readonly nativeTheme: PluginElectronFacadeModule<ElectronExports["nativeTheme"]>;
176
+ readonly net: PluginElectronFacadeModule<ElectronExports["net"]>;
177
+ readonly netLog: PluginElectronFacadeModule<ElectronExports["netLog"]>;
178
+ readonly Notification: PluginElectronFacadeModule<ElectronExports["Notification"]>;
179
+ readonly powerMonitor: PluginElectronFacadeModule<ElectronExports["powerMonitor"]>;
180
+ readonly powerSaveBlocker: PluginElectronFacadeModule<ElectronExports["powerSaveBlocker"]>;
181
+ readonly protocol: PluginElectronFacadeModule<ElectronExports["protocol"]>;
182
+ readonly pushNotifications: PluginElectronFacadeModule<ElectronExports["pushNotifications"]>;
183
+ readonly safeStorage: PluginElectronFacadeModule<ElectronExports["safeStorage"]>;
184
+ readonly screen: PluginElectronFacadeModule<ElectronExports["screen"]>;
185
+ readonly session: PluginElectronFacadeModule<ElectronExports["session"]>;
186
+ readonly ShareMenu: PluginElectronFacadeModule<ElectronExports["ShareMenu"]>;
187
+ readonly shell: PluginElectronFacadeModule<ElectronExports["shell"]>;
188
+ readonly systemPreferences: PluginElectronFacadeModule<ElectronExports["systemPreferences"]>;
189
+ readonly TouchBar: PluginElectronFacadeModule<ElectronExports["TouchBar"]>;
190
+ readonly Tray: PluginElectronFacadeModule<ElectronExports["Tray"]>;
191
+ readonly utilityProcess: PluginElectronFacadeModule<ElectronExports["utilityProcess"]>;
192
+ readonly webContents: PluginElectronFacadeModule<ElectronExports["webContents"]>;
193
+ readonly webFrame: PluginElectronFacadeModule<ElectronExports["webFrame"]>;
194
+ readonly webFrameMain: PluginElectronFacadeModule<ElectronExports["webFrameMain"]>;
195
+ }
114
196
  export interface PluginHostApi {
115
197
  readonly hostVersion: string;
116
198
  readonly hostApiVersion: string;
@@ -123,6 +205,7 @@ export interface PluginHostApi {
123
205
  log(level: PluginHostLogLevel, message: string, exception?: string): Promise<NexusResult<BusinessLogEntry>>;
124
206
  getSettings(): Promise<NexusResult<PluginSettingsDocument>>;
125
207
  updateSettings(values: Record<string, unknown>): Promise<NexusResult<PluginSettingsDocument>>;
208
+ readonly electron: PluginElectronFacade;
126
209
  filesystem: PluginUnsupportedCapabilityApi;
127
210
  udp: PluginUnsupportedCapabilityApi;
128
211
  serial: PluginUnsupportedCapabilityApi;
@@ -143,3 +226,4 @@ export interface PluginMainModule {
143
226
  deactivate?: (reason: string) => unknown;
144
227
  };
145
228
  }
229
+ export {};
@@ -34,8 +34,8 @@ const loopbackHost = "127.0.0.1";
34
34
  const defaultBodyLimit = 1024 * 1024;
35
35
  const browserApiPrefix = "/__nexus_plugin_dev";
36
36
  const pluginAssetPrefix = "/plugin/";
37
- const pluginContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'";
38
- const hostContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'";
37
+ const pluginContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'";
38
+ const hostContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; img-src 'self' data:; frame-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'";
39
39
  function runPluginWebDev(options, io) {
40
40
  const projectDirectory = path.resolve(options.projectDirectory);
41
41
  let packageKind = "auto";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiot-nexus/plugin-sdk",
3
- "version": "2.0.2",
3
+ "version": "2.1.1",
4
4
  "description": "Types, PluginFrame client, and CLI for AIoT Nexus independent plugins.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -37,7 +37,8 @@
37
37
  "build": "node scripts/build.js",
38
38
  "postinstall": "node scripts/sdk-update-check.js --install",
39
39
  "verify:package": "node scripts/verify-package.js",
40
- "test": "npm run build && 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",
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
42
  "prepack": "node scripts/prepack.js",
42
43
  "pack:sdk": "npm run build && npm run verify:package && node scripts/pack.js",
43
44
  "publish:sdk": "npm publish"
@@ -45,8 +46,12 @@
45
46
  "dependencies": {
46
47
  "ajv": "8.17.1"
47
48
  },
49
+ "peerDependencies": {
50
+ "electron": "22.3.27"
51
+ },
48
52
  "devDependencies": {
49
53
  "@types/node": "16.18.126",
54
+ "electron": "22.3.27",
50
55
  "typescript": "4.9.4"
51
56
  },
52
57
  "engines": {
@@ -17,13 +17,94 @@ The check follows the npm-configured registry, falls back to the official npm re
17
17
 
18
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.
19
19
 
20
- The renderer calls the plugin-worker `demo.echo` operation through `@aiot-nexus/plugin-sdk`. Run `npm run validate` after changing the manifest or output layout.
20
+ ## Electron Facade file dialogs
21
21
 
22
- 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. Add or edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
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:
23
+
24
+ ```ts
25
+ import * as fs from "fs/promises";
26
+ import type { NexusResult, PluginMainActivationContext } from "@aiot-nexus/plugin-sdk";
27
+
28
+ const maximumTextFileBytes = 1024 * 1024;
29
+
30
+ export async function chooseAndReadText(
31
+ context: PluginMainActivationContext
32
+ ): Promise<NexusResult<string | undefined>> {
33
+ const result = await context.host.electron.dialog.showOpenDialog({
34
+ properties: ["openFile"],
35
+ filters: [{ name: "Text", extensions: ["txt", "md", "json"] }]
36
+ });
37
+ if (result.canceled || !result.filePaths[0]) {
38
+ return { ok: true, value: undefined };
39
+ }
40
+ return readTextFileBounded(result.filePaths[0]);
41
+ }
42
+
43
+ async function readTextFileBounded(filePath: string): Promise<NexusResult<string>> {
44
+ try {
45
+ const file = await fs.open(filePath, "r");
46
+ try {
47
+ const stat = await file.stat();
48
+ if (!stat.isFile()) {
49
+ return failure("FileNotReadable", "The selected path is not a regular file.");
50
+ }
51
+ if (stat.size > maximumTextFileBytes) {
52
+ return failure("FileTooLarge", "The selected text file exceeds the 1 MiB limit.");
53
+ }
54
+
55
+ // The extra byte detects a file that grows after stat without an unbounded allocation.
56
+ const buffer = Buffer.alloc(maximumTextFileBytes + 1);
57
+ let totalBytes = 0;
58
+ while (totalBytes < buffer.length) {
59
+ const { bytesRead } = await file.read(
60
+ buffer,
61
+ totalBytes,
62
+ buffer.length - totalBytes,
63
+ totalBytes
64
+ );
65
+ if (bytesRead === 0) {
66
+ break;
67
+ }
68
+ totalBytes += bytesRead;
69
+ }
70
+ if (totalBytes > maximumTextFileBytes) {
71
+ return failure("FileTooLarge", "The selected text file exceeds the 1 MiB limit.");
72
+ }
73
+ return { ok: true, value: buffer.subarray(0, totalBytes).toString("utf8") };
74
+ } finally {
75
+ await file.close();
76
+ }
77
+ } catch {
78
+ return failure("FileReadFailed", "The selected text file could not be read.");
79
+ }
80
+ }
81
+
82
+ function failure(code: string, message: string): NexusResult<never> {
83
+ return { ok: false, error: { code, message } };
84
+ }
85
+
86
+ export async function chooseFolder(context: PluginMainActivationContext): Promise<string | undefined> {
87
+ const result = await context.host.electron.dialog.showOpenDialog({
88
+ properties: ["openDirectory", "createDirectory"]
89
+ });
90
+ return result.canceled ? undefined : result.filePaths[0];
91
+ }
92
+
93
+ export async function chooseSavePath(context: PluginMainActivationContext): Promise<string | undefined> {
94
+ const result = await context.host.electron.dialog.showSaveDialog({ defaultPath: "document.txt" });
95
+ return result.canceled ? undefined : result.filePath;
96
+ }
97
+ ```
98
+
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
+
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
+
103
+ 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.
23
104
 
24
105
  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.
25
106
 
26
- 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` 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 integration.
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.
27
108
 
28
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.
29
110
 
@@ -1,6 +1,7 @@
1
1
  import type { PluginMainActivationContext } from "@aiot-nexus/plugin-sdk";
2
2
 
3
3
  export async function activate(context: PluginMainActivationContext): Promise<void> {
4
+ const applicationName = await context.host.electron.app.getName();
4
5
  const registration = await context.host.registerRendererMethod("demo.echo", (request) => {
5
6
  const value = request.input && typeof request.input.value === "string"
6
7
  ? request.input.value.trim()
@@ -19,7 +20,7 @@ export async function activate(context: PluginMainActivationContext): Promise<vo
19
20
  if (!registration.ok) {
20
21
  throw new Error(registration.error.message);
21
22
  }
22
- const logResult = await context.host.log("info", "__PLUGIN_ID__ activated.");
23
+ const logResult = await context.host.log("info", "__PLUGIN_ID__ activated in " + applicationName + ".");
23
24
  if (!logResult.ok) {
24
25
  throw new Error(logResult.error.message);
25
26
  }
@@ -19,11 +19,39 @@ Every `npm run build` rebuilds `better-sqlite3@8.7.0` for Electron 22.3.27 with
19
19
 
20
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.
21
21
 
22
- 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. Static labels, CRUD status messages, action buttons, validation text, and date formatting switch together. Edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
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.
23
+
24
+ ## Electron Facade database dialog
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:
27
+
28
+ ```ts
29
+ const result = await context.host.electron.dialog.showOpenDialog({
30
+ title: "Open SQLite database",
31
+ properties: ["openFile"],
32
+ filters: [
33
+ { name: "SQLite databases", extensions: ["db", "db3", "sqlite", "sqlite3"] },
34
+ { name: "All files", extensions: ["*"] }
35
+ ]
36
+ });
37
+
38
+ if (!result.canceled && result.filePaths[0]) {
39
+ const selectedDatabasePath = result.filePaths[0];
40
+ // Validate the candidate before replacing the active connection.
41
+ }
42
+ ```
43
+
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.
45
+
46
+ 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
+
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.
49
+
50
+ 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.
23
51
 
24
52
  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.
25
53
 
26
- 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`, 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. Database operations report unsupported; use `npm run dev` for SQLite and Host API integration. Pass `-- --no-open` or `-- --port <port>` when needed.
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.
27
55
 
28
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.
29
57
 
@@ -1,12 +1,21 @@
1
1
  declare module "better-sqlite3" {
2
2
  class Database {
3
- constructor(filename: string);
3
+ constructor(filename: string, options?: Database.Options);
4
+ readonly readonly: boolean;
4
5
  close(): void;
6
+ defaultSafeIntegers(toggle?: boolean): this;
5
7
  pragma(source: string): unknown;
6
8
  prepare(source: string): Database.Statement;
9
+ transaction<TResult>(action: () => TResult): () => TResult;
7
10
  }
8
11
 
9
12
  namespace Database {
13
+ interface Options {
14
+ readonly?: boolean;
15
+ fileMustExist?: boolean;
16
+ timeout?: number;
17
+ }
18
+
10
19
  interface RunResult {
11
20
  changes: number;
12
21
  lastInsertRowid: number | bigint;
@@ -16,6 +25,8 @@ declare module "better-sqlite3" {
16
25
  run(...params: unknown[]): RunResult;
17
26
  get(...params: unknown[]): unknown;
18
27
  all(...params: unknown[]): unknown[];
28
+ raw(toggle?: boolean): this;
29
+ safeIntegers(toggle?: boolean): this;
19
30
  }
20
31
  }
21
32