@aiot-nexus/plugin-sdk 1.1.0 → 2.0.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.
@@ -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.manifestChangeRequiresRestart = exports.classifyPluginSourceChange = exports.runPluginNexusDev = void 0;
26
+ exports.shouldSuppressNativeBuildEvent = exports.isRetryablePluginDeploymentError = exports.createNexusHostArguments = exports.mergePluginSourceChange = exports.manifestChangeRequiresPackageUpdate = exports.classifyPluginSourceChange = exports.recoverPreparedPluginPackageUpdate = 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"));
@@ -41,8 +41,6 @@ class PluginNexusDevSession {
41
41
  this.control = new nexus_dev_control_1.NexusDevControlServer();
42
42
  this.deployedPluginDirectory = "";
43
43
  this.expectedExits = new Set();
44
- this.provisionalChildren = new Set();
45
- this.restarting = false;
46
44
  this.stopping = false;
47
45
  this.terminalSettled = false;
48
46
  this.terminalPromise = new Promise((resolve) => {
@@ -76,7 +74,7 @@ class PluginNexusDevSession {
76
74
  try {
77
75
  await this.control.start();
78
76
  this.disposeDisconnectHandler = this.control.setDisconnectHandler((error) => {
79
- if (!this.stopping && !this.restarting) {
77
+ if (!this.stopping) {
80
78
  this.io.stderr("Packaged Nexus plugin development control disconnected: " + error.message);
81
79
  this.settleTerminal(1);
82
80
  }
@@ -88,7 +86,7 @@ class PluginNexusDevSession {
88
86
  }
89
87
  this.watcher = new PluginSourceWatcher(this.projectDirectory, async (kind) => {
90
88
  const currentTarget = this.requireTarget();
91
- await applyPluginSourceChange(kind, this.options, currentTarget, this.deployedPluginDirectory, this.control, this.io, () => this.restartHost(), () => this.runBuild(() => (0, dev_1.buildPluginRendererForDevelopment)(currentTarget.projectDirectory)));
89
+ await applyPluginSourceChange(kind, this.options, currentTarget, this.deployedPluginDirectory, this.control, this.io, () => this.updatePluginPackage(), () => this.runBuild(() => (0, dev_1.buildPluginRendererForDevelopment)(currentTarget.projectDirectory)));
92
90
  }, this.io);
93
91
  this.watcher.start();
94
92
  this.io.stdout("Watching plugin source changes. Press Ctrl+C to stop.");
@@ -105,9 +103,9 @@ class PluginNexusDevSession {
105
103
  await this.control.close();
106
104
  }
107
105
  }
108
- async restartHost() {
106
+ async updatePluginPackage() {
109
107
  const previousTarget = this.requireTarget();
110
- this.io.stdout("Building complete plugin package before restarting Nexus...");
108
+ this.io.stdout("Building complete plugin package before restarting the plugin worker...");
111
109
  this.runBuild(() => (0, dev_1.buildPluginForDevelopment)(this.projectDirectory));
112
110
  const updatedTarget = (0, dev_1.preparePluginDebugTarget)({
113
111
  projectDirectory: this.projectDirectory,
@@ -119,27 +117,19 @@ class PluginNexusDevSession {
119
117
  if (updatedTarget.pluginId !== previousTarget.pluginId) {
120
118
  throw new dev_1.PluginDevError("Plugin id cannot change during a Nexus development session.");
121
119
  }
122
- this.restarting = true;
123
120
  let transaction;
124
- let transactionCommitted = false;
121
+ let prepareAttempted = false;
125
122
  try {
126
- await this.shutdownHostForRestart();
127
- transaction = await retryFileOperation(() => (0, nexus_dev_deployment_1.beginPluginPackageDeployment)(updatedTarget.packageDirectory, this.nexusExecutable, updatedTarget.pluginId), "replace the plugin package");
128
- try {
129
- const updatedChild = await this.launchHost(updatedTarget, true);
130
- await this.ensureProvisionalHostIsStable(updatedChild);
131
- this.acceptProvisionalHost(updatedChild);
123
+ prepareAttempted = true;
124
+ const prepareResult = await this.control.sendUpdate("packageUpdatePreparing", previousTarget.pluginId);
125
+ if (!prepareResult.ok) {
126
+ throw new dev_1.PluginDevError(prepareResult.message);
132
127
  }
133
- catch (error) {
134
- await this.stopCurrentHost();
135
- await retryFileOperation(() => transaction?.rollback(), "roll back the plugin package");
136
- const previousChild = await this.launchHost(previousTarget, true);
137
- await this.ensureProvisionalHostIsStable(previousChild);
138
- this.acceptProvisionalHost(previousChild);
139
- throw new dev_1.PluginDevError("Updated Nexus host failed to start; the previous plugin package was restored. " + errorMessage(error));
128
+ transaction = await retryFileOperation(() => (0, nexus_dev_deployment_1.beginPluginPackageDeployment)(updatedTarget.packageDirectory, this.nexusExecutable, updatedTarget.pluginId), "replace the plugin package");
129
+ const updateResult = await this.control.sendUpdate("packageUpdated", updatedTarget.pluginId);
130
+ if (!updateResult.ok) {
131
+ throw new dev_1.PluginDevError(updateResult.message);
140
132
  }
141
- transactionCommitted = true;
142
- this.restarting = false;
143
133
  try {
144
134
  await retryFileOperation(() => transaction?.commit(), "remove the previous plugin package backup");
145
135
  }
@@ -147,24 +137,21 @@ class PluginNexusDevSession {
147
137
  this.io.stderr("Updated plugin is active, but its previous package backup could not be removed: " + errorMessage(error));
148
138
  }
149
139
  this.target = updatedTarget;
150
- this.io.stdout("Packaged Nexus restarted for plugin mainEntry update. PID: " + String(this.child?.pid || "unknown"));
140
+ this.io.stdout("Plugin package reloaded in a new worker; Nexus PID unchanged: " + String(this.child?.pid || "unknown"));
151
141
  }
152
142
  catch (error) {
153
- if (!this.child) {
154
- if (transaction && !transactionCommitted) {
155
- await retryFileOperation(() => transaction?.rollback(), "roll back the plugin package");
143
+ if (prepareAttempted) {
144
+ try {
145
+ await recoverPreparedPluginPackageUpdate(transaction, previousTarget.pluginId, this.control);
146
+ }
147
+ catch (recoveryError) {
148
+ throw new dev_1.PluginDevError(errorMessage(error) + " Package update recovery failed: " + errorMessage(recoveryError));
156
149
  }
157
- const previousChild = await this.launchHost(previousTarget, true);
158
- await this.ensureProvisionalHostIsStable(previousChild);
159
- this.acceptProvisionalHost(previousChild);
160
150
  }
161
151
  throw error;
162
152
  }
163
- finally {
164
- this.restarting = false;
165
- }
166
153
  }
167
- async launchHost(target, provisional = false) {
154
+ async launchHost(target) {
168
155
  const hostArgs = createNexusHostArguments(this.control, target, this.options);
169
156
  this.io.stdout("Starting packaged Nexus host: " + this.nexusExecutable);
170
157
  const child = (0, child_process_1.spawn)(this.nexusExecutable, hostArgs, {
@@ -174,13 +161,10 @@ class PluginNexusDevSession {
174
161
  windowsHide: false
175
162
  });
176
163
  this.child = child;
177
- if (provisional) {
178
- this.provisionalChildren.add(child);
179
- }
180
164
  const hostExit = waitForHostExit(child);
181
165
  this.childExit = hostExit;
182
166
  hostExit.then((result) => this.handleHostExit(child, result), (error) => {
183
- if (!this.restarting && !this.stopping) {
167
+ if (!this.stopping) {
184
168
  this.io.stderr(errorMessage(error));
185
169
  this.settleTerminal(1);
186
170
  }
@@ -205,8 +189,7 @@ class PluginNexusDevSession {
205
189
  this.child = undefined;
206
190
  this.childExit = undefined;
207
191
  }
208
- const provisional = this.provisionalChildren.delete(child);
209
- if (this.expectedExits.delete(child) || provisional || this.stopping) {
192
+ if (this.expectedExits.delete(child) || this.stopping) {
210
193
  return;
211
194
  }
212
195
  const exitCode = result.status === null ? 1 : result.status;
@@ -215,30 +198,6 @@ class PluginNexusDevSession {
215
198
  + (result.signal ? " (" + result.signal + ")" : "") + ".");
216
199
  this.settleTerminal(exitCode === 0 ? 1 : exitCode);
217
200
  }
218
- async shutdownHostForRestart() {
219
- const child = this.child;
220
- const hostExit = this.childExit;
221
- if (!child || !hostExit) {
222
- throw new dev_1.PluginDevError("Packaged Nexus host is not running.");
223
- }
224
- this.expectedExits.add(child);
225
- try {
226
- await this.control.requestShutdown();
227
- }
228
- catch (error) {
229
- this.expectedExits.delete(child);
230
- throw error;
231
- }
232
- try {
233
- await promiseWithTimeout(hostExit, 10000, "Nexus did not exit after accepting the plugin development restart.");
234
- }
235
- catch (error) {
236
- child.kill();
237
- await promiseWithTimeout(hostExit, 5000, "Nexus could not be stopped for the plugin development restart.");
238
- this.io.stderr("Nexus required forced termination before plugin replacement: " + errorMessage(error));
239
- }
240
- await this.control.waitForDisconnection();
241
- }
242
201
  async stopCurrentHost() {
243
202
  const child = this.child;
244
203
  const hostExit = this.childExit;
@@ -256,22 +215,10 @@ class PluginNexusDevSession {
256
215
  await promiseWithTimeout(hostExit, 3000, "Nexus did not terminate after plugin development shutdown.");
257
216
  }
258
217
  catch {
259
- // The CLI is already stopping; do not start another host or a restart loop.
218
+ // The CLI is already stopping; do not start another shutdown path.
260
219
  }
261
220
  }
262
221
  }
263
- async ensureProvisionalHostIsStable(child) {
264
- await new Promise((resolve) => setTimeout(resolve, 250));
265
- if (this.child !== child || child.exitCode !== null || child.signalCode !== null || !this.control.isConnected()) {
266
- throw new dev_1.PluginDevError("Restarted Nexus host did not remain connected after authentication.");
267
- }
268
- }
269
- acceptProvisionalHost(child) {
270
- if (this.child !== child || !this.control.isConnected()) {
271
- throw new dev_1.PluginDevError("Restarted Nexus host disconnected before activation completed.");
272
- }
273
- this.provisionalChildren.delete(child);
274
- }
275
222
  runBuild(operation) {
276
223
  return this.watcher ? this.watcher.runBuild(operation) : operation();
277
224
  }
@@ -298,6 +245,30 @@ class PluginNexusDevSession {
298
245
  this.terminalResolve = undefined;
299
246
  }
300
247
  }
248
+ async function recoverPreparedPluginPackageUpdate(transaction, pluginId, control) {
249
+ const recoveryErrors = [];
250
+ if (transaction) {
251
+ try {
252
+ await retryFileOperation(() => transaction.rollback(), "roll back the plugin package");
253
+ }
254
+ catch (error) {
255
+ recoveryErrors.push("package rollback failed: " + errorMessage(error));
256
+ }
257
+ }
258
+ try {
259
+ const rollbackResult = await control.sendUpdate("packageUpdated", pluginId);
260
+ if (!rollbackResult.ok) {
261
+ recoveryErrors.push("previous worker activation failed: " + rollbackResult.message);
262
+ }
263
+ }
264
+ catch (error) {
265
+ recoveryErrors.push("previous worker activation failed: " + errorMessage(error));
266
+ }
267
+ if (recoveryErrors.length > 0) {
268
+ throw new dev_1.PluginDevError(recoveryErrors.join(" "));
269
+ }
270
+ }
271
+ exports.recoverPreparedPluginPackageUpdate = recoverPreparedPluginPackageUpdate;
301
272
  function classifyPluginSourceChange(projectDirectory, changedPath) {
302
273
  const relativePath = path.relative(path.resolve(projectDirectory), path.resolve(changedPath)).replace(/\\/g, "/");
303
274
  if (relativePath.startsWith("../") || relativePath === ".." || path.isAbsolute(relativePath)) {
@@ -310,7 +281,7 @@ function classifyPluginSourceChange(projectDirectory, changedPath) {
310
281
  return "renderer";
311
282
  }
312
283
  if (relativePath.toLowerCase().endsWith(".node") || relativePath === "binding.gyp") {
313
- return "restart";
284
+ return "package";
314
285
  }
315
286
  if (relativePath.startsWith("src/")
316
287
  || relativePath.startsWith("scripts/")
@@ -318,13 +289,13 @@ function classifyPluginSourceChange(projectDirectory, changedPath) {
318
289
  || relativePath === "package-lock.json"
319
290
  || relativePath === "npm-shrinkwrap.json"
320
291
  || relativePath === "tsconfig.main.json") {
321
- return "restart";
292
+ return "package";
322
293
  }
323
294
  return undefined;
324
295
  }
325
296
  exports.classifyPluginSourceChange = classifyPluginSourceChange;
326
- function manifestChangeRequiresRestart(previous, next) {
327
- const restartSensitiveFields = [
297
+ function manifestChangeRequiresPackageUpdate(previous, next) {
298
+ const packageSensitiveFields = [
328
299
  "schemaVersion",
329
300
  "pluginId",
330
301
  "pluginVersion",
@@ -334,17 +305,17 @@ function manifestChangeRequiresRestart(previous, next) {
334
305
  "mainEntry",
335
306
  "deferred"
336
307
  ];
337
- return restartSensitiveFields.some((field) => stableJson(previous[field]) !== stableJson(next[field]));
308
+ return packageSensitiveFields.some((field) => stableJson(previous[field]) !== stableJson(next[field]));
338
309
  }
339
- exports.manifestChangeRequiresRestart = manifestChangeRequiresRestart;
310
+ exports.manifestChangeRequiresPackageUpdate = manifestChangeRequiresPackageUpdate;
340
311
  function mergePluginSourceChange(current, incoming) {
341
- const priority = { renderer: 1, manifest: 2, restart: 3 };
312
+ const priority = { renderer: 1, manifest: 2, package: 3 };
342
313
  return !current || priority[incoming] > priority[current] ? incoming : current;
343
314
  }
344
315
  exports.mergePluginSourceChange = mergePluginSourceChange;
345
- async function applyPluginSourceChange(kind, options, initialTarget, deployedPluginDirectory, control, io, restartHost, buildRenderer) {
346
- if (kind === "restart") {
347
- await restartHost();
316
+ async function applyPluginSourceChange(kind, options, initialTarget, deployedPluginDirectory, control, io, updatePackage, buildRenderer) {
317
+ if (kind === "package") {
318
+ await updatePackage();
348
319
  return;
349
320
  }
350
321
  try {
@@ -363,8 +334,8 @@ async function applyPluginSourceChange(kind, options, initialTarget, deployedPlu
363
334
  const rendererDirectory = path.join(updatedTarget.packageDirectory, "renderer");
364
335
  if (kind === "manifest") {
365
336
  const previousManifest = readManifest(path.join(deployedPluginDirectory, "manifest.json"));
366
- if (manifestChangeRequiresRestart(previousManifest, updatedTarget.manifest)) {
367
- await restartHost();
337
+ if (manifestChangeRequiresPackageUpdate(previousManifest, updatedTarget.manifest)) {
338
+ await updatePackage();
368
339
  return;
369
340
  }
370
341
  const transaction = (0, nexus_dev_deployment_1.beginManifestAndRendererDeployment)(path.join(updatedTarget.packageDirectory, "manifest.json"), rendererDirectory, deployedPluginDirectory);
@@ -398,13 +369,13 @@ async function finishUpdateTransaction(transaction, updateType, pluginId, contro
398
369
  throw new dev_1.PluginDevError(result.message);
399
370
  }
400
371
  transaction.commit();
401
- io.stdout((updateType === "rendererUpdated" ? "Plugin renderer reloaded." : "Plugin manifest rescanned.")
402
- + (result.requiresRestart ? " Existing mainEntry state will remain active until the next host restart." : ""));
372
+ io.stdout(updateType === "rendererUpdated" ? "Plugin renderer reloaded." : "Plugin manifest rescanned.");
403
373
  }
404
374
  function createNexusHostArguments(control, target, options) {
405
375
  const args = [];
406
376
  if (options.inspectorMode) {
407
- args.push("--" + options.inspectorMode + "=" + String(options.inspectorPort || 9229));
377
+ args.push("--plugin-dev-worker-inspector=" + options.inspectorMode + ":" + String(options.inspectorPort || 9229));
378
+ args.push("--plugin-dev-worker-plugin=" + target.pluginId);
408
379
  }
409
380
  args.push("--plugin-dev", "--plugin-dev-control=" + control.pipePath, "--plugin-dev-token=" + control.token, "--page=" + resolveContributionPage(target));
410
381
  if (target.surface !== "business") {
@@ -95,6 +95,7 @@ export interface BusinessLogAppendRequest {
95
95
  category?: string;
96
96
  exception?: string;
97
97
  }
98
+ export type PluginHostLogLevel = Exclude<BusinessLogEntry["level"], "debug">;
98
99
  export interface PluginRendererOperationRequest<TInput extends Record<string, unknown> = Record<string, unknown>> {
99
100
  readonly pluginId: string;
100
101
  readonly surface: PluginRendererSurface;
@@ -102,12 +103,12 @@ export interface PluginRendererOperationRequest<TInput extends Record<string, un
102
103
  readonly operation: string;
103
104
  readonly input?: TInput;
104
105
  }
105
- export type PluginRendererOperationHandler<TInput extends Record<string, unknown> = Record<string, unknown>, TResult = unknown> = (request: PluginRendererOperationRequest<TInput>) => NexusResult<TResult> | TResult;
106
+ export type PluginRendererOperationHandler<TInput extends Record<string, unknown> = Record<string, unknown>, TResult = unknown> = (request: PluginRendererOperationRequest<TInput>) => NexusResult<TResult> | TResult | Promise<NexusResult<TResult> | TResult>;
106
107
  export interface PluginUnsupportedCapabilityApi {
107
108
  readonly permission: PluginPermission;
108
109
  readonly supportState: PluginPermissionSupportState;
109
- describe(): NexusResult<PluginPermissionSummary>;
110
- unsupported(operation?: unknown): NexusResult<never>;
110
+ describe(): Promise<NexusResult<PluginPermissionSummary>>;
111
+ unsupported(operation?: unknown): Promise<NexusResult<never>>;
111
112
  [operation: string]: unknown;
112
113
  }
113
114
  export interface PluginHostApi {
@@ -116,12 +117,12 @@ export interface PluginHostApi {
116
117
  readonly pluginId: string;
117
118
  readonly pluginDataDirectory: string;
118
119
  hasPermission(permission: PluginPermission): boolean;
119
- registerRendererMethod(name: string, handler: PluginRendererOperationHandler): NexusResult<boolean>;
120
- getCapabilityStatus(permission: unknown): NexusResult<PluginPermissionSummary>;
121
- unsupportedCapability(permission: unknown, operation?: unknown): NexusResult<never>;
122
- log(level: BusinessLogEntry["level"], message: string, exception?: string): NexusResult<BusinessLogEntry>;
123
- getSettings(): NexusResult<PluginSettingsDocument>;
124
- updateSettings(values: Record<string, unknown>): NexusResult<PluginSettingsDocument>;
120
+ registerRendererMethod(name: string, handler: PluginRendererOperationHandler): Promise<NexusResult<boolean>>;
121
+ getCapabilityStatus(permission: unknown): Promise<NexusResult<PluginPermissionSummary>>;
122
+ unsupportedCapability(permission: unknown, operation?: unknown): Promise<NexusResult<never>>;
123
+ log(level: PluginHostLogLevel, message: string, exception?: string): Promise<NexusResult<BusinessLogEntry>>;
124
+ getSettings(): Promise<NexusResult<PluginSettingsDocument>>;
125
+ updateSettings(values: Record<string, unknown>): Promise<NexusResult<PluginSettingsDocument>>;
125
126
  filesystem: PluginUnsupportedCapabilityApi;
126
127
  udp: PluginUnsupportedCapabilityApi;
127
128
  serial: PluginUnsupportedCapabilityApi;
@@ -136,7 +137,9 @@ export interface PluginMainActivationContext {
136
137
  }
137
138
  export interface PluginMainModule {
138
139
  activate?: (context: PluginMainActivationContext) => unknown;
140
+ deactivate?: (reason: string) => unknown;
139
141
  default?: {
140
142
  activate?: (context: PluginMainActivationContext) => unknown;
143
+ deactivate?: (reason: string) => unknown;
141
144
  };
142
145
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiot-nexus/plugin-sdk",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Types, PluginFrame client, and CLI for AIoT Nexus independent plugins.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -30,12 +30,14 @@
30
30
  "dist",
31
31
  "dev-host",
32
32
  "templates",
33
- "README.md"
33
+ "README.md",
34
+ "scripts/sdk-update-check.js"
34
35
  ],
35
36
  "scripts": {
36
37
  "build": "node scripts/build.js",
38
+ "postinstall": "node scripts/sdk-update-check.js --install",
37
39
  "verify:package": "node scripts/verify-package.js",
38
- "test": "npm run build && node tests/package.test.js && node tests/cli.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": "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",
39
41
  "prepack": "node scripts/prepack.js",
40
42
  "pack:sdk": "npm run build && npm run verify:package && node scripts/pack.js",
41
43
  "publish:sdk": "npm publish"