@aiot-nexus/plugin-sdk 1.1.1 → 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.1",
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": {
@@ -9,7 +9,7 @@
9
9
  "cwd": "${workspaceFolder}"
10
10
  },
11
11
  {
12
- "name": "Attach Nexus plugin main (9229)",
12
+ "name": "Attach Nexus plugin worker (9229)",
13
13
  "type": "node",
14
14
  "request": "attach",
15
15
  "address": "127.0.0.1",
@@ -15,20 +15,20 @@ 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__/`. Copy that directory to the Nexus executable's `Plugins/__PLUGIN_ID__/` directory, then restart Nexus.
18
+ The deployable plugin package is `dist/__PLUGIN_ID__/`. For a manual installation, stop Nexus before replacing `Plugins/__PLUGIN_ID__/`, then start it again. During development, use `npm run dev:nexus` so the host coordinates worker shutdown, atomic replacement, and worker reactivation without changing the Nexus PID.
19
19
 
20
- The renderer calls the main-process `demo.echo` operation through `@aiot-nexus/plugin-sdk`. Run `npm run validate` after changing the manifest or output layout.
20
+ The renderer calls the plugin-worker `demo.echo` operation through `@aiot-nexus/plugin-sdk`. Run `npm run validate` after changing the manifest or output layout.
21
21
 
22
22
  The renderer follows `context.language` and `context.themeMode` from Nexus, subscribes to in-place context updates without reloading the iframe, and includes `en-US` and `zh-CN` message catalogs. The plugin page has no language or theme selector; the controls shown by `npm run dev` belong to the debug host and simulate Nexus context changes. Add or edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
23
23
 
24
24
  Use `npm run dev:web` for renderer-only UI work in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. It does not execute `mainEntry` or overwrite the deployable `dist` package. Language and theme controls still simulate Nexus context, while `invokeMain` is intentionally unavailable. Pass `-- --no-open` or `-- --port <port>` when needed; use `npm run dev` for Host API integration.
25
25
 
26
- 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 rebuild the full package and restart Nexus. Failed builds keep the previous deployment, and a failed restarted host rolls back the package. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
26
+ Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` for live development against a compiled, packaged Nexus. The CLI atomically deploys this plugin to `<exeDir>/Plugins/__PLUGIN_ID__/`. Renderer changes rebuild and reload the iframe without changing the Nexus PID; safe manifest changes rescan contributions in the same process; `mainEntry`, dependency, native-module, or worker-sensitive manifest changes rebuild the full package, stop the old plugin worker, replace the package, and start a new worker while keeping the Nexus PID. Failed builds keep the previous deployment; a failed worker reload rolls the package back and restores the previous worker. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
27
27
 
28
28
  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.
29
29
 
30
30
  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.
31
31
 
32
- For main-process 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 main (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.
32
+ 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.
33
33
 
34
34
  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,7 +1,7 @@
1
1
  import type { PluginMainActivationContext } from "@aiot-nexus/plugin-sdk";
2
2
 
3
- export function activate(context: PluginMainActivationContext): void {
4
- const registration = context.host.registerRendererMethod("demo.echo", (request) => {
3
+ export async function activate(context: PluginMainActivationContext): Promise<void> {
4
+ const registration = await context.host.registerRendererMethod("demo.echo", (request) => {
5
5
  const value = request.input && typeof request.input.value === "string"
6
6
  ? request.input.value.trim()
7
7
  : "";
@@ -19,5 +19,8 @@ export function activate(context: PluginMainActivationContext): void {
19
19
  if (!registration.ok) {
20
20
  throw new Error(registration.error.message);
21
21
  }
22
- context.host.log("info", "__PLUGIN_ID__ activated.");
22
+ const logResult = await context.host.log("info", "__PLUGIN_ID__ activated.");
23
+ if (!logResult.ok) {
24
+ throw new Error(logResult.error.message);
25
+ }
23
26
  }
@@ -9,7 +9,7 @@
9
9
  "cwd": "${workspaceFolder}"
10
10
  },
11
11
  {
12
- "name": "Attach Nexus plugin main (9229)",
12
+ "name": "Attach Nexus plugin worker (9229)",
13
13
  "type": "node",
14
14
  "request": "attach",
15
15
  "address": "127.0.0.1",
@@ -17,18 +17,18 @@ The check follows the npm-configured registry, falls back to the official npm re
17
17
 
18
18
  Every `npm run build` rebuilds `better-sqlite3@8.7.0` for Electron 22.3.27 with `@electron/rebuild@3.7.2` before packaging. `npm run rebuild:native` remains available as a standalone diagnostic command.
19
19
 
20
- The deployable plugin package is `dist/__PLUGIN_ID__/`. Copy that directory to the Nexus executable's `Plugins/__PLUGIN_ID__/` directory, then restart Nexus. Runtime data is stored in the host-provided plugin data directory and is not written into the plugin package.
20
+ The deployable plugin package is `dist/__PLUGIN_ID__/`. For a manual installation, stop Nexus before replacing `Plugins/__PLUGIN_ID__/`, then start it again. During development, use `npm run dev:nexus` so the host coordinates worker shutdown, atomic replacement, and worker reactivation without changing the Nexus PID. Runtime data is stored in the host-provided plugin data directory and is not written into the plugin package.
21
21
 
22
22
  The renderer follows `context.language` and `context.themeMode` from Nexus, subscribes to in-place context updates without reloading the iframe, and includes `en-US` and `zh-CN` message catalogs. The plugin page has no language or theme selector; the controls shown by `npm run dev` belong to the debug host and simulate Nexus context changes. Static labels, CRUD status messages, action buttons, validation text, and date formatting switch together. Edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
23
23
 
24
24
  Use `npm run dev:web` for renderer-only layout, styling, localization, and responsive checks in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, and does not rebuild or load `better-sqlite3`, execute `mainEntry`, copy native runtime files, or overwrite the deployable `dist` package. It builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. Database operations report unsupported; use `npm run dev` for SQLite and Host API integration. Pass `-- --no-open` or `-- --port <port>` when needed.
25
25
 
26
- Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` to run the plugin inside a compiled, packaged Nexus. The initial deployment and every full package replacement are atomic. Renderer-only and safe manifest updates keep the Nexus PID; `mainEntry`, dependency, native-module, or restart-sensitive manifest changes run the complete build (including the Electron 22.3.27 native rebuild) before restarting Nexus. The database remains under the host-provided plugin data directory across restarts. Failed builds keep the previous deployment, and a failed restarted host rolls back the package. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
26
+ Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` to run the plugin inside a compiled, packaged Nexus. The initial deployment and every full package replacement are atomic. Renderer-only and safe manifest updates keep the Nexus PID; `mainEntry`, dependency, native-module, or worker-sensitive manifest changes run the complete build (including the Electron 22.3.27 native rebuild), stop the old plugin worker, replace the package, and start a new worker in the same Nexus process. The database remains under the host-provided plugin data directory across worker replacements. Failed builds keep the previous deployment, and a failed worker reload rolls the package back and restores the previous worker. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
27
27
 
28
28
  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.
29
29
 
30
30
  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.
31
31
 
32
- For main-process 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 main (9229)`. Renderer DevTools opens automatically, with `src/renderer/app.ts` available through the development source map. The default debug build also runs the Electron 22.3.27 native rebuild before launch. Use `npm run dev -- --no-build` only when intentionally reusing a compatible current `dist` output.
32
+ 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. The default debug build also runs the Electron 22.3.27 native rebuild before launch. Use `npm run dev -- --no-build` only when intentionally reusing a compatible current `dist` output.
33
33
 
34
34
  The independent host stores the SQLite database and settings under `.nexus-plugin-dev/data/` 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,6 +1,7 @@
1
1
  declare module "better-sqlite3" {
2
2
  class Database {
3
3
  constructor(filename: string);
4
+ close(): void;
4
5
  pragma(source: string): unknown;
5
6
  prepare(source: string): Database.Statement;
6
7
  }
@@ -19,7 +19,7 @@ let database: Database | undefined;
19
19
  let databasePath = "";
20
20
  let databaseCreated = false;
21
21
 
22
- export function activate(context: PluginMainActivationContext): void {
22
+ export async function activate(context: PluginMainActivationContext): Promise<void> {
23
23
  const fs = require("fs") as typeof import("fs");
24
24
  const path = require("path") as typeof import("path");
25
25
  fs.mkdirSync(context.host.pluginDataDirectory, { recursive: true });
@@ -36,16 +36,24 @@ export function activate(context: PluginMainActivationContext): void {
36
36
  ")"
37
37
  ).run();
38
38
 
39
- register(context, "sqlite.getInfo", getInfo);
40
- register(context, "sqlite.listRecords", listRecords);
41
- register(context, "sqlite.insertRecord", insertRecord);
42
- register(context, "sqlite.updateRecord", updateRecord);
43
- register(context, "sqlite.deleteRecord", deleteRecord);
44
- context.host.log("info", "__PLUGIN_ID__ database " + (databaseCreated ? "created" : "loaded") + ": " + databasePath);
39
+ await register(context, "sqlite.getInfo", getInfo);
40
+ await register(context, "sqlite.listRecords", listRecords);
41
+ await register(context, "sqlite.insertRecord", insertRecord);
42
+ await register(context, "sqlite.updateRecord", updateRecord);
43
+ await register(context, "sqlite.deleteRecord", deleteRecord);
44
+ const logResult = await context.host.log("info", "__PLUGIN_ID__ database " + (databaseCreated ? "created" : "loaded") + ": " + databasePath);
45
+ if (!logResult.ok) {
46
+ throw new Error(logResult.error.message);
47
+ }
48
+ }
49
+
50
+ export function deactivate(): void {
51
+ database?.close();
52
+ database = undefined;
45
53
  }
46
54
 
47
- function register(context: PluginMainActivationContext, name: string, handler: PluginRendererOperationHandler): void {
48
- const result = context.host.registerRendererMethod(name, handler);
55
+ async function register(context: PluginMainActivationContext, name: string, handler: PluginRendererOperationHandler): Promise<void> {
56
+ const result = await context.host.registerRendererMethod(name, handler);
49
57
  if (!result.ok) {
50
58
  throw new Error(result.error.message);
51
59
  }