@aiot-nexus/plugin-sdk 2.0.1 → 2.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 (33) hide show
  1. package/README.md +19 -7
  2. package/dev-host/main.js +164 -8
  3. package/dev-host/plugin-host.js +1 -415
  4. package/dist/.generated/plugin-framework-version.d.ts +1 -1
  5. package/dist/.generated/plugin-framework-version.js +1 -1
  6. package/dist/.generated/plugin-manifest.schema.json +1 -1
  7. package/dist/browser/frame-client.mjs +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/browser/frame-client.js +1 -1
  17. package/dist/src/electron-facade.d.ts +4 -0
  18. package/dist/src/electron-facade.js +56 -0
  19. package/dist/src/index.d.ts +2 -1
  20. package/dist/src/index.js +2 -1
  21. package/dist/src/nexus-dev.js +5 -4
  22. package/dist/src/scaffold.js +2 -2
  23. package/dist/src/types.d.ts +86 -2
  24. package/dist/src/web-dev.js +2 -2
  25. package/package.json +7 -2
  26. package/templates/minimal/README.md.tmpl +37 -2
  27. package/templates/minimal/src/main.ts.tmpl +2 -1
  28. package/templates/sqlite/README.md.tmpl +32 -2
  29. package/templates/sqlite/src/better-sqlite3.d.ts +12 -1
  30. package/templates/sqlite/src/main.ts.tmpl +939 -124
  31. package/templates/sqlite/src/renderer/app.ts.tmpl +688 -181
  32. package/templates/sqlite/src/renderer/index.html +40 -22
  33. 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.1",
3
+ "version": "2.1.0",
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,11 +17,46 @@ 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
+
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 { PluginMainActivationContext } from "@aiot-nexus/plugin-sdk";
27
+
28
+ export async function chooseAndReadText(context: PluginMainActivationContext): Promise<string | undefined> {
29
+ const result = await context.host.electron.dialog.showOpenDialog({
30
+ properties: ["openFile"],
31
+ filters: [{ name: "Text", extensions: ["txt", "md", "json"] }]
32
+ });
33
+ return result.canceled || !result.filePaths[0]
34
+ ? undefined
35
+ : fs.readFile(result.filePaths[0], "utf8");
36
+ }
37
+
38
+ export async function chooseFolder(context: PluginMainActivationContext): Promise<string | undefined> {
39
+ const result = await context.host.electron.dialog.showOpenDialog({
40
+ properties: ["openDirectory", "createDirectory"]
41
+ });
42
+ return result.canceled ? undefined : result.filePaths[0];
43
+ }
44
+
45
+ export async function chooseSavePath(context: PluginMainActivationContext): Promise<string | undefined> {
46
+ const result = await context.host.electron.dialog.showSaveDialog({ defaultPath: "document.txt" });
47
+ return result.canceled ? undefined : result.filePath;
48
+ }
49
+ ```
50
+
51
+ Call these helpers from an operation registered with `context.host.registerRendererMethod`, then let the renderer invoke that narrow operation through `client.invokeMain()`. Do not expose the Facade or unrestricted file paths to the iframe. `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.
52
+
53
+ 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.
21
54
 
22
55
  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.
23
56
 
24
- 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.
57
+ 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.
58
+
59
+ 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.
25
60
 
26
61
  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.
27
62
 
@@ -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,9 +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
23
 
24
- 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.
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. Update and delete run in a transaction and must affect exactly one row. TEXT previews are capped at 16 KiB; 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. 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
+
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.
53
+
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.
25
55
 
26
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.
27
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