@aiot-nexus/plugin-sdk 2.1.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -96
- package/dev-host/main.js +362 -504
- package/dist/.generated/plugin-framework-version.d.ts +1 -1
- package/dist/.generated/plugin-framework-version.js +1 -1
- package/dist/.generated/plugin-manifest.schema.json +1 -1
- package/dist/browser/frame-client.d.ts +1 -0
- package/dist/browser/frame-client.mjs +121 -13
- package/dist/src/browser/frame-client.d.ts +1 -0
- package/dist/src/browser/frame-client.js +121 -13
- package/dist/src/cli/run.js +2 -2
- package/dist/src/dev.js +3 -3
- package/dist/src/{electron-facade.js → electron-namespaces.js} +2 -3
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -2
- package/dist/src/nexus-dev-control.d.ts +1 -1
- package/dist/src/nexus-dev-control.js +2 -2
- package/dist/src/nexus-dev.d.ts +0 -3
- package/dist/src/nexus-dev.js +118 -53
- package/dist/src/types.d.ts +14 -98
- package/package.json +1 -1
- package/templates/minimal/.vscode/launch.json +1 -1
- package/templates/minimal/README.md.tmpl +15 -14
- package/templates/minimal/src/main.ts.tmpl +5 -4
- package/templates/sqlite/.vscode/launch.json +1 -1
- package/templates/sqlite/README.md.tmpl +12 -10
- package/templates/sqlite/src/main.ts.tmpl +24 -20
- package/dev-host/plugin-host.js +0 -4
- package/dist/internal/src-electron/main/core/runtime-result.js +0 -42
- package/dist/internal/src-electron/main/plugins/plugin-electron-facade-broker.js +0 -890
- package/dist/internal/src-electron/plugin-host-bootstrap.js +0 -417
- package/dist/internal/src-electron/shared/contracts.js +0 -2
- package/dist/internal/src-electron/shared/plugin-electron-facade-client.js +0 -181
- package/dist/internal/src-electron/shared/plugin-electron-facade-descriptors.js +0 -70
- package/dist/internal/src-electron/shared/plugin-electron-facade.js +0 -243
- package/dist/internal/src-electron/shared/plugin-rpc.js +0 -245
- /package/dist/src/{electron-facade.d.ts → electron-namespaces.d.ts} +0 -0
package/dist/src/nexus-dev.js
CHANGED
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.shouldSuppressNativeBuildEvent = exports.isRetryablePluginDeploymentError = exports.createNexusHostArguments = exports.mergePluginSourceChange = exports.manifestChangeRequiresPackageUpdate = exports.classifyPluginSourceChange = exports.
|
|
26
|
+
exports.shouldSuppressNativeBuildEvent = exports.isRetryablePluginDeploymentError = exports.createNexusHostArguments = exports.mergePluginSourceChange = exports.manifestChangeRequiresPackageUpdate = exports.classifyPluginSourceChange = exports.runPluginNexusDev = void 0;
|
|
27
27
|
const child_process_1 = require("child_process");
|
|
28
28
|
const fs = __importStar(require("fs"));
|
|
29
29
|
const path = __importStar(require("path"));
|
|
@@ -43,6 +43,7 @@ class PluginNexusDevSession {
|
|
|
43
43
|
this.deployedPluginDirectory = "";
|
|
44
44
|
this.expectedExits = new Set();
|
|
45
45
|
this.stopping = false;
|
|
46
|
+
this.restartingHost = false;
|
|
46
47
|
this.terminalSettled = false;
|
|
47
48
|
this.terminalPromise = new Promise((resolve) => {
|
|
48
49
|
this.terminalResolve = resolve;
|
|
@@ -54,9 +55,16 @@ class PluginNexusDevSession {
|
|
|
54
55
|
this.stopping = true;
|
|
55
56
|
this.watcher?.close();
|
|
56
57
|
void (async () => {
|
|
58
|
+
let exitCode = 0;
|
|
59
|
+
try {
|
|
60
|
+
await this.stopCurrentHost(true);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
exitCode = 1;
|
|
64
|
+
this.io.stderr(errorMessage(error));
|
|
65
|
+
}
|
|
57
66
|
await this.watcher?.drain();
|
|
58
|
-
|
|
59
|
-
this.settleTerminal(0);
|
|
67
|
+
this.settleTerminal(exitCode);
|
|
60
68
|
})();
|
|
61
69
|
};
|
|
62
70
|
this.projectDirectory = path.resolve(options.projectDirectory);
|
|
@@ -67,7 +75,6 @@ class PluginNexusDevSession {
|
|
|
67
75
|
(0, dev_1.buildPluginForDevelopment)(this.projectDirectory);
|
|
68
76
|
const target = (0, dev_1.preparePluginDebugTarget)({ ...this.options, projectDirectory: this.projectDirectory });
|
|
69
77
|
const deployment = (0, nexus_dev_deployment_1.beginPluginPackageDeployment)(target.packageDirectory, this.nexusExecutable, target.pluginId);
|
|
70
|
-
deployment.commit();
|
|
71
78
|
this.target = target;
|
|
72
79
|
this.deployedPluginDirectory = path.join(path.dirname(this.nexusExecutable), "Plugins", target.pluginId);
|
|
73
80
|
this.io.stdout("Deployed plugin package: " + this.deployedPluginDirectory
|
|
@@ -75,13 +82,14 @@ class PluginNexusDevSession {
|
|
|
75
82
|
try {
|
|
76
83
|
await this.control.start();
|
|
77
84
|
this.disposeDisconnectHandler = this.control.setDisconnectHandler((error) => {
|
|
78
|
-
if (!this.stopping) {
|
|
85
|
+
if (!this.stopping && !this.restartingHost) {
|
|
79
86
|
this.io.stderr("Packaged Nexus plugin development control disconnected: " + error.message);
|
|
80
87
|
this.settleTerminal(1);
|
|
81
88
|
}
|
|
82
89
|
});
|
|
83
90
|
this.installSignalHandlers();
|
|
84
91
|
await this.launchHost(target);
|
|
92
|
+
deployment.commit();
|
|
85
93
|
if (this.stopping) {
|
|
86
94
|
return await this.terminalPromise;
|
|
87
95
|
}
|
|
@@ -93,6 +101,26 @@ class PluginNexusDevSession {
|
|
|
93
101
|
this.io.stdout("Watching plugin source changes. Press Ctrl+C to stop.");
|
|
94
102
|
return await this.terminalPromise;
|
|
95
103
|
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
this.stopping = true;
|
|
106
|
+
const recoveryErrors = [];
|
|
107
|
+
try {
|
|
108
|
+
await this.stopCurrentHost(true);
|
|
109
|
+
}
|
|
110
|
+
catch (stopError) {
|
|
111
|
+
recoveryErrors.push("host shutdown failed: " + errorMessage(stopError));
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await retryFileOperation(() => deployment.rollback(), "roll back the initial plugin deployment");
|
|
115
|
+
}
|
|
116
|
+
catch (rollbackError) {
|
|
117
|
+
recoveryErrors.push("deployment rollback failed: " + errorMessage(rollbackError));
|
|
118
|
+
}
|
|
119
|
+
if (recoveryErrors.length > 0) {
|
|
120
|
+
throw new dev_1.PluginDevError(errorMessage(error) + " Initial deployment recovery failed: " + recoveryErrors.join(" "));
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
96
124
|
finally {
|
|
97
125
|
this.stopping = true;
|
|
98
126
|
this.watcher?.close();
|
|
@@ -106,7 +134,7 @@ class PluginNexusDevSession {
|
|
|
106
134
|
}
|
|
107
135
|
async updatePluginPackage() {
|
|
108
136
|
const previousTarget = this.requireTarget();
|
|
109
|
-
this.io.stdout("Building complete plugin package before restarting
|
|
137
|
+
this.io.stdout("Building complete plugin package before restarting Nexus...");
|
|
110
138
|
this.runBuild(() => (0, dev_1.buildPluginForDevelopment)(this.projectDirectory));
|
|
111
139
|
const updatedTarget = (0, dev_1.preparePluginDebugTarget)({
|
|
112
140
|
projectDirectory: this.projectDirectory,
|
|
@@ -119,18 +147,19 @@ class PluginNexusDevSession {
|
|
|
119
147
|
throw new dev_1.PluginDevError("Plugin id cannot change during a Nexus development session.");
|
|
120
148
|
}
|
|
121
149
|
let transaction;
|
|
122
|
-
|
|
150
|
+
this.restartingHost = true;
|
|
123
151
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
throw new dev_1.PluginDevError(prepareResult.message);
|
|
152
|
+
await this.stopCurrentHost(true);
|
|
153
|
+
if (this.stopping) {
|
|
154
|
+
return;
|
|
128
155
|
}
|
|
129
156
|
transaction = await retryFileOperation(() => (0, nexus_dev_deployment_1.beginPluginPackageDeployment)(updatedTarget.packageDirectory, this.nexusExecutable, updatedTarget.pluginId), "replace the plugin package");
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
157
|
+
if (this.stopping) {
|
|
158
|
+
await retryFileOperation(() => transaction?.rollback(), "roll back the plugin package after shutdown");
|
|
159
|
+
transaction = undefined;
|
|
160
|
+
return;
|
|
133
161
|
}
|
|
162
|
+
await this.launchHost(updatedTarget);
|
|
134
163
|
try {
|
|
135
164
|
await retryFileOperation(() => transaction?.commit(), "remove the previous plugin package backup");
|
|
136
165
|
}
|
|
@@ -138,18 +167,48 @@ class PluginNexusDevSession {
|
|
|
138
167
|
this.io.stderr("Updated plugin is active, but its previous package backup could not be removed: " + errorMessage(error));
|
|
139
168
|
}
|
|
140
169
|
this.target = updatedTarget;
|
|
141
|
-
this.io.stdout("Plugin package
|
|
170
|
+
this.io.stdout("Plugin package loaded after Nexus restart. New PID: " + String(this.child?.pid || "unknown"));
|
|
142
171
|
}
|
|
143
172
|
catch (error) {
|
|
144
|
-
|
|
173
|
+
const recoveryErrors = [];
|
|
174
|
+
try {
|
|
175
|
+
await this.stopCurrentHost(true);
|
|
176
|
+
}
|
|
177
|
+
catch (stopError) {
|
|
178
|
+
recoveryErrors.push("updated host shutdown failed: " + errorMessage(stopError));
|
|
179
|
+
}
|
|
180
|
+
if (transaction) {
|
|
145
181
|
try {
|
|
146
|
-
await
|
|
182
|
+
await retryFileOperation(() => transaction?.rollback(), "roll back the plugin package");
|
|
147
183
|
}
|
|
148
|
-
catch (
|
|
149
|
-
|
|
184
|
+
catch (rollbackError) {
|
|
185
|
+
recoveryErrors.push("package rollback failed: " + errorMessage(rollbackError));
|
|
150
186
|
}
|
|
151
187
|
}
|
|
152
|
-
|
|
188
|
+
if (this.stopping && recoveryErrors.length === 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!this.stopping && recoveryErrors.length === 0) {
|
|
192
|
+
try {
|
|
193
|
+
await this.launchHost(previousTarget);
|
|
194
|
+
}
|
|
195
|
+
catch (launchError) {
|
|
196
|
+
recoveryErrors.push("previous Nexus host restart failed: " + errorMessage(launchError));
|
|
197
|
+
try {
|
|
198
|
+
await this.stopCurrentHost(true);
|
|
199
|
+
}
|
|
200
|
+
catch (stopError) {
|
|
201
|
+
recoveryErrors.push("failed recovery host shutdown failed: " + errorMessage(stopError));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (recoveryErrors.length > 0) {
|
|
206
|
+
throw new dev_1.PluginDevError(errorMessage(error) + " Package update recovery failed: " + recoveryErrors.join(" "));
|
|
207
|
+
}
|
|
208
|
+
throw new dev_1.PluginDevError(errorMessage(error) + " The previous plugin package was restored and Nexus was restarted.");
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
this.restartingHost = false;
|
|
153
212
|
}
|
|
154
213
|
}
|
|
155
214
|
async launchHost(target) {
|
|
@@ -165,7 +224,12 @@ class PluginNexusDevSession {
|
|
|
165
224
|
const hostExit = waitForHostExit(child);
|
|
166
225
|
this.childExit = hostExit;
|
|
167
226
|
hostExit.then((result) => this.handleHostExit(child, result), (error) => {
|
|
168
|
-
if (
|
|
227
|
+
if (this.child === child) {
|
|
228
|
+
this.child = undefined;
|
|
229
|
+
this.childExit = undefined;
|
|
230
|
+
}
|
|
231
|
+
this.expectedExits.delete(child);
|
|
232
|
+
if (!this.stopping && !this.restartingHost) {
|
|
169
233
|
this.io.stderr(errorMessage(error));
|
|
170
234
|
this.settleTerminal(1);
|
|
171
235
|
}
|
|
@@ -180,7 +244,14 @@ class PluginNexusDevSession {
|
|
|
180
244
|
})
|
|
181
245
|
]);
|
|
182
246
|
if (this.stopping) {
|
|
183
|
-
|
|
247
|
+
throw new dev_1.PluginDevError("Packaged Nexus startup was cancelled because plugin development is stopping.");
|
|
248
|
+
}
|
|
249
|
+
if (child.exitCode !== null || this.child !== child) {
|
|
250
|
+
throw new dev_1.PluginDevError(formatEarlyHostExit({ status: child.exitCode, signal: child.signalCode }));
|
|
251
|
+
}
|
|
252
|
+
const readyResult = await this.control.sendUpdate("hostReady", target.pluginId);
|
|
253
|
+
if (!readyResult.ok) {
|
|
254
|
+
throw new dev_1.PluginDevError("Packaged Nexus started, but the target plugin is not ready: " + readyResult.message);
|
|
184
255
|
}
|
|
185
256
|
this.io.stdout("Packaged Nexus plugin development control connected. PID: " + String(child.pid || "unknown"));
|
|
186
257
|
return child;
|
|
@@ -190,7 +261,7 @@ class PluginNexusDevSession {
|
|
|
190
261
|
this.child = undefined;
|
|
191
262
|
this.childExit = undefined;
|
|
192
263
|
}
|
|
193
|
-
if (this.expectedExits.delete(child) || this.stopping) {
|
|
264
|
+
if (this.expectedExits.delete(child) || this.stopping || this.restartingHost) {
|
|
194
265
|
return;
|
|
195
266
|
}
|
|
196
267
|
const exitCode = result.status === null ? 1 : result.status;
|
|
@@ -199,25 +270,41 @@ class PluginNexusDevSession {
|
|
|
199
270
|
+ (result.signal ? " (" + result.signal + ")" : "") + ".");
|
|
200
271
|
this.settleTerminal(exitCode === 0 ? 1 : exitCode);
|
|
201
272
|
}
|
|
202
|
-
async stopCurrentHost() {
|
|
273
|
+
async stopCurrentHost(requireTermination = false) {
|
|
203
274
|
const child = this.child;
|
|
204
275
|
const hostExit = this.childExit;
|
|
205
276
|
if (!child || !hostExit) {
|
|
206
277
|
return;
|
|
207
278
|
}
|
|
208
279
|
this.expectedExits.add(child);
|
|
280
|
+
let gracefulFailure;
|
|
209
281
|
try {
|
|
210
282
|
await this.control.requestShutdown();
|
|
211
283
|
await promiseWithTimeout(hostExit, 5000, "Nexus did not exit during plugin development shutdown.");
|
|
212
284
|
}
|
|
213
|
-
catch {
|
|
285
|
+
catch (error) {
|
|
286
|
+
gracefulFailure = error;
|
|
214
287
|
child.kill();
|
|
215
288
|
try {
|
|
216
289
|
await promiseWithTimeout(hostExit, 3000, "Nexus did not terminate after plugin development shutdown.");
|
|
217
290
|
}
|
|
218
|
-
catch {
|
|
219
|
-
|
|
291
|
+
catch (terminationError) {
|
|
292
|
+
this.expectedExits.delete(child);
|
|
293
|
+
if (requireTermination) {
|
|
294
|
+
throw new dev_1.PluginDevError(errorMessage(terminationError) + " Graceful shutdown also failed: " + errorMessage(gracefulFailure));
|
|
295
|
+
}
|
|
296
|
+
this.io.stderr(errorMessage(terminationError));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
await this.control.waitForDisconnection(3000);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (requireTermination) {
|
|
305
|
+
throw error;
|
|
220
306
|
}
|
|
307
|
+
this.io.stderr(errorMessage(error));
|
|
221
308
|
}
|
|
222
309
|
}
|
|
223
310
|
runBuild(operation) {
|
|
@@ -246,30 +333,6 @@ class PluginNexusDevSession {
|
|
|
246
333
|
this.terminalResolve = undefined;
|
|
247
334
|
}
|
|
248
335
|
}
|
|
249
|
-
async function recoverPreparedPluginPackageUpdate(transaction, pluginId, control) {
|
|
250
|
-
const recoveryErrors = [];
|
|
251
|
-
if (transaction) {
|
|
252
|
-
try {
|
|
253
|
-
await retryFileOperation(() => transaction.rollback(), "roll back the plugin package");
|
|
254
|
-
}
|
|
255
|
-
catch (error) {
|
|
256
|
-
recoveryErrors.push("package rollback failed: " + errorMessage(error));
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
try {
|
|
260
|
-
const rollbackResult = await control.sendUpdate("packageUpdated", pluginId);
|
|
261
|
-
if (!rollbackResult.ok) {
|
|
262
|
-
recoveryErrors.push("previous worker activation failed: " + rollbackResult.message);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
catch (error) {
|
|
266
|
-
recoveryErrors.push("previous worker activation failed: " + errorMessage(error));
|
|
267
|
-
}
|
|
268
|
-
if (recoveryErrors.length > 0) {
|
|
269
|
-
throw new dev_1.PluginDevError(recoveryErrors.join(" "));
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
exports.recoverPreparedPluginPackageUpdate = recoverPreparedPluginPackageUpdate;
|
|
273
336
|
function classifyPluginSourceChange(projectDirectory, changedPath) {
|
|
274
337
|
const relativePath = path.relative(path.resolve(projectDirectory), path.resolve(changedPath)).replace(/\\/g, "/");
|
|
275
338
|
if (relativePath.startsWith("../") || relativePath === ".." || path.isAbsolute(relativePath)) {
|
|
@@ -296,6 +359,9 @@ function classifyPluginSourceChange(projectDirectory, changedPath) {
|
|
|
296
359
|
}
|
|
297
360
|
exports.classifyPluginSourceChange = classifyPluginSourceChange;
|
|
298
361
|
function manifestChangeRequiresPackageUpdate(previous, next) {
|
|
362
|
+
if (previous.mainEntry || next.mainEntry) {
|
|
363
|
+
return stableJson(previous) !== stableJson(next);
|
|
364
|
+
}
|
|
299
365
|
const packageSensitiveFields = [
|
|
300
366
|
"schemaVersion",
|
|
301
367
|
"pluginId",
|
|
@@ -375,8 +441,7 @@ async function finishUpdateTransaction(transaction, updateType, pluginId, contro
|
|
|
375
441
|
function createNexusHostArguments(control, target, options) {
|
|
376
442
|
const args = [];
|
|
377
443
|
if (options.inspectorMode) {
|
|
378
|
-
args.push("--
|
|
379
|
-
args.push("--plugin-dev-worker-plugin=" + target.pluginId);
|
|
444
|
+
args.push("--" + options.inspectorMode + "=" + String(options.inspectorPort || 9229));
|
|
380
445
|
}
|
|
381
446
|
args.push("--plugin-dev", "--plugin-dev-control=" + control.pipePath, "--plugin-dev-token=" + control.token, "--page=" + resolveContributionPage(target));
|
|
382
447
|
if (target.surface !== "business") {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
/// <reference types="node" />
|
|
3
1
|
export interface NexusError {
|
|
4
2
|
code: string;
|
|
5
3
|
message: string;
|
|
@@ -14,8 +12,8 @@ export type NexusResult<T> = {
|
|
|
14
12
|
};
|
|
15
13
|
export type ThemeMode = "light" | "dark" | "system";
|
|
16
14
|
export type LaunchPageParams = Record<string, string>;
|
|
17
|
-
export type PluginPermission = "businessLog" | "settings" | "filesystem" | "udp" | "serial" | "externalProcess" | "dialog"
|
|
18
|
-
export type PluginPermissionSupportState = "implemented" | "
|
|
15
|
+
export type PluginPermission = "businessLog" | "settings" | "filesystem" | "udp" | "serial" | "externalProcess" | "dialog";
|
|
16
|
+
export type PluginPermissionSupportState = "implemented" | "routeGuard" | "reserved";
|
|
19
17
|
export interface PluginPermissionSummary {
|
|
20
18
|
permission: PluginPermission;
|
|
21
19
|
supportState: PluginPermissionSupportState;
|
|
@@ -109,103 +107,22 @@ export type PluginRendererOperationHandler<TInput extends Record<string, unknown
|
|
|
109
107
|
export interface PluginUnsupportedCapabilityApi {
|
|
110
108
|
readonly permission: PluginPermission;
|
|
111
109
|
readonly supportState: PluginPermissionSupportState;
|
|
112
|
-
describe():
|
|
113
|
-
unsupported(operation?: unknown):
|
|
110
|
+
describe(): NexusResult<PluginPermissionSummary>;
|
|
111
|
+
unsupported(operation?: unknown): NexusResult<never>;
|
|
114
112
|
[operation: string]: unknown;
|
|
115
113
|
}
|
|
116
|
-
type ElectronExports = typeof import("electron");
|
|
117
|
-
type PluginElectronFacadeScalar = string | number | boolean | bigint | null | undefined | Date | Buffer | Error;
|
|
118
|
-
type PluginElectronFacadeRemoteObject = import("electron").BrowserView | import("electron").BrowserWindow | import("electron").ClientRequest | import("electron").CommandLine | import("electron").Cookies | import("electron").Debugger | import("electron").DownloadItem | import("electron").Dock | import("electron").Event | import("electron").IncomingMessage | import("electron").IpcMainEvent | import("electron").IpcMainInvokeEvent | import("electron").Menu | import("electron").MenuItem | import("electron").MessageChannelMain | import("electron").MessagePortMain | import("electron").NativeImage | import("electron").Notification | import("electron").Session | import("electron").ServiceWorkers | import("electron").ShareMenu | import("electron").TouchBar | import("electron").TouchBarButton | import("electron").TouchBarColorPicker | import("electron").TouchBarGroup | import("electron").TouchBarLabel | import("electron").TouchBarOtherItemsProxy | import("electron").TouchBarPopover | import("electron").TouchBarScrubber | import("electron").TouchBarSegmentedControl | import("electron").TouchBarSlider | import("electron").TouchBarSpacer | import("electron").Tray | import("electron").UtilityProcess | import("electron").WebContents | import("electron").WebFrameMain | import("electron").WebRequest;
|
|
119
|
-
type PluginElectronFacadeArguments<T extends readonly unknown[]> = {
|
|
120
|
-
[K in keyof T]: PluginElectronFacadeArgument<T[K]>;
|
|
121
|
-
};
|
|
122
|
-
type PluginElectronFacadeArgument<T> = T extends PluginElectronFacadeScalar ? T : T extends PluginElectronFacadeRemoteObject ? PluginElectronFacadeModule<T> : T extends (...args: infer TArgs) => infer TResult ? (...args: PluginElectronFacadeArguments<TArgs>) => PluginElectronFacadeValue<TResult> | Promise<PluginElectronFacadeValue<TResult>> : T extends readonly unknown[] ? PluginElectronFacadeArguments<T> : T extends object ? {
|
|
123
|
-
[K in keyof T]: PluginElectronFacadeArgument<T[K]>;
|
|
124
|
-
} : T;
|
|
125
|
-
type PluginElectronFacadeValue<T> = T extends PromiseLike<infer TValue> ? PluginElectronFacadeValue<TValue> : T extends PluginElectronFacadeScalar ? T : T extends PluginElectronFacadeRemoteObject ? PluginElectronFacadeModule<T> : T extends readonly unknown[] ? {
|
|
126
|
-
[K in keyof T]: PluginElectronFacadeValue<T[K]>;
|
|
127
|
-
} : T extends object ? {
|
|
128
|
-
readonly [K in keyof T]: PluginElectronFacadeValue<T[K]>;
|
|
129
|
-
} : T;
|
|
130
|
-
type PluginElectronFacadeAsyncMembers<T> = {
|
|
131
|
-
readonly [K in keyof T as T[K] extends (...args: never[]) => unknown ? K : never]: T[K] extends (...args: infer TArgs) => infer TResult ? (...args: PluginElectronFacadeArguments<TArgs>) => Promise<PluginElectronFacadeValue<TResult>> : never;
|
|
132
|
-
};
|
|
133
|
-
type PluginElectronFacadeConstructed<T> = T extends abstract new (...args: never[]) => infer TInstance ? TInstance : Record<string, unknown>;
|
|
134
|
-
type PluginElectronFacadeConstructorArguments<T> = T extends abstract new (...args: infer TArgs) => unknown ? PluginElectronFacadeArguments<TArgs> : unknown[];
|
|
135
|
-
export type PluginElectronFacadeMethod<TArgs extends readonly unknown[] = unknown[], TResult = unknown> = (...args: PluginElectronFacadeArguments<TArgs>) => Promise<PluginElectronFacadeValue<TResult>>;
|
|
136
|
-
export interface PluginElectronFacadeSubscription {
|
|
137
|
-
readonly $typeName?: "ElectronEventSubscription" | string;
|
|
138
|
-
$release(): Promise<void>;
|
|
139
|
-
}
|
|
140
|
-
export interface PluginElectronFacadeEventSnapshot {
|
|
141
|
-
readonly defaultPrevented: boolean;
|
|
142
|
-
readonly preventDefaultSupported: false;
|
|
143
|
-
readonly senderId?: number | string;
|
|
144
|
-
}
|
|
145
|
-
export type PluginElectronFacadeModule<TModule = Record<string, unknown>> = PluginElectronFacadeAsyncMembers<TModule> & {
|
|
146
|
-
readonly $typeName?: string;
|
|
147
|
-
$construct(...args: PluginElectronFacadeConstructorArguments<TModule>): Promise<PluginElectronFacadeModule<PluginElectronFacadeConstructed<TModule>>>;
|
|
148
|
-
$get<TKey extends keyof TModule>(member: TKey): Promise<PluginElectronFacadeValue<TModule[TKey]>>;
|
|
149
|
-
$get(member: string): Promise<unknown>;
|
|
150
|
-
$set<TKey extends keyof TModule>(member: TKey, value: PluginElectronFacadeArgument<TModule[TKey]>): Promise<void>;
|
|
151
|
-
$set(member: string, value: unknown): Promise<void>;
|
|
152
|
-
$on(event: string, listener: (...args: unknown[]) => unknown): Promise<PluginElectronFacadeSubscription>;
|
|
153
|
-
$once(event: string, listener: (...args: unknown[]) => unknown): Promise<PluginElectronFacadeSubscription>;
|
|
154
|
-
$release(): Promise<void>;
|
|
155
|
-
};
|
|
156
|
-
export interface PluginElectronFacade {
|
|
157
|
-
readonly app: PluginElectronFacadeModule<ElectronExports["app"]>;
|
|
158
|
-
readonly autoUpdater: PluginElectronFacadeModule<ElectronExports["autoUpdater"]>;
|
|
159
|
-
readonly BrowserView: PluginElectronFacadeModule<ElectronExports["BrowserView"]>;
|
|
160
|
-
readonly BrowserWindow: PluginElectronFacadeModule<ElectronExports["BrowserWindow"]>;
|
|
161
|
-
readonly clipboard: PluginElectronFacadeModule<ElectronExports["clipboard"]>;
|
|
162
|
-
readonly contentTracing: PluginElectronFacadeModule<ElectronExports["contentTracing"]>;
|
|
163
|
-
readonly contextBridge: PluginElectronFacadeModule<ElectronExports["contextBridge"]>;
|
|
164
|
-
readonly crashReporter: PluginElectronFacadeModule<ElectronExports["crashReporter"]>;
|
|
165
|
-
readonly desktopCapturer: PluginElectronFacadeModule<ElectronExports["desktopCapturer"]>;
|
|
166
|
-
readonly dialog: PluginElectronFacadeModule<ElectronExports["dialog"]>;
|
|
167
|
-
readonly globalShortcut: PluginElectronFacadeModule<ElectronExports["globalShortcut"]>;
|
|
168
|
-
readonly inAppPurchase: PluginElectronFacadeModule<ElectronExports["inAppPurchase"]>;
|
|
169
|
-
readonly ipcMain: PluginElectronFacadeModule<ElectronExports["ipcMain"]>;
|
|
170
|
-
readonly ipcRenderer: PluginElectronFacadeModule<ElectronExports["ipcRenderer"]>;
|
|
171
|
-
readonly Menu: PluginElectronFacadeModule<ElectronExports["Menu"]>;
|
|
172
|
-
readonly MenuItem: PluginElectronFacadeModule<ElectronExports["MenuItem"]>;
|
|
173
|
-
readonly MessageChannelMain: PluginElectronFacadeModule<ElectronExports["MessageChannelMain"]>;
|
|
174
|
-
readonly nativeImage: PluginElectronFacadeModule<ElectronExports["nativeImage"]>;
|
|
175
|
-
readonly nativeTheme: PluginElectronFacadeModule<ElectronExports["nativeTheme"]>;
|
|
176
|
-
readonly net: PluginElectronFacadeModule<ElectronExports["net"]>;
|
|
177
|
-
readonly netLog: PluginElectronFacadeModule<ElectronExports["netLog"]>;
|
|
178
|
-
readonly Notification: PluginElectronFacadeModule<ElectronExports["Notification"]>;
|
|
179
|
-
readonly powerMonitor: PluginElectronFacadeModule<ElectronExports["powerMonitor"]>;
|
|
180
|
-
readonly powerSaveBlocker: PluginElectronFacadeModule<ElectronExports["powerSaveBlocker"]>;
|
|
181
|
-
readonly protocol: PluginElectronFacadeModule<ElectronExports["protocol"]>;
|
|
182
|
-
readonly pushNotifications: PluginElectronFacadeModule<ElectronExports["pushNotifications"]>;
|
|
183
|
-
readonly safeStorage: PluginElectronFacadeModule<ElectronExports["safeStorage"]>;
|
|
184
|
-
readonly screen: PluginElectronFacadeModule<ElectronExports["screen"]>;
|
|
185
|
-
readonly session: PluginElectronFacadeModule<ElectronExports["session"]>;
|
|
186
|
-
readonly ShareMenu: PluginElectronFacadeModule<ElectronExports["ShareMenu"]>;
|
|
187
|
-
readonly shell: PluginElectronFacadeModule<ElectronExports["shell"]>;
|
|
188
|
-
readonly systemPreferences: PluginElectronFacadeModule<ElectronExports["systemPreferences"]>;
|
|
189
|
-
readonly TouchBar: PluginElectronFacadeModule<ElectronExports["TouchBar"]>;
|
|
190
|
-
readonly Tray: PluginElectronFacadeModule<ElectronExports["Tray"]>;
|
|
191
|
-
readonly utilityProcess: PluginElectronFacadeModule<ElectronExports["utilityProcess"]>;
|
|
192
|
-
readonly webContents: PluginElectronFacadeModule<ElectronExports["webContents"]>;
|
|
193
|
-
readonly webFrame: PluginElectronFacadeModule<ElectronExports["webFrame"]>;
|
|
194
|
-
readonly webFrameMain: PluginElectronFacadeModule<ElectronExports["webFrameMain"]>;
|
|
195
|
-
}
|
|
196
114
|
export interface PluginHostApi {
|
|
197
115
|
readonly hostVersion: string;
|
|
198
116
|
readonly hostApiVersion: string;
|
|
199
117
|
readonly pluginId: string;
|
|
200
118
|
readonly pluginDataDirectory: string;
|
|
201
119
|
hasPermission(permission: PluginPermission): boolean;
|
|
202
|
-
registerRendererMethod(name: string, handler: PluginRendererOperationHandler):
|
|
203
|
-
getCapabilityStatus(permission: unknown):
|
|
204
|
-
unsupportedCapability(permission: unknown, operation?: unknown):
|
|
205
|
-
log(level: PluginHostLogLevel, message: string, exception?: string):
|
|
206
|
-
getSettings():
|
|
207
|
-
updateSettings(values: Record<string, unknown>):
|
|
208
|
-
readonly electron: PluginElectronFacade;
|
|
120
|
+
registerRendererMethod(name: string, handler: PluginRendererOperationHandler): NexusResult<boolean>;
|
|
121
|
+
getCapabilityStatus(permission: unknown): NexusResult<PluginPermissionSummary>;
|
|
122
|
+
unsupportedCapability(permission: unknown, operation?: unknown): NexusResult<never>;
|
|
123
|
+
log(level: PluginHostLogLevel, message: string, exception?: string): NexusResult<BusinessLogEntry>;
|
|
124
|
+
getSettings(): NexusResult<PluginSettingsDocument>;
|
|
125
|
+
updateSettings(values: Record<string, unknown>): NexusResult<PluginSettingsDocument>;
|
|
209
126
|
filesystem: PluginUnsupportedCapabilityApi;
|
|
210
127
|
udp: PluginUnsupportedCapabilityApi;
|
|
211
128
|
serial: PluginUnsupportedCapabilityApi;
|
|
@@ -219,11 +136,10 @@ export interface PluginMainActivationContext {
|
|
|
219
136
|
readonly host: PluginHostApi;
|
|
220
137
|
}
|
|
221
138
|
export interface PluginMainModule {
|
|
222
|
-
activate?: (context: PluginMainActivationContext) =>
|
|
223
|
-
deactivate?: (reason:
|
|
139
|
+
activate?: (context: PluginMainActivationContext) => void;
|
|
140
|
+
deactivate?: (reason: "reload" | "disable" | "quit") => void;
|
|
224
141
|
default?: {
|
|
225
|
-
activate?: (context: PluginMainActivationContext) =>
|
|
226
|
-
deactivate?: (reason:
|
|
142
|
+
activate?: (context: PluginMainActivationContext) => void;
|
|
143
|
+
deactivate?: (reason: "reload" | "disable" | "quit") => void;
|
|
227
144
|
};
|
|
228
145
|
}
|
|
229
|
-
export {};
|
package/package.json
CHANGED
|
@@ -15,22 +15,23 @@ SDK checks for a newer `@aiot-nexus/plugin-sdk` in two steps. The package `posti
|
|
|
15
15
|
|
|
16
16
|
The check follows the npm-configured registry, falls back to the official npm registry when needed, and silently ignores offline, timeout, malformed-response, or corrupt-cache failures. Use `NEXUS_PLUGIN_SDK_UPDATE_CHECK=0` or `NO_UPDATE_NOTIFIER=1` to disable both checks. A major SDK update requires confirming Nexus Framework API compatibility; upgrades are never automatic. When a notice appears, review compatibility and run `npm install --save-dev @aiot-nexus/plugin-sdk@latest` manually.
|
|
17
17
|
|
|
18
|
-
The deployable plugin package is `dist/__PLUGIN_ID__/`. For a manual installation, stop Nexus before replacing `Plugins/__PLUGIN_ID__/`, then start it again. During development, use `npm run dev:nexus` so the
|
|
18
|
+
The deployable plugin package is `dist/__PLUGIN_ID__/`. For a manual installation, stop Nexus before replacing `Plugins/__PLUGIN_ID__/`, then start it again. During development, use `npm run dev:nexus` so the CLI coordinates Nexus shutdown, atomic replacement, and restart for trusted `mainEntry` changes.
|
|
19
19
|
|
|
20
|
-
## Electron
|
|
20
|
+
## Native Electron file dialogs
|
|
21
21
|
|
|
22
|
-
`mainEntry`
|
|
22
|
+
Framework API 3.0 loads trusted `mainEntry` code in the Electron main process. Import Electron 22 directly to open native file, folder, and save dialogs; no Facade or dialog permission is required:
|
|
23
23
|
|
|
24
24
|
```ts
|
|
25
25
|
import * as fs from "fs/promises";
|
|
26
|
+
import { dialog } from "electron";
|
|
26
27
|
import type { NexusResult, PluginMainActivationContext } from "@aiot-nexus/plugin-sdk";
|
|
27
28
|
|
|
28
29
|
const maximumTextFileBytes = 1024 * 1024;
|
|
29
30
|
|
|
30
31
|
export async function chooseAndReadText(
|
|
31
|
-
|
|
32
|
+
_context: PluginMainActivationContext
|
|
32
33
|
): Promise<NexusResult<string | undefined>> {
|
|
33
|
-
const result = await
|
|
34
|
+
const result = await dialog.showOpenDialog({
|
|
34
35
|
properties: ["openFile"],
|
|
35
36
|
filters: [{ name: "Text", extensions: ["txt", "md", "json"] }]
|
|
36
37
|
});
|
|
@@ -83,35 +84,35 @@ function failure(code: string, message: string): NexusResult<never> {
|
|
|
83
84
|
return { ok: false, error: { code, message } };
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
export async function chooseFolder(
|
|
87
|
-
const result = await
|
|
87
|
+
export async function chooseFolder(): Promise<string | undefined> {
|
|
88
|
+
const result = await dialog.showOpenDialog({
|
|
88
89
|
properties: ["openDirectory", "createDirectory"]
|
|
89
90
|
});
|
|
90
91
|
return result.canceled ? undefined : result.filePaths[0];
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
export async function chooseSavePath(
|
|
94
|
-
const result = await
|
|
94
|
+
export async function chooseSavePath(): Promise<string | undefined> {
|
|
95
|
+
const result = await dialog.showSaveDialog({ defaultPath: "document.txt" });
|
|
95
96
|
return result.canceled ? undefined : result.filePath;
|
|
96
97
|
}
|
|
97
98
|
```
|
|
98
99
|
|
|
99
|
-
Call these helpers from an operation registered with `context.host.registerRendererMethod`, then let the renderer invoke that narrow operation through `client.invokeMain()`. Return the `NexusResult` from `chooseAndReadText` directly so the renderer receives the stable `FileNotReadable`, `FileTooLarge`, or `FileReadFailed` code. Do not expose
|
|
100
|
+
Call these helpers from an operation registered with `context.host.registerRendererMethod`, then let the renderer invoke that narrow operation through `client.invokeMain()`. Return the `NexusResult` from `chooseAndReadText` directly so the renderer receives the stable `FileNotReadable`, `FileTooLarge`, or `FileReadFailed` code. Do not expose Electron objects or unrestricted file paths to the iframe. For larger files, stream or paginate in `mainEntry` and return only bounded chunks or summaries instead of the whole file. `npm run dev:web` is renderer-only and deliberately has no `mainEntry`, Node filesystem, or Electron main-process API; use `npm run dev` or a formal Nexus host to test these dialogs.
|
|
100
101
|
|
|
101
|
-
The renderer calls the
|
|
102
|
+
The renderer calls the trusted-main `demo.echo` operation through `@aiot-nexus/plugin-sdk`. The main entry also calls the directly imported `app.getName()` during activation, so `npm run dev` verifies the real Electron main-process path. Run `npm run validate` after changing the manifest or output layout.
|
|
102
103
|
|
|
103
104
|
The renderer follows `context.language` and `context.themeMode` from Nexus, subscribes to in-place context updates without reloading the iframe, and includes `en-US` and `zh-CN` message catalogs. The plugin page has no language or theme selector; the controls shown by `npm run dev` belong to the debug host and simulate Nexus context changes. Below that toolbar, the iframe has no host padding, border, minimum size, or content-driven height and fills the remaining host viewport. Add or edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
|
|
104
105
|
|
|
105
106
|
The current SDK has no generic business-event subscription API. For a long-running operation, register a fast `job.start` operation that returns a `taskId`, then expose `job.getStatus` and optional `job.cancel` for renderer polling. See the Nexus repository's plugin IPC and event notification example for the full contract and code.
|
|
106
107
|
|
|
107
|
-
Use `npm run dev:web` for renderer-only UI work in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. It does not execute `mainEntry`,
|
|
108
|
+
Use `npm run dev:web` for renderer-only UI work in the system browser. It runs `build:web`, writes an isolated package under `.nexus-plugin-dev/web-package/`, builds with source maps, serves only on `127.0.0.1:4173`, and opens the browser automatically. It does not execute `mainEntry`, provide Node or Electron main-process APIs, or overwrite the deployable `dist` package. Language and theme controls still simulate Nexus context, while `invokeMain` is intentionally unavailable. Pass `-- --no-open` or `-- --port <port>` when needed; use `npm run dev` for Host API and Electron integration.
|
|
108
109
|
|
|
109
|
-
Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` for live development against a compiled, packaged Nexus. The CLI atomically deploys this plugin to `<exeDir>/Plugins/__PLUGIN_ID__/`. Renderer changes rebuild and reload the iframe without changing the Nexus PID; safe manifest changes rescan contributions in the same process; `mainEntry`, dependency, native-module, or
|
|
110
|
+
Use `npm run dev:nexus -- --host C:\path\to\nexus.exe` for live development against a compiled, packaged Nexus. The CLI atomically deploys this plugin to `<exeDir>/Plugins/__PLUGIN_ID__/`. Renderer changes rebuild and reload the iframe without changing the Nexus PID; safe manifest changes rescan contributions in the same process; `mainEntry`, dependency, native-module, or restart-sensitive manifest changes stop Nexus, replace the complete package, and start a new Nexus process because trusted modules cannot be unloaded safely from Node's module cache. Failed builds keep the previous deployment; launch or readiness failures restore the previous package and host. Add `--no-devtools`, `--inspect[=<port>]`, `--inspect-brk[=<port>]`, `--surface`, or `--contribution-id` after the second `--` as needed.
|
|
110
111
|
|
|
111
112
|
When this project was created from a local SDK archive, `vendor/aiot-nexus-plugin-sdk.tgz` is part of the source project and must be kept when the project is moved or committed.
|
|
112
113
|
|
|
113
114
|
For a local SDK archive, the tgz version remains the plugin's current SDK version for comparison. It is not replaced automatically by a newer npm package; fully offline work can disable checks with the environment variables above.
|
|
114
115
|
|
|
115
|
-
For
|
|
116
|
+
For trusted-main breakpoints, open this plugin directory in VS Code, set breakpoints in `src/main.ts`, and run `Debug Nexus plugin`. To attach manually, start `npm run dev:debug` and select `Attach Nexus plugin main process (9229)`. Renderer DevTools opens automatically, with `src/renderer/app.ts` available through the development source map. `npm run dev` rebuilds before launch; use `npm run dev -- --no-build` only when intentionally reusing the current `dist` output.
|
|
116
117
|
|
|
117
118
|
The independent host stores data under `.nexus-plugin-dev/` and mirrors the PluginFrame protocol and current Host API surface. Its simulated Nexus application version defaults to the version recorded by the SDK build; pass `npm run dev -- --host-version <X.Y.Z>` to exercise another compatibility branch. Run the deployable `dist/__PLUGIN_ID__/` in Nexus before delivery because the standalone host does not reproduce the full application shell or routing state.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { app } from "electron";
|
|
1
2
|
import type { PluginMainActivationContext } from "@aiot-nexus/plugin-sdk";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
const applicationName =
|
|
5
|
-
const registration =
|
|
4
|
+
export function activate(context: PluginMainActivationContext): void {
|
|
5
|
+
const applicationName = app.getName();
|
|
6
|
+
const registration = context.host.registerRendererMethod("demo.echo", (request) => {
|
|
6
7
|
const value = request.input && typeof request.input.value === "string"
|
|
7
8
|
? request.input.value.trim()
|
|
8
9
|
: "";
|
|
@@ -20,7 +21,7 @@ export async function activate(context: PluginMainActivationContext): Promise<vo
|
|
|
20
21
|
if (!registration.ok) {
|
|
21
22
|
throw new Error(registration.error.message);
|
|
22
23
|
}
|
|
23
|
-
const logResult =
|
|
24
|
+
const logResult = context.host.log("info", "__PLUGIN_ID__ activated in " + applicationName + ".");
|
|
24
25
|
if (!logResult.ok) {
|
|
25
26
|
throw new Error(logResult.error.message);
|
|
26
27
|
}
|