@aiot-nexus/plugin-sdk 2.1.1 → 3.0.2

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 (36) hide show
  1. package/README.md +151 -96
  2. package/dev-host/main.js +362 -504
  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 +1 -1
  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 +2 -2
  11. package/dist/src/dev.js +3 -3
  12. package/dist/src/{electron-facade.js → electron-namespaces.js} +2 -3
  13. package/dist/src/index.d.ts +2 -2
  14. package/dist/src/index.js +2 -2
  15. package/dist/src/nexus-dev-control.d.ts +1 -1
  16. package/dist/src/nexus-dev-control.js +2 -2
  17. package/dist/src/nexus-dev.d.ts +0 -3
  18. package/dist/src/nexus-dev.js +118 -53
  19. package/dist/src/types.d.ts +14 -98
  20. package/package.json +1 -1
  21. package/templates/minimal/.vscode/launch.json +1 -1
  22. package/templates/minimal/README.md.tmpl +15 -14
  23. package/templates/minimal/src/main.ts.tmpl +5 -4
  24. package/templates/sqlite/.vscode/launch.json +1 -1
  25. package/templates/sqlite/README.md.tmpl +12 -10
  26. package/templates/sqlite/src/main.ts.tmpl +24 -20
  27. package/dev-host/plugin-host.js +0 -4
  28. package/dist/internal/src-electron/main/core/runtime-result.js +0 -42
  29. package/dist/internal/src-electron/main/plugins/plugin-electron-facade-broker.js +0 -890
  30. package/dist/internal/src-electron/plugin-host-bootstrap.js +0 -417
  31. package/dist/internal/src-electron/shared/contracts.js +0 -2
  32. package/dist/internal/src-electron/shared/plugin-electron-facade-client.js +0 -181
  33. package/dist/internal/src-electron/shared/plugin-electron-facade-descriptors.js +0 -70
  34. package/dist/internal/src-electron/shared/plugin-electron-facade.js +0 -243
  35. package/dist/internal/src-electron/shared/plugin-rpc.js +0 -245
  36. /package/dist/src/{electron-facade.d.ts → electron-namespaces.d.ts} +0 -0
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.shouldSuppressNativeBuildEvent = exports.isRetryablePluginDeploymentError = exports.createNexusHostArguments = exports.mergePluginSourceChange = exports.manifestChangeRequiresPackageUpdate = exports.classifyPluginSourceChange = exports.recoverPreparedPluginPackageUpdate = exports.runPluginNexusDev = void 0;
26
+ exports.shouldSuppressNativeBuildEvent = exports.isRetryablePluginDeploymentError = exports.createNexusHostArguments = exports.mergePluginSourceChange = exports.manifestChangeRequiresPackageUpdate = exports.classifyPluginSourceChange = exports.runPluginNexusDev = void 0;
27
27
  const child_process_1 = require("child_process");
28
28
  const fs = __importStar(require("fs"));
29
29
  const path = __importStar(require("path"));
@@ -43,6 +43,7 @@ class PluginNexusDevSession {
43
43
  this.deployedPluginDirectory = "";
44
44
  this.expectedExits = new Set();
45
45
  this.stopping = false;
46
+ this.restartingHost = false;
46
47
  this.terminalSettled = false;
47
48
  this.terminalPromise = new Promise((resolve) => {
48
49
  this.terminalResolve = resolve;
@@ -54,9 +55,16 @@ class PluginNexusDevSession {
54
55
  this.stopping = true;
55
56
  this.watcher?.close();
56
57
  void (async () => {
58
+ let exitCode = 0;
59
+ try {
60
+ await this.stopCurrentHost(true);
61
+ }
62
+ catch (error) {
63
+ exitCode = 1;
64
+ this.io.stderr(errorMessage(error));
65
+ }
57
66
  await this.watcher?.drain();
58
- await this.stopCurrentHost();
59
- this.settleTerminal(0);
67
+ this.settleTerminal(exitCode);
60
68
  })();
61
69
  };
62
70
  this.projectDirectory = path.resolve(options.projectDirectory);
@@ -67,7 +75,6 @@ class PluginNexusDevSession {
67
75
  (0, dev_1.buildPluginForDevelopment)(this.projectDirectory);
68
76
  const target = (0, dev_1.preparePluginDebugTarget)({ ...this.options, projectDirectory: this.projectDirectory });
69
77
  const deployment = (0, nexus_dev_deployment_1.beginPluginPackageDeployment)(target.packageDirectory, this.nexusExecutable, target.pluginId);
70
- deployment.commit();
71
78
  this.target = target;
72
79
  this.deployedPluginDirectory = path.join(path.dirname(this.nexusExecutable), "Plugins", target.pluginId);
73
80
  this.io.stdout("Deployed plugin package: " + this.deployedPluginDirectory
@@ -75,13 +82,14 @@ class PluginNexusDevSession {
75
82
  try {
76
83
  await this.control.start();
77
84
  this.disposeDisconnectHandler = this.control.setDisconnectHandler((error) => {
78
- if (!this.stopping) {
85
+ if (!this.stopping && !this.restartingHost) {
79
86
  this.io.stderr("Packaged Nexus plugin development control disconnected: " + error.message);
80
87
  this.settleTerminal(1);
81
88
  }
82
89
  });
83
90
  this.installSignalHandlers();
84
91
  await this.launchHost(target);
92
+ deployment.commit();
85
93
  if (this.stopping) {
86
94
  return await this.terminalPromise;
87
95
  }
@@ -93,6 +101,26 @@ class PluginNexusDevSession {
93
101
  this.io.stdout("Watching plugin source changes. Press Ctrl+C to stop.");
94
102
  return await this.terminalPromise;
95
103
  }
104
+ catch (error) {
105
+ this.stopping = true;
106
+ const recoveryErrors = [];
107
+ try {
108
+ await this.stopCurrentHost(true);
109
+ }
110
+ catch (stopError) {
111
+ recoveryErrors.push("host shutdown failed: " + errorMessage(stopError));
112
+ }
113
+ try {
114
+ await retryFileOperation(() => deployment.rollback(), "roll back the initial plugin deployment");
115
+ }
116
+ catch (rollbackError) {
117
+ recoveryErrors.push("deployment rollback failed: " + errorMessage(rollbackError));
118
+ }
119
+ if (recoveryErrors.length > 0) {
120
+ throw new dev_1.PluginDevError(errorMessage(error) + " Initial deployment recovery failed: " + recoveryErrors.join(" "));
121
+ }
122
+ throw error;
123
+ }
96
124
  finally {
97
125
  this.stopping = true;
98
126
  this.watcher?.close();
@@ -106,7 +134,7 @@ class PluginNexusDevSession {
106
134
  }
107
135
  async updatePluginPackage() {
108
136
  const previousTarget = this.requireTarget();
109
- this.io.stdout("Building complete plugin package before restarting the plugin worker...");
137
+ this.io.stdout("Building complete plugin package before restarting Nexus...");
110
138
  this.runBuild(() => (0, dev_1.buildPluginForDevelopment)(this.projectDirectory));
111
139
  const updatedTarget = (0, dev_1.preparePluginDebugTarget)({
112
140
  projectDirectory: this.projectDirectory,
@@ -119,18 +147,19 @@ class PluginNexusDevSession {
119
147
  throw new dev_1.PluginDevError("Plugin id cannot change during a Nexus development session.");
120
148
  }
121
149
  let transaction;
122
- let prepareAttempted = false;
150
+ this.restartingHost = true;
123
151
  try {
124
- prepareAttempted = true;
125
- const prepareResult = await this.control.sendUpdate("packageUpdatePreparing", previousTarget.pluginId);
126
- if (!prepareResult.ok) {
127
- throw new dev_1.PluginDevError(prepareResult.message);
152
+ await this.stopCurrentHost(true);
153
+ if (this.stopping) {
154
+ return;
128
155
  }
129
156
  transaction = await retryFileOperation(() => (0, nexus_dev_deployment_1.beginPluginPackageDeployment)(updatedTarget.packageDirectory, this.nexusExecutable, updatedTarget.pluginId), "replace the plugin package");
130
- const updateResult = await this.control.sendUpdate("packageUpdated", updatedTarget.pluginId);
131
- if (!updateResult.ok) {
132
- throw new dev_1.PluginDevError(updateResult.message);
157
+ if (this.stopping) {
158
+ await retryFileOperation(() => transaction?.rollback(), "roll back the plugin package after shutdown");
159
+ transaction = undefined;
160
+ return;
133
161
  }
162
+ await this.launchHost(updatedTarget);
134
163
  try {
135
164
  await retryFileOperation(() => transaction?.commit(), "remove the previous plugin package backup");
136
165
  }
@@ -138,18 +167,48 @@ class PluginNexusDevSession {
138
167
  this.io.stderr("Updated plugin is active, but its previous package backup could not be removed: " + errorMessage(error));
139
168
  }
140
169
  this.target = updatedTarget;
141
- this.io.stdout("Plugin package reloaded in a new worker; Nexus PID unchanged: " + String(this.child?.pid || "unknown"));
170
+ this.io.stdout("Plugin package loaded after Nexus restart. New PID: " + String(this.child?.pid || "unknown"));
142
171
  }
143
172
  catch (error) {
144
- if (prepareAttempted) {
173
+ const recoveryErrors = [];
174
+ try {
175
+ await this.stopCurrentHost(true);
176
+ }
177
+ catch (stopError) {
178
+ recoveryErrors.push("updated host shutdown failed: " + errorMessage(stopError));
179
+ }
180
+ if (transaction) {
145
181
  try {
146
- await recoverPreparedPluginPackageUpdate(transaction, previousTarget.pluginId, this.control);
182
+ await retryFileOperation(() => transaction?.rollback(), "roll back the plugin package");
147
183
  }
148
- catch (recoveryError) {
149
- throw new dev_1.PluginDevError(errorMessage(error) + " Package update recovery failed: " + errorMessage(recoveryError));
184
+ catch (rollbackError) {
185
+ recoveryErrors.push("package rollback failed: " + errorMessage(rollbackError));
150
186
  }
151
187
  }
152
- throw error;
188
+ if (this.stopping && recoveryErrors.length === 0) {
189
+ return;
190
+ }
191
+ if (!this.stopping && recoveryErrors.length === 0) {
192
+ try {
193
+ await this.launchHost(previousTarget);
194
+ }
195
+ catch (launchError) {
196
+ recoveryErrors.push("previous Nexus host restart failed: " + errorMessage(launchError));
197
+ try {
198
+ await this.stopCurrentHost(true);
199
+ }
200
+ catch (stopError) {
201
+ recoveryErrors.push("failed recovery host shutdown failed: " + errorMessage(stopError));
202
+ }
203
+ }
204
+ }
205
+ if (recoveryErrors.length > 0) {
206
+ throw new dev_1.PluginDevError(errorMessage(error) + " Package update recovery failed: " + recoveryErrors.join(" "));
207
+ }
208
+ throw new dev_1.PluginDevError(errorMessage(error) + " The previous plugin package was restored and Nexus was restarted.");
209
+ }
210
+ finally {
211
+ this.restartingHost = false;
153
212
  }
154
213
  }
155
214
  async launchHost(target) {
@@ -165,7 +224,12 @@ class PluginNexusDevSession {
165
224
  const hostExit = waitForHostExit(child);
166
225
  this.childExit = hostExit;
167
226
  hostExit.then((result) => this.handleHostExit(child, result), (error) => {
168
- if (!this.stopping) {
227
+ if (this.child === child) {
228
+ this.child = undefined;
229
+ this.childExit = undefined;
230
+ }
231
+ this.expectedExits.delete(child);
232
+ if (!this.stopping && !this.restartingHost) {
169
233
  this.io.stderr(errorMessage(error));
170
234
  this.settleTerminal(1);
171
235
  }
@@ -180,7 +244,14 @@ class PluginNexusDevSession {
180
244
  })
181
245
  ]);
182
246
  if (this.stopping) {
183
- return child;
247
+ throw new dev_1.PluginDevError("Packaged Nexus startup was cancelled because plugin development is stopping.");
248
+ }
249
+ if (child.exitCode !== null || this.child !== child) {
250
+ throw new dev_1.PluginDevError(formatEarlyHostExit({ status: child.exitCode, signal: child.signalCode }));
251
+ }
252
+ const readyResult = await this.control.sendUpdate("hostReady", target.pluginId);
253
+ if (!readyResult.ok) {
254
+ throw new dev_1.PluginDevError("Packaged Nexus started, but the target plugin is not ready: " + readyResult.message);
184
255
  }
185
256
  this.io.stdout("Packaged Nexus plugin development control connected. PID: " + String(child.pid || "unknown"));
186
257
  return child;
@@ -190,7 +261,7 @@ class PluginNexusDevSession {
190
261
  this.child = undefined;
191
262
  this.childExit = undefined;
192
263
  }
193
- if (this.expectedExits.delete(child) || this.stopping) {
264
+ if (this.expectedExits.delete(child) || this.stopping || this.restartingHost) {
194
265
  return;
195
266
  }
196
267
  const exitCode = result.status === null ? 1 : result.status;
@@ -199,25 +270,41 @@ class PluginNexusDevSession {
199
270
  + (result.signal ? " (" + result.signal + ")" : "") + ".");
200
271
  this.settleTerminal(exitCode === 0 ? 1 : exitCode);
201
272
  }
202
- async stopCurrentHost() {
273
+ async stopCurrentHost(requireTermination = false) {
203
274
  const child = this.child;
204
275
  const hostExit = this.childExit;
205
276
  if (!child || !hostExit) {
206
277
  return;
207
278
  }
208
279
  this.expectedExits.add(child);
280
+ let gracefulFailure;
209
281
  try {
210
282
  await this.control.requestShutdown();
211
283
  await promiseWithTimeout(hostExit, 5000, "Nexus did not exit during plugin development shutdown.");
212
284
  }
213
- catch {
285
+ catch (error) {
286
+ gracefulFailure = error;
214
287
  child.kill();
215
288
  try {
216
289
  await promiseWithTimeout(hostExit, 3000, "Nexus did not terminate after plugin development shutdown.");
217
290
  }
218
- catch {
219
- // The CLI is already stopping; do not start another shutdown path.
291
+ catch (terminationError) {
292
+ this.expectedExits.delete(child);
293
+ if (requireTermination) {
294
+ throw new dev_1.PluginDevError(errorMessage(terminationError) + " Graceful shutdown also failed: " + errorMessage(gracefulFailure));
295
+ }
296
+ this.io.stderr(errorMessage(terminationError));
297
+ return;
298
+ }
299
+ }
300
+ try {
301
+ await this.control.waitForDisconnection(3000);
302
+ }
303
+ catch (error) {
304
+ if (requireTermination) {
305
+ throw error;
220
306
  }
307
+ this.io.stderr(errorMessage(error));
221
308
  }
222
309
  }
223
310
  runBuild(operation) {
@@ -246,30 +333,6 @@ class PluginNexusDevSession {
246
333
  this.terminalResolve = undefined;
247
334
  }
248
335
  }
249
- async function recoverPreparedPluginPackageUpdate(transaction, pluginId, control) {
250
- const recoveryErrors = [];
251
- if (transaction) {
252
- try {
253
- await retryFileOperation(() => transaction.rollback(), "roll back the plugin package");
254
- }
255
- catch (error) {
256
- recoveryErrors.push("package rollback failed: " + errorMessage(error));
257
- }
258
- }
259
- try {
260
- const rollbackResult = await control.sendUpdate("packageUpdated", pluginId);
261
- if (!rollbackResult.ok) {
262
- recoveryErrors.push("previous worker activation failed: " + rollbackResult.message);
263
- }
264
- }
265
- catch (error) {
266
- recoveryErrors.push("previous worker activation failed: " + errorMessage(error));
267
- }
268
- if (recoveryErrors.length > 0) {
269
- throw new dev_1.PluginDevError(recoveryErrors.join(" "));
270
- }
271
- }
272
- exports.recoverPreparedPluginPackageUpdate = recoverPreparedPluginPackageUpdate;
273
336
  function classifyPluginSourceChange(projectDirectory, changedPath) {
274
337
  const relativePath = path.relative(path.resolve(projectDirectory), path.resolve(changedPath)).replace(/\\/g, "/");
275
338
  if (relativePath.startsWith("../") || relativePath === ".." || path.isAbsolute(relativePath)) {
@@ -296,6 +359,9 @@ function classifyPluginSourceChange(projectDirectory, changedPath) {
296
359
  }
297
360
  exports.classifyPluginSourceChange = classifyPluginSourceChange;
298
361
  function manifestChangeRequiresPackageUpdate(previous, next) {
362
+ if (previous.mainEntry || next.mainEntry) {
363
+ return stableJson(previous) !== stableJson(next);
364
+ }
299
365
  const packageSensitiveFields = [
300
366
  "schemaVersion",
301
367
  "pluginId",
@@ -375,8 +441,7 @@ async function finishUpdateTransaction(transaction, updateType, pluginId, contro
375
441
  function createNexusHostArguments(control, target, options) {
376
442
  const args = [];
377
443
  if (options.inspectorMode) {
378
- args.push("--plugin-dev-worker-inspector=" + options.inspectorMode + ":" + String(options.inspectorPort || 9229));
379
- args.push("--plugin-dev-worker-plugin=" + target.pluginId);
444
+ args.push("--" + options.inspectorMode + "=" + String(options.inspectorPort || 9229));
380
445
  }
381
446
  args.push("--plugin-dev", "--plugin-dev-control=" + control.pipePath, "--plugin-dev-token=" + control.token, "--page=" + resolveContributionPage(target));
382
447
  if (target.surface !== "business") {
@@ -1,5 +1,3 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
1
  export interface NexusError {
4
2
  code: string;
5
3
  message: string;
@@ -14,8 +12,8 @@ export type NexusResult<T> = {
14
12
  };
15
13
  export type ThemeMode = "light" | "dark" | "system";
16
14
  export type LaunchPageParams = Record<string, string>;
17
- export type PluginPermission = "businessLog" | "settings" | "filesystem" | "udp" | "serial" | "externalProcess" | "dialog" | "electronHostControl";
18
- export type PluginPermissionSupportState = "implemented" | "hostControl" | "routeGuard" | "reserved";
15
+ export type PluginPermission = "businessLog" | "settings" | "filesystem" | "udp" | "serial" | "externalProcess" | "dialog";
16
+ export type PluginPermissionSupportState = "implemented" | "routeGuard" | "reserved";
19
17
  export interface PluginPermissionSummary {
20
18
  permission: PluginPermission;
21
19
  supportState: PluginPermissionSupportState;
@@ -109,103 +107,22 @@ export type PluginRendererOperationHandler<TInput extends Record<string, unknown
109
107
  export interface PluginUnsupportedCapabilityApi {
110
108
  readonly permission: PluginPermission;
111
109
  readonly supportState: PluginPermissionSupportState;
112
- describe(): Promise<NexusResult<PluginPermissionSummary>>;
113
- unsupported(operation?: unknown): Promise<NexusResult<never>>;
110
+ describe(): NexusResult<PluginPermissionSummary>;
111
+ unsupported(operation?: unknown): NexusResult<never>;
114
112
  [operation: string]: unknown;
115
113
  }
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
- }
196
114
  export interface PluginHostApi {
197
115
  readonly hostVersion: string;
198
116
  readonly hostApiVersion: string;
199
117
  readonly pluginId: string;
200
118
  readonly pluginDataDirectory: string;
201
119
  hasPermission(permission: PluginPermission): boolean;
202
- registerRendererMethod(name: string, handler: PluginRendererOperationHandler): Promise<NexusResult<boolean>>;
203
- getCapabilityStatus(permission: unknown): Promise<NexusResult<PluginPermissionSummary>>;
204
- unsupportedCapability(permission: unknown, operation?: unknown): Promise<NexusResult<never>>;
205
- log(level: PluginHostLogLevel, message: string, exception?: string): Promise<NexusResult<BusinessLogEntry>>;
206
- getSettings(): Promise<NexusResult<PluginSettingsDocument>>;
207
- updateSettings(values: Record<string, unknown>): Promise<NexusResult<PluginSettingsDocument>>;
208
- readonly electron: PluginElectronFacade;
120
+ registerRendererMethod(name: string, handler: PluginRendererOperationHandler): NexusResult<boolean>;
121
+ getCapabilityStatus(permission: unknown): NexusResult<PluginPermissionSummary>;
122
+ unsupportedCapability(permission: unknown, operation?: unknown): NexusResult<never>;
123
+ log(level: PluginHostLogLevel, message: string, exception?: string): NexusResult<BusinessLogEntry>;
124
+ getSettings(): NexusResult<PluginSettingsDocument>;
125
+ updateSettings(values: Record<string, unknown>): NexusResult<PluginSettingsDocument>;
209
126
  filesystem: PluginUnsupportedCapabilityApi;
210
127
  udp: PluginUnsupportedCapabilityApi;
211
128
  serial: PluginUnsupportedCapabilityApi;
@@ -219,11 +136,10 @@ export interface PluginMainActivationContext {
219
136
  readonly host: PluginHostApi;
220
137
  }
221
138
  export interface PluginMainModule {
222
- activate?: (context: PluginMainActivationContext) => unknown;
223
- deactivate?: (reason: string) => unknown;
139
+ activate?: (context: PluginMainActivationContext) => void;
140
+ deactivate?: (reason: "reload" | "disable" | "quit") => void;
224
141
  default?: {
225
- activate?: (context: PluginMainActivationContext) => unknown;
226
- deactivate?: (reason: string) => unknown;
142
+ activate?: (context: PluginMainActivationContext) => void;
143
+ deactivate?: (reason: "reload" | "disable" | "quit") => void;
227
144
  };
228
145
  }
229
- export {};
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.0.2",
4
4
  "description": "Types, PluginFrame client, and CLI for AIoT Nexus independent plugins.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -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",
@@ -15,22 +15,23 @@ SDK checks for a newer `@aiot-nexus/plugin-sdk` in two steps. The package `posti
15
15
 
16
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.
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
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.
112
113
 
113
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; 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",