@aiot-nexus/plugin-sdk 2.1.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +197 -90
  2. package/dev-host/main.js +366 -505
  3. package/dist/.generated/plugin-framework-version.d.ts +1 -1
  4. package/dist/.generated/plugin-framework-version.js +1 -1
  5. package/dist/.generated/plugin-manifest.schema.json +3 -2
  6. package/dist/browser/frame-client.d.ts +1 -0
  7. package/dist/browser/frame-client.mjs +121 -13
  8. package/dist/src/browser/frame-client.d.ts +1 -0
  9. package/dist/src/browser/frame-client.js +121 -13
  10. package/dist/src/cli/run.js +81 -6
  11. package/dist/src/cli/update-notice.js +1 -1
  12. package/dist/src/dev.js +3 -3
  13. package/dist/src/{electron-facade.js → electron-namespaces.js} +2 -3
  14. package/dist/src/index.d.ts +2 -2
  15. package/dist/src/index.js +3 -3
  16. package/dist/src/nexus-dev-control.d.ts +1 -1
  17. package/dist/src/nexus-dev-control.js +2 -2
  18. package/dist/src/nexus-dev.d.ts +0 -3
  19. package/dist/src/nexus-dev.js +118 -53
  20. package/dist/src/scaffold.js +6 -5
  21. package/dist/src/sdk-info.d.ts +17 -0
  22. package/dist/src/sdk-info.js +23 -0
  23. package/dist/src/types.d.ts +15 -98
  24. package/dist/src/upgrade.d.ts +44 -0
  25. package/dist/src/upgrade.js +1189 -0
  26. package/dist/src/validation.js +9 -0
  27. package/package.json +2 -2
  28. package/scripts/sdk-update-check.js +1 -1
  29. package/templates/minimal/.vscode/launch.json +1 -1
  30. package/templates/minimal/README.md.tmpl +18 -17
  31. package/templates/minimal/src/main.ts.tmpl +5 -4
  32. package/templates/sqlite/.vscode/launch.json +1 -1
  33. package/templates/sqlite/README.md.tmpl +15 -13
  34. package/templates/sqlite/src/main.ts.tmpl +24 -20
  35. package/dev-host/plugin-host.js +0 -4
  36. package/dist/internal/src-electron/main/core/runtime-result.js +0 -42
  37. package/dist/internal/src-electron/main/plugins/plugin-electron-facade-broker.js +0 -890
  38. package/dist/internal/src-electron/plugin-host-bootstrap.js +0 -417
  39. package/dist/internal/src-electron/shared/contracts.js +0 -2
  40. package/dist/internal/src-electron/shared/plugin-electron-facade-client.js +0 -181
  41. package/dist/internal/src-electron/shared/plugin-electron-facade-descriptors.js +0 -70
  42. package/dist/internal/src-electron/shared/plugin-electron-facade.js +0 -243
  43. package/dist/internal/src-electron/shared/plugin-rpc.js +0 -245
  44. /package/dist/src/{electron-facade.d.ts → electron-namespaces.d.ts} +0 -0
package/dev-host/main.js CHANGED
@@ -2,19 +2,10 @@
2
2
  /* eslint-disable @typescript-eslint/no-var-requires */
3
3
 
4
4
  const fs = require("fs");
5
+ const { createRequire } = require("module");
5
6
  const path = require("path");
6
- const { app, BrowserWindow, ipcMain, protocol, utilityProcess } = require("electron");
7
+ const { app, BrowserWindow, ipcMain, protocol } = require("electron");
7
8
  const sdk = require(path.join(__dirname, "..", "dist", "src", "index.js"));
8
- const { PluginElectronFacadeBrokerService } = require(path.join(
9
- __dirname,
10
- "..",
11
- "dist",
12
- "internal",
13
- "src-electron",
14
- "main",
15
- "plugins",
16
- "plugin-electron-facade-broker.js"
17
- ));
18
9
 
19
10
  const pluginRendererProtocol = "nexus-plugin";
20
11
  const ipcChannels = {
@@ -22,13 +13,9 @@ const ipcChannels = {
22
13
  invoke: "nexus-plugin-dev:invoke",
23
14
  frameReady: "nexus-plugin-dev:frame-ready"
24
15
  };
25
- const pluginPermissions = ["businessLog", "settings", "filesystem", "udp", "serial", "externalProcess", "dialog", "electronHostControl"];
16
+ const pluginPermissions = ["businessLog", "settings", "filesystem", "udp", "serial", "externalProcess", "dialog"];
26
17
  const implementedPermissions = new Set(["businessLog", "settings"]);
27
- const hostControlPermissions = new Set(["electronHostControl"]);
28
18
  const routeGuardPermissions = new Set(["filesystem", "udp", "serial", "dialog"]);
29
- const workerProtocolVersion = 3;
30
- const workerOperationTimeoutMs = 15000;
31
- const electronFacadeBroker = new PluginElectronFacadeBrokerService();
32
19
 
33
20
  protocol.registerSchemesAsPrivileged([
34
21
  {
@@ -57,13 +44,14 @@ app.setName("Nexus Plugin Dev: " + configuration.manifest.pluginId);
57
44
  let mainWindow;
58
45
  let smokeTimeout;
59
46
  let smokeVerificationRunning = false;
60
- let pluginWorker;
61
- let workerShutdownStarted = false;
62
- let workerShutdownComplete = false;
47
+ let pluginModule;
48
+ let pluginActivated = false;
49
+ let pluginDeactivated = false;
50
+ const rendererOperations = new Map();
63
51
 
64
- app.whenReady().then(async function startDevHost() {
52
+ app.whenReady().then(function startDevHost() {
65
53
  registerPluginRendererProtocol();
66
- await startPluginWorker();
54
+ activatePluginMainEntry();
67
55
  registerIpcHandlers();
68
56
  createWindow();
69
57
  }).catch(failStartup);
@@ -72,26 +60,18 @@ app.on("window-all-closed", function quitAfterWindowClose() {
72
60
  app.quit();
73
61
  });
74
62
 
75
- app.on("before-quit", function waitForPluginWorkerShutdown(event) {
76
- if (!pluginWorker || workerShutdownComplete) {
77
- return;
78
- }
79
- event.preventDefault();
80
- if (workerShutdownStarted) {
81
- return;
82
- }
83
- workerShutdownStarted = true;
84
- void pluginWorker.shutdown("quit")
85
- .catch(function reportWorkerShutdownFailure(error) {
86
- process.stderr.write("[nexus-plugin-dev] plugin worker shutdown failed: " + errorMessage(error) + "\n");
87
- })
88
- .finally(function finishPluginWorkerShutdown() {
89
- electronFacadeBroker.disposePlugin(configuration.manifest.pluginId);
90
- workerShutdownComplete = true;
91
- app.quit();
92
- });
63
+ app.on("before-quit", function deactivateBeforeQuit() {
64
+ deactivatePluginMainEntry("quit");
93
65
  });
94
66
 
67
+ process.once("SIGINT", requestSignalQuit);
68
+ process.once("SIGTERM", requestSignalQuit);
69
+
70
+ function requestSignalQuit() {
71
+ deactivatePluginMainEntry("quit");
72
+ app.quit();
73
+ }
74
+
95
75
  function loadConfiguration(args) {
96
76
  const packageDirectory = requiredPathArgument(args, "--plugin-package");
97
77
  const projectDirectory = requiredPathArgument(args, "--project-directory");
@@ -107,7 +87,6 @@ function loadConfiguration(args) {
107
87
  const language = requiredArgument(args, "--language");
108
88
  const themeMode = requiredArgument(args, "--theme");
109
89
  const hostVersion = requiredArgument(args, "--host-version");
110
- const workerInspector = parseWorkerInspector(args);
111
90
  if (!isStrictVersion(hostVersion)) {
112
91
  throw new Error("--host-version must use X.Y.Z format.");
113
92
  }
@@ -127,7 +106,6 @@ function loadConfiguration(args) {
127
106
  language,
128
107
  themeMode,
129
108
  hostVersion,
130
- workerInspector,
131
109
  openDevTools: !args.includes("--no-devtools"),
132
110
  smoke: args.includes("--smoke"),
133
111
  devDataDirectory,
@@ -194,451 +172,233 @@ function resolvePluginRendererFile(url) {
194
172
  });
195
173
  }
196
174
 
197
- async function startPluginWorker() {
175
+ function activatePluginMainEntry() {
198
176
  if (!configuration.manifest.mainEntry) {
199
177
  return;
200
178
  }
201
- pluginWorker = new PluginWorkerClient(configuration);
202
- electronFacadeBroker.attachPlugin(configuration.manifest.pluginId, {
203
- pluginDirectory: configuration.packageDirectory,
204
- hostControl: configuration.manifest.permissions.includes("electronHostControl"),
205
- invokeCallback: (callbackId, args) => pluginWorker.invokeElectronCallback(callbackId, args),
206
- emitEvent: (callbackId, args) => pluginWorker.emitElectronEvent(callbackId, args)
207
- });
208
- let ready;
179
+ const mainEntryPath = resolvePluginMainEntry(configuration.manifest.mainEntry);
180
+ const pluginRequire = createRequire(mainEntryPath);
181
+ const loaded = pluginRequire(mainEntryPath);
182
+ if (!loaded || typeof loaded !== "object") {
183
+ throw new Error("Plugin main entry must export an object.");
184
+ }
185
+ pluginModule = loaded;
186
+ let activationStarted = false;
209
187
  try {
210
- ready = await pluginWorker.start();
188
+ const activate = loaded.activate || (loaded.default && loaded.default.activate);
189
+ if (activate) {
190
+ if (typeof activate !== "function") {
191
+ throw new Error("Plugin activate export must be a function.");
192
+ }
193
+ if (isAsyncFunction(activate)) {
194
+ throw new Error("Plugin activate must complete synchronously in Framework API 3.1.0.");
195
+ }
196
+ activationStarted = true;
197
+ const result = activate(createActivationContext());
198
+ if (isPromiseLike(result)) {
199
+ observeRejectedLifecyclePromise(result, "activate");
200
+ throw new Error("Plugin activate must complete synchronously in Framework API 3.1.0.");
201
+ }
202
+ }
203
+ pluginActivated = true;
204
+ process.stdout.write(
205
+ "[nexus-plugin-dev] activated " + configuration.manifest.pluginId
206
+ + " directly in the Electron main process with " + String(rendererOperations.size) + " renderer operation(s).\n"
207
+ );
211
208
  } catch (error) {
212
- electronFacadeBroker.disposePlugin(configuration.manifest.pluginId);
209
+ if (activationStarted) {
210
+ runActivationCleanup(loaded);
211
+ }
212
+ rendererOperations.clear();
213
+ pluginModule = undefined;
213
214
  throw error;
214
215
  }
215
- process.stdout.write(
216
- "[nexus-plugin-dev] activated " + configuration.manifest.pluginId
217
- + " with " + String(ready.operations.length) + " renderer operation(s).\n"
218
- );
219
216
  }
220
217
 
221
- class PluginWorkerClient {
222
- constructor(workerConfiguration) {
223
- this.configuration = workerConfiguration;
224
- this.child = undefined;
225
- this.pending = new Map();
226
- this.startPromise = undefined;
227
- this.startResolve = undefined;
228
- this.startReject = undefined;
229
- this.startTimer = undefined;
230
- this.shutdownPromise = undefined;
231
- this.shutdownResolve = undefined;
232
- this.shutdownTimer = undefined;
233
- this.initializationRequestId = undefined;
234
- this.requestSequence = 0;
235
- this.intentionalStop = false;
236
- this.ready = false;
218
+ function resolvePluginMainEntry(relativePath) {
219
+ if (path.isAbsolute(relativePath)) {
220
+ throw new Error("Plugin mainEntry must be relative to the plugin package.");
237
221
  }
238
-
239
- start() {
240
- if (this.startPromise) {
241
- return this.startPromise;
242
- }
243
- const startPromise = new Promise((resolve, reject) => {
244
- this.startResolve = resolve;
245
- this.startReject = reject;
246
- });
247
- this.startPromise = startPromise;
248
- this.intentionalStop = false;
249
- this.ready = false;
250
- try {
251
- this.child = utilityProcess.fork(path.join(__dirname, "plugin-host.js"), [], {
252
- cwd: this.configuration.packageDirectory,
253
- serviceName: "nexus-plugin-dev-" + this.configuration.manifest.pluginId,
254
- stdio: this.configuration.workerInspector ? "inherit" : "ignore",
255
- execArgv: this.configuration.workerInspector ? [this.configuration.workerInspector.execArg] : []
256
- });
257
- this.child.on("message", (message) => { void this.handleMessage(readWorkerMessage(message)); });
258
- this.child.on("exit", (code) => this.handleExit(code));
259
- if (!this.configuration.workerInspector || !this.configuration.workerInspector.breakOnStart) {
260
- this.startTimer = setTimeout(() => {
261
- this.failStart(new Error("Plugin worker activation timed out."));
262
- this.kill();
263
- }, 15_000);
264
- }
265
- const initializationRequestId = this.nextRequestId("initialize");
266
- this.initializationRequestId = initializationRequestId;
267
- this.post({
268
- protocolVersion: workerProtocolVersion,
269
- type: "initialize",
270
- requestId: initializationRequestId,
271
- payload: {
272
- pluginId: this.configuration.manifest.pluginId,
273
- pluginDirectory: this.configuration.packageDirectory,
274
- pluginDataDirectory: this.configuration.pluginDataDirectory,
275
- manifest: sanitizeJsonValue(this.configuration.manifest),
276
- hostVersion: this.configuration.hostVersion,
277
- hostApiVersion: sdk.PLUGIN_FRAMEWORK_VERSION
278
- }
279
- });
280
- } catch (error) {
281
- this.failStart(error);
282
- }
283
- return startPromise;
222
+ const resolvedPath = path.resolve(configuration.packageDirectory, relativePath);
223
+ if (!isPathInside(configuration.packageDirectory, resolvedPath)) {
224
+ throw new Error("Plugin mainEntry resolves outside the plugin package.");
284
225
  }
285
-
286
- invoke(payload) {
287
- if (!this.child || !this.ready) {
288
- return Promise.resolve(fail("PluginWorkerNotReady", "Plugin worker is not ready."));
289
- }
290
- const requestId = this.nextRequestId("invoke");
291
- return new Promise((resolve) => {
292
- const pending = {
293
- kind: "invoke",
294
- resolve,
295
- timer: setTimeout(() => {
296
- if (this.pending.get(requestId) !== pending) {
297
- return;
298
- }
299
- this.pending.delete(requestId);
300
- pending.timer = undefined;
301
- this.ready = false;
302
- resolve(fail("PluginOperationTimeout", "Plugin worker operation timed out.", {
303
- pluginId: this.configuration.manifest.pluginId,
304
- operation: payload.operation,
305
- timeoutMs: workerOperationTimeoutMs
306
- }));
307
- this.kill();
308
- }, workerOperationTimeoutMs)
309
- };
310
- this.pending.set(requestId, pending);
311
- try {
312
- this.post({ protocolVersion: workerProtocolVersion, type: "invoke", requestId, payload });
313
- } catch (error) {
314
- this.pending.delete(requestId);
315
- clearPendingTimer(pending);
316
- resolve(fail("PluginWorkerExited", "Plugin worker request could not be sent.", errorMessage(error)));
317
- }
318
- });
319
- }
320
-
321
- invokeElectronCallback(callbackId, args) {
322
- if (!this.child) {
323
- return Promise.resolve(fail("PluginProcessNotReady", "Plugin worker is not available for Electron callback."));
324
- }
325
- const requestId = this.nextRequestId("electron-callback");
326
- return new Promise((resolve) => {
327
- const pending = {
328
- kind: "electronCallback",
329
- resolve,
330
- timer: setTimeout(() => {
331
- if (this.pending.get(requestId) !== pending) {
332
- return;
333
- }
334
- this.pending.delete(requestId);
335
- pending.timer = undefined;
336
- resolve(fail("PluginOperationTimeout", "Electron facade callback timed out.", {
337
- pluginId: this.configuration.manifest.pluginId,
338
- callbackId,
339
- timeoutMs: workerOperationTimeoutMs
340
- }));
341
- }, workerOperationTimeoutMs)
342
- };
343
- this.pending.set(requestId, pending);
344
- try {
345
- this.post({
346
- protocolVersion: workerProtocolVersion,
347
- type: "electronCallback",
348
- requestId,
349
- payload: { callbackId, args }
350
- });
351
- } catch (error) {
352
- this.pending.delete(requestId);
353
- clearPendingTimer(pending);
354
- resolve(fail("PluginProcessExited", "Electron facade callback could not be sent.", errorMessage(error)));
355
- }
356
- });
357
- }
358
-
359
- emitElectronEvent(callbackId, args) {
360
- if (!this.child) {
361
- return false;
362
- }
363
- try {
364
- this.post({
365
- protocolVersion: workerProtocolVersion,
366
- type: "electronEvent",
367
- payload: { callbackId, args }
368
- });
369
- return true;
370
- } catch {
371
- return false;
372
- }
226
+ if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile() || fs.lstatSync(resolvedPath).isSymbolicLink()) {
227
+ throw new Error("Plugin mainEntry was not found or is not a regular file: " + relativePath);
373
228
  }
374
-
375
- shutdown(reason) {
376
- this.intentionalStop = true;
377
- if (!this.child) {
378
- return Promise.resolve(ok(true));
379
- }
380
- if (this.shutdownPromise) {
381
- return this.shutdownPromise;
382
- }
383
- const requestId = this.nextRequestId("shutdown");
384
- this.shutdownPromise = new Promise((resolve) => {
385
- this.shutdownResolve = resolve;
386
- this.pending.set(requestId, { kind: "shutdown", resolve });
387
- this.shutdownTimer = setTimeout(() => {
388
- this.pending.delete(requestId);
389
- this.shutdownResolve = undefined;
390
- this.shutdownPromise = undefined;
391
- const result = fail("PluginWorkerShutdownTimeout", "Plugin worker shutdown timed out.");
392
- resolve(result);
393
- this.kill();
394
- }, 5000);
395
- try {
396
- this.post({ protocolVersion: workerProtocolVersion, type: "shutdown", requestId, payload: { reason: reason || "quit" } });
397
- } catch (error) {
398
- clearTimeout(this.shutdownTimer);
399
- this.shutdownTimer = undefined;
400
- this.pending.delete(requestId);
401
- this.shutdownPromise = undefined;
402
- resolve(fail("PluginWorkerExited", "Plugin worker shutdown could not be sent.", errorMessage(error)));
403
- this.kill();
404
- }
405
- });
406
- return this.shutdownPromise;
229
+ const realRoot = fs.realpathSync(configuration.packageDirectory);
230
+ const realPath = fs.realpathSync(resolvedPath);
231
+ if (!isPathInside(realRoot, realPath)) {
232
+ throw new Error("Plugin mainEntry resolves outside the plugin package.");
407
233
  }
234
+ return realPath;
235
+ }
408
236
 
409
- async handleMessage(message) {
410
- if (!isRecord(message) || message.protocolVersion !== workerProtocolVersion) {
411
- this.handleProtocolViolation("Plugin worker sent an invalid RPC message.");
412
- return;
413
- }
414
- if (message.type === "ready") {
415
- if (!this.startPromise
416
- || this.ready
417
- || message.requestId !== this.initializationRequestId
418
- || !isRecord(message.payload)
419
- || !Array.isArray(message.payload.operations)
420
- || !message.payload.operations.every(isSafeOperationName)
421
- || new Set(message.payload.operations).size !== message.payload.operations.length
422
- || typeof message.payload.activatedAtUtc !== "string"
423
- || !message.payload.activatedAtUtc) {
424
- this.handleProtocolViolation("Plugin worker ready response did not match initialization.");
425
- return;
426
- }
427
- clearTimeout(this.startTimer);
428
- this.startTimer = undefined;
429
- this.initializationRequestId = undefined;
430
- this.ready = true;
431
- this.startResolve?.(message.payload);
432
- this.startResolve = undefined;
433
- this.startReject = undefined;
434
- this.startPromise = undefined;
435
- return;
436
- }
437
- if (message.type === "hostRequest") {
438
- if (!isWorkerRequestId(message.requestId)) {
439
- this.handleProtocolViolation("Plugin worker Host API request id is invalid.");
440
- return;
441
- }
442
- const result = normalizeRpcResult(await handlePluginHostRequest(this.configuration, message.payload));
443
- try {
444
- this.post({ protocolVersion: workerProtocolVersion, type: "hostResult", requestId: message.requestId, result });
445
- } catch {
446
- // The worker may have exited while a Host API request was running.
447
- }
448
- return;
449
- }
450
- if (message.type === "result") {
451
- if (!isWorkerRequestId(message.requestId) || !isNexusResult(message.result)) {
452
- this.handleProtocolViolation("Plugin worker result is invalid.");
453
- return;
454
- }
455
- const pending = this.pending.get(message.requestId);
456
- if (!pending) {
457
- return;
458
- }
459
- if (pending.kind === "electronCallback") {
460
- this.handleProtocolViolation("Plugin worker used a normal result for an Electron callback.");
461
- return;
462
- }
463
- this.pending.delete(message.requestId);
464
- clearPendingTimer(pending);
465
- if (pending.kind === "shutdown") {
466
- clearTimeout(this.shutdownTimer);
467
- this.shutdownTimer = undefined;
468
- this.shutdownResolve = undefined;
469
- this.shutdownPromise = undefined;
470
- pending.resolve(message.result);
471
- this.kill();
472
- return;
473
- }
474
- pending.resolve(message.result);
475
- return;
476
- }
477
- if (message.type === "electronCallbackResult") {
478
- if (!isWorkerRequestId(message.requestId) || !isNexusResult(message.result)) {
479
- this.handleProtocolViolation("Plugin worker Electron callback result is invalid.");
480
- return;
481
- }
482
- const pending = this.pending.get(message.requestId);
483
- if (!pending) {
484
- return;
485
- }
486
- if (pending.kind !== "electronCallback") {
487
- this.handleProtocolViolation("Plugin worker used an Electron callback result for another request.");
488
- return;
489
- }
490
- this.pending.delete(message.requestId);
491
- clearPendingTimer(pending);
492
- pending.resolve(message.result);
237
+ function runActivationCleanup(loaded) {
238
+ try {
239
+ const deactivate = loaded.deactivate || (loaded.default && loaded.default.deactivate);
240
+ if (!deactivate) {
493
241
  return;
494
242
  }
495
- if (message.type === "fatal") {
496
- const error = new Error(isRecord(message.payload) && typeof message.payload.message === "string" ? message.payload.message : "Plugin worker failed.");
497
- if (this.startPromise) {
498
- this.failStart(error);
499
- } else {
500
- process.stderr.write("[nexus-plugin-dev] plugin worker failed: " + error.message + "\n");
501
- }
502
- this.kill();
243
+ if (typeof deactivate !== "function" || isAsyncFunction(deactivate)) {
244
+ process.stderr.write("[nexus-plugin-dev] activation cleanup must complete synchronously.\n");
503
245
  return;
504
246
  }
505
- this.handleProtocolViolation("Plugin worker sent a message in the wrong direction.");
506
- }
507
-
508
- handleExit(code) {
509
- electronFacadeBroker.disposePlugin(this.configuration.manifest.pluginId);
510
- this.child = undefined;
511
- this.ready = false;
512
- this.initializationRequestId = undefined;
513
- clearTimeout(this.startTimer);
514
- this.startTimer = undefined;
515
- clearTimeout(this.shutdownTimer);
516
- this.shutdownTimer = undefined;
517
- this.shutdownPromise = undefined;
518
- this.shutdownResolve = undefined;
519
- const result = fail("PluginWorkerExited", "Plugin worker process exited.", { code });
520
- if (this.startPromise) {
521
- this.failStart(new Error(result.error.message));
522
- }
523
- for (const pending of this.pending.values()) {
524
- clearPendingTimer(pending);
525
- pending.resolve(result);
526
- }
527
- this.pending.clear();
528
- if (!this.intentionalStop) {
529
- process.stderr.write("[nexus-plugin-dev] plugin worker exited with code " + String(code) + ".\n");
247
+ const result = deactivate("reload");
248
+ if (isPromiseLike(result)) {
249
+ observeRejectedLifecyclePromise(result, "activation cleanup");
250
+ process.stderr.write("[nexus-plugin-dev] activation cleanup must complete synchronously.\n");
530
251
  }
252
+ } catch (error) {
253
+ process.stderr.write("[nexus-plugin-dev] activation cleanup failed: " + errorMessage(error) + "\n");
531
254
  }
255
+ }
532
256
 
533
- failStart(error) {
534
- clearTimeout(this.startTimer);
535
- this.startTimer = undefined;
536
- this.initializationRequestId = undefined;
537
- this.startReject?.(error instanceof Error ? error : new Error(String(error)));
538
- this.startResolve = undefined;
539
- this.startReject = undefined;
540
- this.startPromise = undefined;
257
+ function deactivatePluginMainEntry(reason) {
258
+ if (!pluginModule || pluginDeactivated) {
259
+ return;
541
260
  }
542
-
543
- handleProtocolViolation(message) {
544
- const error = new Error(message);
545
- if (this.startPromise) {
546
- this.failStart(error);
547
- } else {
548
- process.stderr.write("[nexus-plugin-dev] " + message + "\n");
261
+ pluginDeactivated = true;
262
+ try {
263
+ const deactivate = pluginModule.deactivate || (pluginModule.default && pluginModule.default.deactivate);
264
+ if (!deactivate) {
265
+ return;
549
266
  }
550
- this.kill();
551
- }
552
-
553
- post(message) {
554
- if (!this.child) {
555
- throw new Error("Plugin worker process has exited.");
267
+ if (typeof deactivate !== "function" || isAsyncFunction(deactivate)) {
268
+ process.stderr.write("[nexus-plugin-dev] plugin deactivate must complete synchronously in Framework API 3.1.0.\n");
269
+ return;
556
270
  }
557
- this.child.postMessage(message);
558
- }
559
-
560
- kill() {
561
- if (this.child) {
562
- this.child.kill();
271
+ const result = deactivate(reason);
272
+ if (isPromiseLike(result)) {
273
+ observeRejectedLifecyclePromise(result, "deactivate");
274
+ process.stderr.write("[nexus-plugin-dev] plugin deactivate must complete synchronously in Framework API 3.1.0.\n");
563
275
  }
564
- }
565
-
566
- nextRequestId(kind) {
567
- this.requestSequence += 1;
568
- return this.configuration.manifest.pluginId + "-" + kind + "-" + String(this.requestSequence);
569
- }
570
- }
571
-
572
- function clearPendingTimer(pending) {
573
- if (pending && pending.timer) {
574
- clearTimeout(pending.timer);
575
- pending.timer = undefined;
276
+ } catch (error) {
277
+ process.stderr.write("[nexus-plugin-dev] plugin deactivation failed: " + errorMessage(error) + "\n");
278
+ } finally {
279
+ pluginActivated = false;
280
+ rendererOperations.clear();
576
281
  }
577
282
  }
578
283
 
579
- function readWorkerMessage(message) {
580
- return isRecord(message) && Object.prototype.hasOwnProperty.call(message, "data") ? message.data : message;
284
+ function observeRejectedLifecyclePromise(value, lifecycle) {
285
+ void Promise.resolve(value).catch(function reportLifecycleRejection(error) {
286
+ process.stderr.write("[nexus-plugin-dev] plugin " + lifecycle + " returned a rejected Promise: " + errorMessage(error) + "\n");
287
+ });
581
288
  }
582
289
 
583
- async function handlePluginHostRequest(workerConfiguration, request) {
584
- if (!isRecord(request) || typeof request.method !== "string") {
585
- return fail("InvalidRequest", "Plugin Host API request is invalid.");
586
- }
587
- const manifest = workerConfiguration.manifest;
588
- const params = isRecord(request.params) ? request.params : undefined;
589
- if (request.method === "electronFacade") {
590
- return electronFacadeBroker.invoke(manifest.pluginId, params);
591
- }
592
- if (request.method === "getCapabilityStatus") {
593
- return isPluginPermission(params && params.permission)
594
- ? ok(createPermissionSummary(params.permission))
595
- : fail("InvalidRequest", "Plugin capability name is invalid.");
596
- }
597
- if (request.method === "unsupportedCapability") {
598
- return unsupportedCapability(manifest, params && params.permission, params && params.operation);
599
- }
600
- if (request.method === "log") {
601
- if (!manifest.permissions.includes("businessLog")) {
602
- return fail("PluginPermissionDenied", "Plugin does not declare businessLog permission.");
603
- }
604
- if (!params || (params.level !== "info" && params.level !== "warn" && params.level !== "error") || typeof params.message !== "string" || !params.message.trim()) {
605
- return fail("InvalidRequest", "Plugin log request is invalid.");
606
- }
607
- return ok(appendLog(params.level, "plugin:" + manifest.pluginId, params.message.trim(), typeof params.exception === "string" ? params.exception : undefined));
608
- }
609
- if (request.method === "getSettings") {
610
- return manifest.permissions.includes("settings")
611
- ? ok(loadSettings())
612
- : fail("PluginPermissionDenied", "Plugin does not declare settings permission.");
613
- }
614
- if (request.method === "updateSettings") {
615
- if (!manifest.permissions.includes("settings")) {
616
- return fail("PluginPermissionDenied", "Plugin does not declare settings permission.");
617
- }
618
- if (!params || !isRecord(params.values)) {
619
- return fail("InvalidRequest", "Plugin settings values must be an object.");
290
+ function createActivationContext() {
291
+ const manifest = configuration.manifest;
292
+ const unsupported = function createUnsupportedApi(permission) {
293
+ const summary = createPermissionSummary(permission);
294
+ const target = {
295
+ permission,
296
+ supportState: summary.supportState,
297
+ describe: function describe() {
298
+ return ok(summary);
299
+ },
300
+ unsupported: function unsupported(operation) {
301
+ return unsupportedCapability(manifest, permission, operation);
302
+ }
303
+ };
304
+ return new Proxy(target, {
305
+ get: function getUnsupported(apiTarget, property) {
306
+ if (property in apiTarget) {
307
+ return Reflect.get(apiTarget, property);
308
+ }
309
+ if (typeof property === "symbol" || property === "then") {
310
+ return undefined;
311
+ }
312
+ return function reportUnsupported() {
313
+ return apiTarget.unsupported(property);
314
+ };
315
+ }
316
+ });
317
+ };
318
+ return {
319
+ pluginId: manifest.pluginId,
320
+ pluginDirectory: configuration.packageDirectory,
321
+ manifest,
322
+ host: {
323
+ hostVersion: configuration.hostVersion,
324
+ hostApiVersion: sdk.PLUGIN_FRAMEWORK_VERSION,
325
+ pluginId: manifest.pluginId,
326
+ pluginDataDirectory: configuration.pluginDataDirectory,
327
+ hasPermission: function hasPermission(permission) {
328
+ return manifest.permissions.includes(permission);
329
+ },
330
+ registerRendererMethod: function registerRendererMethod(name, handler) {
331
+ if (!isSafeOperationName(name) || typeof handler !== "function") {
332
+ return fail("InvalidRequest", "Plugin renderer operation registration is invalid.");
333
+ }
334
+ if (rendererOperations.has(name)) {
335
+ return fail("InvalidRequest", "Plugin renderer operation is already registered.", {
336
+ pluginId: manifest.pluginId,
337
+ operation: name
338
+ });
339
+ }
340
+ rendererOperations.set(name, handler);
341
+ return ok(true);
342
+ },
343
+ getCapabilityStatus: function getCapabilityStatus(permission) {
344
+ return isPluginPermission(permission)
345
+ ? ok(createPermissionSummary(permission))
346
+ : fail("InvalidRequest", "Plugin capability name is invalid.", { pluginId: manifest.pluginId, permission });
347
+ },
348
+ unsupportedCapability: function reportUnsupported(permission, operation) {
349
+ return unsupportedCapability(manifest, permission, operation);
350
+ },
351
+ log: function log(level, message, exception) {
352
+ if (!manifest.permissions.includes("businessLog")) {
353
+ return fail("PluginPermissionDenied", "Plugin does not declare businessLog permission.", { pluginId: manifest.pluginId });
354
+ }
355
+ if ((level !== "info" && level !== "warn" && level !== "error") || typeof message !== "string" || !message.trim()) {
356
+ return fail("InvalidRequest", "Plugin log request is invalid.", { pluginId: manifest.pluginId });
357
+ }
358
+ return ok(appendLog(level, "plugin:" + manifest.pluginId, message.trim(), typeof exception === "string" ? exception : undefined));
359
+ },
360
+ getSettings: function getSettings() {
361
+ return manifest.permissions.includes("settings")
362
+ ? ok(loadSettings())
363
+ : fail("PluginPermissionDenied", "Plugin does not declare settings permission.", { pluginId: manifest.pluginId });
364
+ },
365
+ updateSettings: function updateSettings(values) {
366
+ if (!manifest.permissions.includes("settings")) {
367
+ return fail("PluginPermissionDenied", "Plugin does not declare settings permission.", { pluginId: manifest.pluginId });
368
+ }
369
+ if (!isRecord(values)) {
370
+ return fail("InvalidRequest", "Plugin settings values must be an object.");
371
+ }
372
+ return ok(saveSettings(values));
373
+ },
374
+ filesystem: unsupported("filesystem"),
375
+ udp: unsupported("udp"),
376
+ serial: unsupported("serial"),
377
+ dialog: unsupported("dialog"),
378
+ externalProcess: unsupported("externalProcess")
620
379
  }
621
- return ok(saveSettings(params.values));
622
- }
623
- return fail("InvalidRequest", "Plugin Host API method is invalid.", { method: request.method });
380
+ };
624
381
  }
625
382
 
626
383
  function unsupportedCapability(manifest, permission, operation) {
627
384
  if (!isPluginPermission(permission)) {
628
- return fail("InvalidRequest", "Plugin capability name is invalid.", { permission });
385
+ return fail("InvalidRequest", "Plugin capability name is invalid.", { pluginId: manifest.pluginId, permission });
629
386
  }
630
387
  if (!manifest.permissions.includes(permission)) {
631
- return fail("PluginPermissionDenied", "Plugin does not declare " + permission + " permission.", { permission });
388
+ return fail("PluginPermissionDenied", "Plugin does not declare " + permission + " permission.", {
389
+ pluginId: manifest.pluginId,
390
+ permission
391
+ });
632
392
  }
633
393
  const summary = createPermissionSummary(permission);
634
394
  return fail(
635
395
  "PluginCapabilityUnsupported",
636
- "Plugin Host API capability is not available to trusted mainEntry.",
396
+ "This Host API capability is not available; use the native Electron or Node API from mainEntry.",
637
397
  {
398
+ pluginId: manifest.pluginId,
638
399
  permission,
639
400
  operation: typeof operation === "string" ? operation : undefined,
640
- supportState: summary.supportState,
641
- statusMessage: summary.statusMessage
401
+ supportState: summary.supportState
642
402
  }
643
403
  );
644
404
  }
@@ -680,8 +440,11 @@ async function verifyRendererContextSmoke() {
680
440
  if (!frame) {
681
441
  throw new Error("Plugin renderer frame was not found.");
682
442
  }
683
- if (configuration.manifest.hostApiVersion !== sdk.PLUGIN_FRAMEWORK_VERSION
684
- || (configuration.manifest.mainEntry && !pluginWorker)
443
+ if (!sdk.isPluginFrameworkVersionCompatible(
444
+ configuration.manifest.hostApiVersion,
445
+ sdk.PLUGIN_FRAMEWORK_VERSION
446
+ )
447
+ || (configuration.manifest.mainEntry && !pluginActivated)
685
448
  || configuration.manifest.minHostVersion === undefined) {
686
449
  throw new Error("Development Host API version contract is inconsistent.");
687
450
  }
@@ -817,15 +580,32 @@ async function invokeMain(params) {
817
580
  if (!isSafeOperationName(params.operation)) {
818
581
  return fail("InvalidRequest", "Plugin renderer operation name is invalid.", { operation: params.operation });
819
582
  }
820
- if (!pluginWorker) {
583
+ if (!pluginActivated) {
821
584
  return fail("PluginCapabilityUnsupported", "Plugin mainEntry is not available in this debug target.", { operation: params.operation });
822
585
  }
823
- return pluginWorker.invoke({
824
- surface: configuration.surface,
825
- contributionId: configuration.contributionId,
826
- operation: params.operation,
827
- input: isRecord(params.input) ? sanitizeJsonValue(params.input) : undefined
828
- });
586
+ const handler = rendererOperations.get(params.operation);
587
+ if (!handler) {
588
+ return fail("NotFound", "Plugin renderer operation was not registered.", {
589
+ pluginId: configuration.manifest.pluginId,
590
+ operation: params.operation
591
+ });
592
+ }
593
+ try {
594
+ const value = await handler({
595
+ pluginId: configuration.manifest.pluginId,
596
+ surface: configuration.surface,
597
+ contributionId: configuration.contributionId,
598
+ operation: params.operation,
599
+ input: isRecord(params.input) ? decodeOperationValue(params.input) : undefined
600
+ });
601
+ return normalizeOperationResult(value);
602
+ } catch (error) {
603
+ return fail("PluginMainOperationFailed", "Plugin renderer operation failed.", {
604
+ pluginId: configuration.manifest.pluginId,
605
+ operation: params.operation,
606
+ error: errorMessage(error)
607
+ });
608
+ }
829
609
  }
830
610
 
831
611
  function createWindow() {
@@ -905,12 +685,10 @@ function saveSettings(values) {
905
685
  const document = {
906
686
  schemaVersion: sdk.PLUGIN_MANIFEST_SCHEMA_VERSION,
907
687
  pluginId: configuration.manifest.pluginId,
908
- values: Object.assign({}, existing.values, sanitizeJsonObject(values))
688
+ values: { ...existing.values, ...sanitizeJsonObject(values) }
909
689
  };
910
690
  const filePath = settingsFilePath();
911
- const temporaryPath = filePath + ".tmp";
912
- fs.writeFileSync(temporaryPath, JSON.stringify(document, null, 2) + "\n", "utf8");
913
- fs.renameSync(temporaryPath, filePath);
691
+ fs.writeFileSync(filePath, JSON.stringify(document, null, 2) + "\n", "utf8");
914
692
  return document;
915
693
  }
916
694
 
@@ -973,37 +751,131 @@ function isSafeOperationName(value) {
973
751
  });
974
752
  }
975
753
 
976
- function isNexusResult(value) {
977
- if (!isRecord(value) || typeof value.ok !== "boolean") {
978
- return false;
754
+ function normalizeOperationResult(value) {
755
+ if (isNexusResult(value)) {
756
+ return value.ok
757
+ ? ok(encodeOperationValue(value.value))
758
+ : fail(value.error.code, value.error.message, encodeOperationValue(value.error.details));
979
759
  }
980
- return value.ok
981
- ? Object.prototype.hasOwnProperty.call(value, "value")
982
- : isRecord(value.error) && typeof value.error.code === "string" && typeof value.error.message === "string";
760
+ return ok(encodeOperationValue(value));
983
761
  }
984
762
 
985
- function normalizeRpcResult(result) {
986
- if (!isNexusResult(result)) {
987
- return fail("UnexpectedFailure", "Plugin Host API returned an invalid result.");
763
+ function isNexusResult(value) {
764
+ return isRecord(value)
765
+ && typeof value.ok === "boolean"
766
+ && (value.ok
767
+ ? Object.prototype.hasOwnProperty.call(value, "value")
768
+ : isRecord(value.error) && typeof value.error.code === "string" && typeof value.error.message === "string");
769
+ }
770
+
771
+ function encodeOperationValue(value, seen) {
772
+ const visited = seen || new WeakSet();
773
+ if (value === undefined) {
774
+ return { __nexusWire: "undefined" };
988
775
  }
989
- if (result.ok) {
990
- return { ok: true, value: sanitizeJsonValue(result.value) };
776
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
777
+ return value;
991
778
  }
992
- return {
993
- ok: false,
994
- error: {
995
- code: result.error.code,
996
- message: result.error.message,
997
- ...(result.error.details === undefined ? {} : { details: sanitizeJsonValue(result.error.details) })
779
+ if (typeof value === "bigint") {
780
+ return { __nexusWire: "bigint", value: value.toString(10) };
781
+ }
782
+ if (typeof value === "number") {
783
+ return Number.isFinite(value) ? value : null;
784
+ }
785
+ if (typeof value !== "object" || visited.has(value)) {
786
+ return null;
787
+ }
788
+ if (value instanceof Date) {
789
+ return { __nexusWire: "date", value: value.toISOString() };
790
+ }
791
+ if (value instanceof Error) {
792
+ return {
793
+ __nexusWire: "error",
794
+ name: value.name,
795
+ message: value.message,
796
+ ...(value.stack ? { stack: value.stack } : {})
797
+ };
798
+ }
799
+ if (Buffer.isBuffer(value)) {
800
+ return { __nexusWire: "buffer", value: value.toString("hex") };
801
+ }
802
+ if (!Array.isArray(value) && !isPlainObject(value)) {
803
+ return null;
804
+ }
805
+ visited.add(value);
806
+ try {
807
+ if (Array.isArray(value)) {
808
+ return value.map(function encodeItem(item) {
809
+ return encodeOperationValue(item, visited);
810
+ });
998
811
  }
999
- };
812
+ const encoded = Object.fromEntries(Object.entries(value).map(function encodeEntry(entry) {
813
+ return [entry[0], encodeOperationValue(entry[1], visited)];
814
+ }));
815
+ return Object.prototype.hasOwnProperty.call(value, "__nexusWire")
816
+ ? { __nexusWire: "object", value: encoded }
817
+ : encoded;
818
+ } finally {
819
+ visited.delete(value);
820
+ }
1000
821
  }
1001
822
 
1002
- function isWorkerRequestId(value) {
1003
- return typeof value === "string"
1004
- && value.length > 0
1005
- && value.length <= 128
1006
- && /^[A-Za-z0-9._:-]+$/.test(value);
823
+ function decodeOperationValue(value, seen) {
824
+ if (value === null || typeof value !== "object") {
825
+ return value;
826
+ }
827
+ const visited = seen || new WeakSet();
828
+ if (visited.has(value)) {
829
+ return null;
830
+ }
831
+ if (value.__nexusWire === "undefined") {
832
+ return undefined;
833
+ }
834
+ if (value.__nexusWire === "bigint" && typeof value.value === "string") {
835
+ try {
836
+ return BigInt(value.value);
837
+ } catch {
838
+ return null;
839
+ }
840
+ }
841
+ if (value.__nexusWire === "date" && typeof value.value === "string") {
842
+ const date = new Date(value.value);
843
+ return Number.isNaN(date.getTime()) ? null : date;
844
+ }
845
+ if (value.__nexusWire === "buffer" && typeof value.value === "string") {
846
+ return value.value.length % 2 === 0 && /^[0-9a-f]*$/i.test(value.value)
847
+ ? Buffer.from(value.value, "hex")
848
+ : null;
849
+ }
850
+ if (value.__nexusWire === "error" && typeof value.message === "string") {
851
+ const error = new Error(value.message);
852
+ if (typeof value.name === "string") {
853
+ error.name = value.name;
854
+ }
855
+ if (typeof value.stack === "string") {
856
+ error.stack = value.stack;
857
+ }
858
+ return error;
859
+ }
860
+ if (value.__nexusWire === "object" && isRecord(value.value)) {
861
+ return decodeOperationValue(value.value, visited);
862
+ }
863
+ visited.add(value);
864
+ try {
865
+ if (Array.isArray(value)) {
866
+ return value.map(function decodeItem(item) {
867
+ return decodeOperationValue(item, visited);
868
+ });
869
+ }
870
+ if (!isPlainObject(value)) {
871
+ return null;
872
+ }
873
+ return Object.fromEntries(Object.entries(value).map(function decodeEntry(entry) {
874
+ return [entry[0], decodeOperationValue(entry[1], visited)];
875
+ }));
876
+ } finally {
877
+ visited.delete(value);
878
+ }
1007
879
  }
1008
880
 
1009
881
  function isPluginPermission(value) {
@@ -1011,9 +883,6 @@ function isPluginPermission(value) {
1011
883
  }
1012
884
 
1013
885
  function permissionSupportState(permission) {
1014
- if (hostControlPermissions.has(permission)) {
1015
- return "hostControl";
1016
- }
1017
886
  if (implementedPermissions.has(permission)) {
1018
887
  return "implemented";
1019
888
  }
@@ -1027,8 +896,6 @@ function createPermissionSummary(permission) {
1027
896
  const supportState = permissionSupportState(permission);
1028
897
  const statusMessage = supportState === "implemented"
1029
898
  ? "Available to trusted mainEntry Host API."
1030
- : supportState === "hostControl"
1031
- ? "Grants direct control of Nexus Electron host objects; global changes can affect the application and may not be reversible."
1032
899
  : supportState === "routeGuard"
1033
900
  ? "Declared for renderer route guards; not exposed to trusted mainEntry Host API."
1034
901
  : "Reserved for a future Host API; trusted mainEntry calls return PluginCapabilityUnsupported.";
@@ -1061,6 +928,22 @@ function isRecord(value) {
1061
928
  return typeof value === "object" && value !== null && !Array.isArray(value);
1062
929
  }
1063
930
 
931
+ function isPlainObject(value) {
932
+ if (!isRecord(value)) {
933
+ return false;
934
+ }
935
+ const prototype = Object.getPrototypeOf(value);
936
+ return prototype === Object.prototype || prototype === null;
937
+ }
938
+
939
+ function isPromiseLike(value) {
940
+ return isRecord(value) && typeof value.then === "function";
941
+ }
942
+
943
+ function isAsyncFunction(value) {
944
+ return typeof value === "function" && value.constructor && value.constructor.name === "AsyncFunction";
945
+ }
946
+
1064
947
  function isLogLevel(value) {
1065
948
  return value === "debug" || value === "info" || value === "warn" || value === "error";
1066
949
  }
@@ -1109,28 +992,6 @@ function isStrictVersion(value) {
1109
992
  }));
1110
993
  }
1111
994
 
1112
- function parseWorkerInspector(args) {
1113
- const prefix = "--worker-inspector=";
1114
- const matches = args.filter(function isWorkerInspectorArgument(value) {
1115
- return value.startsWith(prefix);
1116
- });
1117
- if (matches.length > 1) {
1118
- throw new Error("--worker-inspector must not be repeated.");
1119
- }
1120
- if (matches.length === 0) {
1121
- return undefined;
1122
- }
1123
- const match = /^(inspect|inspect-brk):([0-9]+)$/.exec(matches[0].slice(prefix.length));
1124
- const port = match ? Number(match[2]) : 0;
1125
- if (!match || !Number.isInteger(port) || port < 1 || port > 65535) {
1126
- throw new Error("--worker-inspector must use inspect:<port> or inspect-brk:<port>.");
1127
- }
1128
- return {
1129
- execArg: "--" + match[1] + "=" + String(port),
1130
- breakOnStart: match[1] === "inspect-brk"
1131
- };
1132
- }
1133
-
1134
995
  function requiredArgument(args, name) {
1135
996
  const prefix = name + "=";
1136
997
  const argument = args.find(function findArgument(value) {