@aiot-nexus/plugin-sdk 1.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.
- package/README.md +74 -0
- package/dev-host/host.html +344 -0
- package/dev-host/main.js +694 -0
- package/dev-host/preload.js +22 -0
- package/dist/.generated/plugin-framework-version.d.ts +11 -0
- package/dist/.generated/plugin-framework-version.js +39 -0
- package/dist/.generated/plugin-manifest.schema.json +106 -0
- package/dist/browser/frame-client.d.ts +26 -0
- package/dist/browser/frame-client.mjs +239 -0
- package/dist/src/browser/frame-client.d.ts +26 -0
- package/dist/src/browser/frame-client.js +244 -0
- package/dist/src/cli/run.d.ts +7 -0
- package/dist/src/cli/run.js +270 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +9 -0
- package/dist/src/dev.d.ts +36 -0
- package/dist/src/dev.js +224 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +27 -0
- package/dist/src/scaffold.d.ts +23 -0
- package/dist/src/scaffold.js +306 -0
- package/dist/src/types.d.ts +142 -0
- package/dist/src/types.js +2 -0
- package/dist/src/validation.d.ts +9 -0
- package/dist/src/validation.js +160 -0
- package/package.json +47 -0
- package/templates/minimal/.gitignore.tmpl +4 -0
- package/templates/minimal/.vscode/launch.json +22 -0
- package/templates/minimal/README.md.tmpl +22 -0
- package/templates/minimal/scripts/build.js.tmpl +83 -0
- package/templates/minimal/src/main.ts.tmpl +23 -0
- package/templates/minimal/src/renderer/app.ts.tmpl +194 -0
- package/templates/minimal/src/renderer/index.html +34 -0
- package/templates/minimal/src/renderer/plugin.css +164 -0
- package/templates/minimal/tsconfig.main.json +15 -0
- package/templates/minimal/tsconfig.renderer.json +17 -0
- package/templates/sqlite/.gitignore.tmpl +4 -0
- package/templates/sqlite/.vscode/launch.json +22 -0
- package/templates/sqlite/README.md.tmpl +22 -0
- package/templates/sqlite/scripts/build.js.tmpl +143 -0
- package/templates/sqlite/src/better-sqlite3.d.ts +22 -0
- package/templates/sqlite/src/main.ts.tmpl +190 -0
- package/templates/sqlite/src/renderer/app.ts.tmpl +428 -0
- package/templates/sqlite/src/renderer/index.html +57 -0
- package/templates/sqlite/src/renderer/plugin.css +275 -0
- package/templates/sqlite/tsconfig.main.json +15 -0
- package/templates/sqlite/tsconfig.renderer.json +17 -0
package/dev-host/main.js
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { app, BrowserWindow, ipcMain, protocol } = require("electron");
|
|
7
|
+
const sdk = require(path.join(__dirname, "..", "dist", "src", "index.js"));
|
|
8
|
+
|
|
9
|
+
const pluginRendererProtocol = "nexus-plugin";
|
|
10
|
+
const ipcChannels = {
|
|
11
|
+
getConfig: "nexus-plugin-dev:get-config",
|
|
12
|
+
invoke: "nexus-plugin-dev:invoke",
|
|
13
|
+
frameReady: "nexus-plugin-dev:frame-ready"
|
|
14
|
+
};
|
|
15
|
+
const pluginPermissions = ["businessLog", "settings", "filesystem", "udp", "serial", "externalProcess", "dialog"];
|
|
16
|
+
const implementedPermissions = new Set(["businessLog", "settings"]);
|
|
17
|
+
const routeGuardPermissions = new Set(["filesystem", "udp", "serial", "dialog"]);
|
|
18
|
+
const handlers = new Map();
|
|
19
|
+
|
|
20
|
+
protocol.registerSchemesAsPrivileged([
|
|
21
|
+
{
|
|
22
|
+
scheme: pluginRendererProtocol,
|
|
23
|
+
privileges: {
|
|
24
|
+
standard: true,
|
|
25
|
+
secure: true,
|
|
26
|
+
supportFetchAPI: true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
let configuration;
|
|
32
|
+
try {
|
|
33
|
+
configuration = loadConfiguration(process.argv.slice(1));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
process.stderr.write("[nexus-plugin-dev] " + errorMessage(error) + "\n");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fs.mkdirSync(configuration.devDataDirectory, { recursive: true });
|
|
40
|
+
fs.mkdirSync(configuration.pluginDataDirectory, { recursive: true });
|
|
41
|
+
app.setPath("userData", path.join(configuration.devDataDirectory, "electron-user-data"));
|
|
42
|
+
app.setName("Nexus Plugin Dev: " + configuration.manifest.pluginId);
|
|
43
|
+
|
|
44
|
+
let mainWindow;
|
|
45
|
+
let smokeTimeout;
|
|
46
|
+
|
|
47
|
+
app.whenReady().then(function startDevHost() {
|
|
48
|
+
registerPluginRendererProtocol();
|
|
49
|
+
activatePluginMain();
|
|
50
|
+
registerIpcHandlers();
|
|
51
|
+
createWindow();
|
|
52
|
+
}).catch(failStartup);
|
|
53
|
+
|
|
54
|
+
app.on("window-all-closed", function quitAfterWindowClose() {
|
|
55
|
+
app.quit();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function loadConfiguration(args) {
|
|
59
|
+
const packageDirectory = requiredPathArgument(args, "--plugin-package");
|
|
60
|
+
const projectDirectory = requiredPathArgument(args, "--project-directory");
|
|
61
|
+
const validation = sdk.validatePluginPackage(packageDirectory);
|
|
62
|
+
if (!validation.valid) {
|
|
63
|
+
throw new Error("Plugin package validation failed:\n" + validation.errors.map(function format(error) {
|
|
64
|
+
return " - " + error;
|
|
65
|
+
}).join("\n"));
|
|
66
|
+
}
|
|
67
|
+
const manifest = readJson(path.join(packageDirectory, "manifest.json"));
|
|
68
|
+
const surface = requiredArgument(args, "--surface");
|
|
69
|
+
const contributionId = requiredArgument(args, "--contribution-id");
|
|
70
|
+
const language = requiredArgument(args, "--language");
|
|
71
|
+
const themeMode = requiredArgument(args, "--theme");
|
|
72
|
+
if (!hasContribution(manifest, surface, contributionId)) {
|
|
73
|
+
throw new Error("Selected PluginFrame contribution is not declared: " + surface + ":" + contributionId);
|
|
74
|
+
}
|
|
75
|
+
if (!manifest.rendererEntry) {
|
|
76
|
+
throw new Error("Plugin rendererEntry is required for independent debugging.");
|
|
77
|
+
}
|
|
78
|
+
const devDataDirectory = path.join(projectDirectory, ".nexus-plugin-dev");
|
|
79
|
+
return {
|
|
80
|
+
packageDirectory,
|
|
81
|
+
projectDirectory,
|
|
82
|
+
manifest,
|
|
83
|
+
surface,
|
|
84
|
+
contributionId,
|
|
85
|
+
language,
|
|
86
|
+
themeMode,
|
|
87
|
+
openDevTools: !args.includes("--no-devtools"),
|
|
88
|
+
smoke: args.includes("--smoke"),
|
|
89
|
+
devDataDirectory,
|
|
90
|
+
pluginDataDirectory: path.join(devDataDirectory, "data")
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function registerPluginRendererProtocol() {
|
|
95
|
+
const registered = protocol.registerFileProtocol(pluginRendererProtocol, function resolvePluginAsset(request, callback) {
|
|
96
|
+
try {
|
|
97
|
+
const result = resolvePluginRendererFile(request.url);
|
|
98
|
+
if (!result.ok) {
|
|
99
|
+
callback({ error: -6 });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
callback(result.value);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
process.stderr.write("[nexus-plugin-dev] renderer asset failed: " + errorMessage(error) + "\n");
|
|
105
|
+
callback({ error: -6 });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
if (!registered) {
|
|
109
|
+
throw new Error("Failed to register the nexus-plugin renderer protocol.");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolvePluginRendererFile(url) {
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = new URL(url);
|
|
117
|
+
} catch {
|
|
118
|
+
return fail("InvalidRequest", "Plugin renderer URL is invalid.");
|
|
119
|
+
}
|
|
120
|
+
if (parsed.protocol !== pluginRendererProtocol + ":" || parsed.hostname !== configuration.manifest.pluginId) {
|
|
121
|
+
return fail("InvalidRequest", "Plugin renderer URL identity is invalid.");
|
|
122
|
+
}
|
|
123
|
+
const entryPath = path.resolve(configuration.packageDirectory, configuration.manifest.rendererEntry);
|
|
124
|
+
const rendererRoot = path.dirname(entryPath);
|
|
125
|
+
let requestedRelativePath;
|
|
126
|
+
try {
|
|
127
|
+
requestedRelativePath = decodeURIComponent(parsed.pathname.replace(/^\/+/, "")) || configuration.manifest.rendererEntry;
|
|
128
|
+
} catch {
|
|
129
|
+
return fail("InvalidRequest", "Plugin renderer asset path is invalid.");
|
|
130
|
+
}
|
|
131
|
+
const requestedPath = path.resolve(configuration.packageDirectory, requestedRelativePath);
|
|
132
|
+
if (!isPathInside(rendererRoot, requestedPath)) {
|
|
133
|
+
return fail("PluginBlocked", "Plugin renderer asset resolves outside renderer directory.");
|
|
134
|
+
}
|
|
135
|
+
if (!fs.existsSync(requestedPath) || !fs.statSync(requestedPath).isFile() || fs.lstatSync(requestedPath).isSymbolicLink()) {
|
|
136
|
+
return fail("NotFound", "Plugin renderer asset was not found.");
|
|
137
|
+
}
|
|
138
|
+
const realRoot = fs.realpathSync(rendererRoot);
|
|
139
|
+
const realPath = fs.realpathSync(requestedPath);
|
|
140
|
+
if (!isPathInside(realRoot, realPath)) {
|
|
141
|
+
return fail("PluginBlocked", "Plugin renderer asset resolves outside renderer directory.");
|
|
142
|
+
}
|
|
143
|
+
return ok({
|
|
144
|
+
path: realPath,
|
|
145
|
+
mimeType: mimeType(realPath),
|
|
146
|
+
headers: {
|
|
147
|
+
"Content-Security-Policy": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'",
|
|
148
|
+
"X-Content-Type-Options": "nosniff"
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function activatePluginMain() {
|
|
154
|
+
if (!configuration.manifest.mainEntry) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const mainEntryPath = path.resolve(configuration.packageDirectory, configuration.manifest.mainEntry);
|
|
158
|
+
const pluginModule = require(mainEntryPath);
|
|
159
|
+
const activate = typeof pluginModule.activate === "function"
|
|
160
|
+
? pluginModule.activate
|
|
161
|
+
: pluginModule.default && typeof pluginModule.default.activate === "function"
|
|
162
|
+
? pluginModule.default.activate
|
|
163
|
+
: undefined;
|
|
164
|
+
if (activate) {
|
|
165
|
+
activate({
|
|
166
|
+
pluginId: configuration.manifest.pluginId,
|
|
167
|
+
pluginDirectory: configuration.packageDirectory,
|
|
168
|
+
manifest: configuration.manifest,
|
|
169
|
+
host: createHostApi()
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
process.stdout.write(
|
|
173
|
+
"[nexus-plugin-dev] activated " + configuration.manifest.pluginId
|
|
174
|
+
+ " with " + String(handlers.size) + " renderer operation(s).\n"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function createHostApi() {
|
|
179
|
+
const manifest = configuration.manifest;
|
|
180
|
+
return {
|
|
181
|
+
hostVersion: manifest.minHostVersion || "0.0.0",
|
|
182
|
+
hostApiVersion: sdk.PLUGIN_FRAMEWORK_VERSION,
|
|
183
|
+
pluginId: manifest.pluginId,
|
|
184
|
+
pluginDataDirectory: configuration.pluginDataDirectory,
|
|
185
|
+
hasPermission: function hasPermission(permission) {
|
|
186
|
+
return manifest.permissions.includes(permission);
|
|
187
|
+
},
|
|
188
|
+
registerRendererMethod: function registerRendererMethod(name, handler) {
|
|
189
|
+
if (!isSafeOperationName(name)) {
|
|
190
|
+
return fail("InvalidRequest", "Plugin renderer operation name is invalid.", { operation: name });
|
|
191
|
+
}
|
|
192
|
+
if (typeof handler !== "function") {
|
|
193
|
+
return fail("InvalidRequest", "Plugin renderer operation handler must be a function.", { operation: name });
|
|
194
|
+
}
|
|
195
|
+
if (handlers.has(name)) {
|
|
196
|
+
return fail("InvalidRequest", "Plugin renderer operation is already registered.", { operation: name });
|
|
197
|
+
}
|
|
198
|
+
handlers.set(name, handler);
|
|
199
|
+
return ok(true);
|
|
200
|
+
},
|
|
201
|
+
getCapabilityStatus: function getCapabilityStatus(permission) {
|
|
202
|
+
return isPluginPermission(permission)
|
|
203
|
+
? ok(createPermissionSummary(permission))
|
|
204
|
+
: fail("InvalidRequest", "Plugin capability name is invalid.", { permission });
|
|
205
|
+
},
|
|
206
|
+
unsupportedCapability: function unsupportedCapability(permission, operation) {
|
|
207
|
+
return unsupportedCapability(manifest, permission, operation);
|
|
208
|
+
},
|
|
209
|
+
log: function log(level, message, exception) {
|
|
210
|
+
if (!manifest.permissions.includes("businessLog")) {
|
|
211
|
+
return fail("PluginPermissionDenied", "Plugin does not declare businessLog permission.");
|
|
212
|
+
}
|
|
213
|
+
return ok(appendLog(level, "plugin:" + manifest.pluginId, message, exception));
|
|
214
|
+
},
|
|
215
|
+
getSettings: function getSettings() {
|
|
216
|
+
return manifest.permissions.includes("settings")
|
|
217
|
+
? ok(loadSettings())
|
|
218
|
+
: fail("PluginPermissionDenied", "Plugin does not declare settings permission.");
|
|
219
|
+
},
|
|
220
|
+
updateSettings: function updateSettings(values) {
|
|
221
|
+
if (!manifest.permissions.includes("settings")) {
|
|
222
|
+
return fail("PluginPermissionDenied", "Plugin does not declare settings permission.");
|
|
223
|
+
}
|
|
224
|
+
if (!isRecord(values)) {
|
|
225
|
+
return fail("InvalidRequest", "Plugin settings values must be an object.");
|
|
226
|
+
}
|
|
227
|
+
return ok(saveSettings(values));
|
|
228
|
+
},
|
|
229
|
+
filesystem: createUnsupportedCapabilityApi(manifest, "filesystem"),
|
|
230
|
+
udp: createUnsupportedCapabilityApi(manifest, "udp"),
|
|
231
|
+
serial: createUnsupportedCapabilityApi(manifest, "serial"),
|
|
232
|
+
dialog: createUnsupportedCapabilityApi(manifest, "dialog"),
|
|
233
|
+
externalProcess: createUnsupportedCapabilityApi(manifest, "externalProcess")
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function createUnsupportedCapabilityApi(manifest, permission) {
|
|
238
|
+
const target = {
|
|
239
|
+
permission,
|
|
240
|
+
supportState: permissionSupportState(permission),
|
|
241
|
+
describe: function describe() {
|
|
242
|
+
return ok(createPermissionSummary(permission));
|
|
243
|
+
},
|
|
244
|
+
unsupported: function unsupported(operation) {
|
|
245
|
+
return unsupportedCapability(manifest, permission, operation);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
return new Proxy(target, {
|
|
249
|
+
get: function get(apiTarget, property) {
|
|
250
|
+
if (property in apiTarget) {
|
|
251
|
+
return Reflect.get(apiTarget, property);
|
|
252
|
+
}
|
|
253
|
+
if (typeof property === "symbol" || property === "then") {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
return function unsupportedOperation() {
|
|
257
|
+
return unsupportedCapability(manifest, permission, property);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function unsupportedCapability(manifest, permission, operation) {
|
|
264
|
+
if (!isPluginPermission(permission)) {
|
|
265
|
+
return fail("InvalidRequest", "Plugin capability name is invalid.", { permission });
|
|
266
|
+
}
|
|
267
|
+
if (!manifest.permissions.includes(permission)) {
|
|
268
|
+
return fail("PluginPermissionDenied", "Plugin does not declare " + permission + " permission.", { permission });
|
|
269
|
+
}
|
|
270
|
+
const summary = createPermissionSummary(permission);
|
|
271
|
+
return fail(
|
|
272
|
+
"PluginCapabilityUnsupported",
|
|
273
|
+
"Plugin Host API capability is not available to trusted mainEntry.",
|
|
274
|
+
{
|
|
275
|
+
permission,
|
|
276
|
+
operation: typeof operation === "string" ? operation : undefined,
|
|
277
|
+
supportState: summary.supportState,
|
|
278
|
+
statusMessage: summary.statusMessage
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function registerIpcHandlers() {
|
|
284
|
+
ipcMain.handle(ipcChannels.getConfig, function getConfig(event) {
|
|
285
|
+
assertMainWindowSender(event);
|
|
286
|
+
return createRendererConfiguration();
|
|
287
|
+
});
|
|
288
|
+
ipcMain.handle(ipcChannels.invoke, function invoke(event, request) {
|
|
289
|
+
assertMainWindowSender(event);
|
|
290
|
+
return invokeRendererRequest(request);
|
|
291
|
+
});
|
|
292
|
+
ipcMain.on(ipcChannels.frameReady, function frameReady(event) {
|
|
293
|
+
assertMainWindowSender(event);
|
|
294
|
+
if (configuration.smoke) {
|
|
295
|
+
clearTimeout(smokeTimeout);
|
|
296
|
+
process.stdout.write("[nexus-plugin-dev] renderer initialization smoke passed.\n");
|
|
297
|
+
setTimeout(function exitAfterSmokeSettles() {
|
|
298
|
+
app.exit(0);
|
|
299
|
+
}, 100);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function createRendererConfiguration() {
|
|
305
|
+
const manifest = configuration.manifest;
|
|
306
|
+
const frameOrigin = pluginRendererProtocol + "://" + manifest.pluginId;
|
|
307
|
+
const encodedEntry = manifest.rendererEntry.split("/").map(encodeURIComponent).join("/");
|
|
308
|
+
const query = new URLSearchParams({
|
|
309
|
+
surface: configuration.surface,
|
|
310
|
+
contributionId: configuration.contributionId
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
pluginId: manifest.pluginId,
|
|
314
|
+
frameOrigin,
|
|
315
|
+
frameUrl: frameOrigin + "/" + encodedEntry + "?" + query.toString(),
|
|
316
|
+
supportedCultures: manifest.supportedCultures,
|
|
317
|
+
smoke: configuration.smoke,
|
|
318
|
+
context: {
|
|
319
|
+
schemaVersion: sdk.PLUGIN_MANIFEST_SCHEMA_VERSION,
|
|
320
|
+
pluginId: manifest.pluginId,
|
|
321
|
+
surface: configuration.surface,
|
|
322
|
+
contributionId: configuration.contributionId,
|
|
323
|
+
language: configuration.language,
|
|
324
|
+
themeMode: configuration.themeMode,
|
|
325
|
+
hostApiVersion: sdk.PLUGIN_FRAMEWORK_VERSION
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function invokeRendererRequest(request) {
|
|
331
|
+
if (!isRecord(request) || typeof request.method !== "string") {
|
|
332
|
+
return fail("InvalidRequest", "Plugin renderer request is invalid.");
|
|
333
|
+
}
|
|
334
|
+
const params = isRecord(request.params) ? request.params : undefined;
|
|
335
|
+
if (request.method === "getSettings") {
|
|
336
|
+
return configuration.manifest.permissions.includes("settings")
|
|
337
|
+
? ok(loadSettings())
|
|
338
|
+
: fail("PluginPermissionDenied", "Plugin does not declare settings permission.");
|
|
339
|
+
}
|
|
340
|
+
if (request.method === "updateSettings") {
|
|
341
|
+
if (!configuration.manifest.permissions.includes("settings")) {
|
|
342
|
+
return fail("PluginPermissionDenied", "Plugin does not declare settings permission.");
|
|
343
|
+
}
|
|
344
|
+
if (!params || !isRecord(params.values)) {
|
|
345
|
+
return fail("InvalidRequest", "Plugin renderer settings update requires values.");
|
|
346
|
+
}
|
|
347
|
+
return ok(saveSettings(params.values));
|
|
348
|
+
}
|
|
349
|
+
if (request.method === "appendLog") {
|
|
350
|
+
if (!configuration.manifest.permissions.includes("businessLog")) {
|
|
351
|
+
return fail("PluginPermissionDenied", "Plugin does not declare businessLog permission.");
|
|
352
|
+
}
|
|
353
|
+
if (!params || !isLogLevel(params.level) || typeof params.message !== "string" || !params.message.trim()) {
|
|
354
|
+
return fail("InvalidRequest", "Plugin renderer log request is invalid.");
|
|
355
|
+
}
|
|
356
|
+
const category = typeof params.category === "string" && params.category.trim()
|
|
357
|
+
? ":" + params.category.trim()
|
|
358
|
+
: "";
|
|
359
|
+
const exception = typeof params.exception === "string" && params.exception.trim()
|
|
360
|
+
? params.exception.trim()
|
|
361
|
+
: undefined;
|
|
362
|
+
return ok(appendLog(params.level, "plugin:" + configuration.manifest.pluginId + category, params.message.trim(), exception));
|
|
363
|
+
}
|
|
364
|
+
if (request.method === "invokeMain") {
|
|
365
|
+
return invokeMain(params);
|
|
366
|
+
}
|
|
367
|
+
return fail("InvalidRequest", "Plugin renderer request method is invalid.", { method: request.method });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function invokeMain(params) {
|
|
371
|
+
if (!params || typeof params.operation !== "string") {
|
|
372
|
+
return fail("InvalidRequest", "Plugin renderer invokeMain request requires an operation name.");
|
|
373
|
+
}
|
|
374
|
+
if (params.input !== undefined && !isRecord(params.input)) {
|
|
375
|
+
return fail("InvalidRequest", "Plugin renderer invokeMain input must be an object.", { operation: params.operation });
|
|
376
|
+
}
|
|
377
|
+
if (!isSafeOperationName(params.operation)) {
|
|
378
|
+
return fail("InvalidRequest", "Plugin renderer operation name is invalid.", { operation: params.operation });
|
|
379
|
+
}
|
|
380
|
+
const handler = handlers.get(params.operation);
|
|
381
|
+
if (!handler) {
|
|
382
|
+
return fail("NotFound", "Plugin renderer operation was not registered.", { operation: params.operation });
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const value = handler({
|
|
386
|
+
pluginId: configuration.manifest.pluginId,
|
|
387
|
+
surface: configuration.surface,
|
|
388
|
+
contributionId: configuration.contributionId,
|
|
389
|
+
operation: params.operation,
|
|
390
|
+
input: isRecord(params.input) ? params.input : undefined
|
|
391
|
+
});
|
|
392
|
+
return normalizeRendererOperationResult(value);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
process.stderr.write("[nexus-plugin-dev] operation failed: " + params.operation + ": " + errorMessage(error) + "\n");
|
|
395
|
+
return fail("UnexpectedFailure", "Plugin renderer operation failed.", {
|
|
396
|
+
operation: params.operation,
|
|
397
|
+
message: errorMessage(error)
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function normalizeRendererOperationResult(value) {
|
|
403
|
+
if (isNexusResult(value)) {
|
|
404
|
+
if (value.ok) {
|
|
405
|
+
return ok(sanitizeJsonValue(value.value));
|
|
406
|
+
}
|
|
407
|
+
return fail(
|
|
408
|
+
value.error.code,
|
|
409
|
+
value.error.message,
|
|
410
|
+
value.error.details === undefined ? undefined : sanitizeJsonValue(value.error.details)
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return ok(sanitizeJsonValue(value));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function createWindow() {
|
|
417
|
+
mainWindow = new BrowserWindow({
|
|
418
|
+
width: 1200,
|
|
419
|
+
height: 800,
|
|
420
|
+
minWidth: 640,
|
|
421
|
+
minHeight: 480,
|
|
422
|
+
show: !configuration.smoke,
|
|
423
|
+
backgroundColor: configuration.themeMode === "dark" ? "#181a1d" : "#f4f5f7",
|
|
424
|
+
webPreferences: {
|
|
425
|
+
preload: path.join(__dirname, "preload.js"),
|
|
426
|
+
contextIsolation: true,
|
|
427
|
+
nodeIntegration: false,
|
|
428
|
+
sandbox: true,
|
|
429
|
+
webSecurity: true
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
mainWindow.webContents.setWindowOpenHandler(function denyWindowOpen() {
|
|
433
|
+
return { action: "deny" };
|
|
434
|
+
});
|
|
435
|
+
mainWindow.webContents.on("did-fail-load", function reportLoadFailure(_event, errorCode, errorDescription, validatedUrl, isMainFrame) {
|
|
436
|
+
process.stderr.write(
|
|
437
|
+
"[nexus-plugin-dev] load failed: " + String(errorCode) + " " + errorDescription + " " + validatedUrl + "\n"
|
|
438
|
+
);
|
|
439
|
+
if (configuration.smoke && isMainFrame) {
|
|
440
|
+
app.exit(1);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
mainWindow.webContents.on("render-process-gone", function reportRendererExit(_event, details) {
|
|
444
|
+
process.stderr.write("[nexus-plugin-dev] renderer process exited: " + details.reason + "\n");
|
|
445
|
+
if (configuration.smoke) {
|
|
446
|
+
app.exit(1);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
mainWindow.on("closed", function clearWindow() {
|
|
450
|
+
mainWindow = undefined;
|
|
451
|
+
});
|
|
452
|
+
void mainWindow.loadFile(path.join(__dirname, "host.html"));
|
|
453
|
+
if (configuration.openDevTools && !configuration.smoke) {
|
|
454
|
+
mainWindow.webContents.once("did-finish-load", function openDevTools() {
|
|
455
|
+
mainWindow && mainWindow.webContents.openDevTools({ mode: "detach" });
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (configuration.smoke) {
|
|
459
|
+
smokeTimeout = setTimeout(function failSmokeTimeout() {
|
|
460
|
+
process.stderr.write("[nexus-plugin-dev] renderer initialization smoke timed out.\n");
|
|
461
|
+
app.exit(1);
|
|
462
|
+
}, 15000);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function assertMainWindowSender(event) {
|
|
467
|
+
if (!mainWindow || event.sender !== mainWindow.webContents) {
|
|
468
|
+
throw new Error("Rejected IPC request from an unexpected renderer.");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function loadSettings() {
|
|
473
|
+
const filePath = settingsFilePath();
|
|
474
|
+
if (!fs.existsSync(filePath)) {
|
|
475
|
+
return emptySettings();
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const value = readJson(filePath);
|
|
479
|
+
return {
|
|
480
|
+
schemaVersion: sdk.PLUGIN_MANIFEST_SCHEMA_VERSION,
|
|
481
|
+
pluginId: configuration.manifest.pluginId,
|
|
482
|
+
values: isRecord(value.values) ? sanitizeJsonObject(value.values) : {}
|
|
483
|
+
};
|
|
484
|
+
} catch (error) {
|
|
485
|
+
process.stderr.write("[nexus-plugin-dev] settings reset after read failure: " + errorMessage(error) + "\n");
|
|
486
|
+
return emptySettings();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function saveSettings(values) {
|
|
491
|
+
const existing = loadSettings();
|
|
492
|
+
const document = {
|
|
493
|
+
schemaVersion: sdk.PLUGIN_MANIFEST_SCHEMA_VERSION,
|
|
494
|
+
pluginId: configuration.manifest.pluginId,
|
|
495
|
+
values: Object.assign({}, existing.values, sanitizeJsonObject(values))
|
|
496
|
+
};
|
|
497
|
+
const filePath = settingsFilePath();
|
|
498
|
+
const temporaryPath = filePath + ".tmp";
|
|
499
|
+
fs.writeFileSync(temporaryPath, JSON.stringify(document, null, 2) + "\n", "utf8");
|
|
500
|
+
fs.renameSync(temporaryPath, filePath);
|
|
501
|
+
return document;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function settingsFilePath() {
|
|
505
|
+
fs.mkdirSync(configuration.pluginDataDirectory, { recursive: true });
|
|
506
|
+
return path.join(configuration.pluginDataDirectory, "settings.json");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function emptySettings() {
|
|
510
|
+
return {
|
|
511
|
+
schemaVersion: sdk.PLUGIN_MANIFEST_SCHEMA_VERSION,
|
|
512
|
+
pluginId: configuration.manifest.pluginId,
|
|
513
|
+
values: {}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function appendLog(level, category, message, exception) {
|
|
518
|
+
const entry = {
|
|
519
|
+
timestamp: new Date().toISOString(),
|
|
520
|
+
level,
|
|
521
|
+
category,
|
|
522
|
+
message,
|
|
523
|
+
exception
|
|
524
|
+
};
|
|
525
|
+
const output = "[nexus-plugin-dev][" + String(level) + "][" + category + "] " + String(message)
|
|
526
|
+
+ (exception ? "\n" + exception : "") + "\n";
|
|
527
|
+
if (level === "error" || level === "warn") {
|
|
528
|
+
process.stderr.write(output);
|
|
529
|
+
} else {
|
|
530
|
+
process.stdout.write(output);
|
|
531
|
+
}
|
|
532
|
+
return entry;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function hasContribution(manifest, surface, contributionId) {
|
|
536
|
+
if (surface === "business") {
|
|
537
|
+
return (manifest.businessEntries || []).some(function matchesBusiness(entry) {
|
|
538
|
+
return entry.component === "PluginFrame" && entry.businessId === contributionId;
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (surface === "tool") {
|
|
542
|
+
return (manifest.tools || []).some(function matchesTool(entry) {
|
|
543
|
+
return entry.component === "PluginFrame" && entry.toolId === contributionId;
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
if (surface === "settings") {
|
|
547
|
+
return (manifest.settingsSections || []).some(function matchesSettings(entry) {
|
|
548
|
+
return entry.component === "PluginFrame" && entry.sectionId === contributionId;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function isSafeOperationName(value) {
|
|
555
|
+
return typeof value === "string"
|
|
556
|
+
&& value.length <= 80
|
|
557
|
+
&& /^[a-z][A-Za-z0-9]*(?:\.[a-z][A-Za-z0-9]*){1,4}$/.test(value)
|
|
558
|
+
&& value.split(".").every(function isSafePart(part) {
|
|
559
|
+
return part !== "constructor" && part !== "prototype" && part !== "__proto__";
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function isNexusResult(value) {
|
|
564
|
+
if (!isRecord(value) || typeof value.ok !== "boolean") {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
return value.ok
|
|
568
|
+
? Object.prototype.hasOwnProperty.call(value, "value")
|
|
569
|
+
: isRecord(value.error) && typeof value.error.code === "string" && typeof value.error.message === "string";
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function isPluginPermission(value) {
|
|
573
|
+
return typeof value === "string" && pluginPermissions.includes(value);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function permissionSupportState(permission) {
|
|
577
|
+
if (implementedPermissions.has(permission)) {
|
|
578
|
+
return "implemented";
|
|
579
|
+
}
|
|
580
|
+
if (routeGuardPermissions.has(permission)) {
|
|
581
|
+
return "routeGuard";
|
|
582
|
+
}
|
|
583
|
+
return "reserved";
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function createPermissionSummary(permission) {
|
|
587
|
+
const supportState = permissionSupportState(permission);
|
|
588
|
+
const statusMessage = supportState === "implemented"
|
|
589
|
+
? "Available to trusted mainEntry Host API."
|
|
590
|
+
: supportState === "routeGuard"
|
|
591
|
+
? "Declared for renderer route guards; not exposed to trusted mainEntry Host API."
|
|
592
|
+
: "Reserved for a future Host API; trusted mainEntry calls return PluginCapabilityUnsupported.";
|
|
593
|
+
return { permission, supportState, statusMessage };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function sanitizeJsonValue(value) {
|
|
597
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
598
|
+
return value;
|
|
599
|
+
}
|
|
600
|
+
if (typeof value === "number") {
|
|
601
|
+
return Number.isFinite(value) ? value : null;
|
|
602
|
+
}
|
|
603
|
+
if (Array.isArray(value)) {
|
|
604
|
+
return value.map(sanitizeJsonValue);
|
|
605
|
+
}
|
|
606
|
+
if (isRecord(value)) {
|
|
607
|
+
return sanitizeJsonObject(value);
|
|
608
|
+
}
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function sanitizeJsonObject(value) {
|
|
613
|
+
return Object.fromEntries(Object.entries(value).map(function sanitizeEntry(entry) {
|
|
614
|
+
return [entry[0], sanitizeJsonValue(entry[1])];
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function isRecord(value) {
|
|
619
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function isLogLevel(value) {
|
|
623
|
+
return value === "debug" || value === "info" || value === "warn" || value === "error";
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function isPathInside(rootDirectory, childPath) {
|
|
627
|
+
const root = path.resolve(rootDirectory);
|
|
628
|
+
const child = path.resolve(childPath);
|
|
629
|
+
const relative = path.relative(root, child);
|
|
630
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function mimeType(filePath) {
|
|
634
|
+
const types = {
|
|
635
|
+
".html": "text/html",
|
|
636
|
+
".js": "text/javascript",
|
|
637
|
+
".mjs": "text/javascript",
|
|
638
|
+
".css": "text/css",
|
|
639
|
+
".json": "application/json",
|
|
640
|
+
".svg": "image/svg+xml",
|
|
641
|
+
".png": "image/png",
|
|
642
|
+
".jpg": "image/jpeg",
|
|
643
|
+
".jpeg": "image/jpeg",
|
|
644
|
+
".gif": "image/gif",
|
|
645
|
+
".webp": "image/webp",
|
|
646
|
+
".ico": "image/x-icon",
|
|
647
|
+
".woff": "font/woff",
|
|
648
|
+
".woff2": "font/woff2",
|
|
649
|
+
".ttf": "font/ttf"
|
|
650
|
+
};
|
|
651
|
+
return types[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function requiredPathArgument(args, name) {
|
|
655
|
+
const value = requiredArgument(args, name);
|
|
656
|
+
const resolved = path.resolve(value);
|
|
657
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
|
|
658
|
+
throw new Error(name + " directory does not exist: " + resolved);
|
|
659
|
+
}
|
|
660
|
+
return resolved;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function requiredArgument(args, name) {
|
|
664
|
+
const prefix = name + "=";
|
|
665
|
+
const argument = args.find(function findArgument(value) {
|
|
666
|
+
return value.startsWith(prefix);
|
|
667
|
+
});
|
|
668
|
+
if (!argument || argument.length === prefix.length) {
|
|
669
|
+
throw new Error("Missing required argument: " + name);
|
|
670
|
+
}
|
|
671
|
+
return argument.slice(prefix.length);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function readJson(filePath) {
|
|
675
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function ok(value) {
|
|
679
|
+
return { ok: true, value };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function fail(code, message, details) {
|
|
683
|
+
return { ok: false, error: { code, message, details } };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function failStartup(error) {
|
|
687
|
+
clearTimeout(smokeTimeout);
|
|
688
|
+
process.stderr.write("[nexus-plugin-dev] startup failed: " + errorMessage(error) + "\n");
|
|
689
|
+
app.exit(1);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function errorMessage(error) {
|
|
693
|
+
return error instanceof Error ? error.stack || error.message : String(error);
|
|
694
|
+
}
|