@aiot-nexus/plugin-sdk 1.0.1 → 1.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 +14 -5
- package/dev-host/host.html +82 -15
- package/dev-host/main.js +98 -8
- package/dist/dev-host-metadata.json +3 -0
- package/dist/src/cli/run.js +103 -3
- package/dist/src/dev.d.ts +21 -9
- package/dist/src/dev.js +100 -19
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/scaffold.js +2 -0
- package/dist/src/validation.d.ts +4 -1
- package/dist/src/validation.js +8 -6
- package/dist/src/web-dev.d.ts +11 -0
- package/dist/src/web-dev.js +379 -0
- package/package.json +6 -3
- package/templates/minimal/README.md.tmpl +5 -2
- package/templates/minimal/scripts/build.js.tmpl +21 -4
- package/templates/minimal/src/renderer/app.ts.tmpl +37 -4
- package/templates/minimal/src/renderer/index.html +2 -2
- package/templates/minimal/src/renderer/plugin.css +34 -27
- package/templates/sqlite/README.md.tmpl +5 -2
- package/templates/sqlite/scripts/build.js.tmpl +28 -7
- package/templates/sqlite/src/renderer/app.ts.tmpl +37 -4
- package/templates/sqlite/src/renderer/index.html +2 -2
- package/templates/sqlite/src/renderer/plugin.css +52 -38
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.pluginWebDevServerUrl = exports.createPluginWebDevServer = exports.runPluginWebDev = void 0;
|
|
27
|
+
const child_process_1 = require("child_process");
|
|
28
|
+
const fs = __importStar(require("fs"));
|
|
29
|
+
const http = __importStar(require("http"));
|
|
30
|
+
const path = __importStar(require("path"));
|
|
31
|
+
const dev_1 = require("./dev");
|
|
32
|
+
const plugin_framework_version_1 = require("../.generated/plugin-framework-version");
|
|
33
|
+
const loopbackHost = "127.0.0.1";
|
|
34
|
+
const defaultBodyLimit = 1024 * 1024;
|
|
35
|
+
const browserApiPrefix = "/__nexus_plugin_dev";
|
|
36
|
+
const pluginAssetPrefix = "/plugin/";
|
|
37
|
+
const pluginContentSecurityPolicy = "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'";
|
|
38
|
+
const hostContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'";
|
|
39
|
+
function runPluginWebDev(options, io) {
|
|
40
|
+
const projectDirectory = path.resolve(options.projectDirectory);
|
|
41
|
+
let packageKind = "auto";
|
|
42
|
+
if (options.build) {
|
|
43
|
+
io.stdout("Building plugin UI with development source maps: " + projectDirectory);
|
|
44
|
+
packageKind = (0, dev_1.buildPluginRendererForDevelopment)(projectDirectory);
|
|
45
|
+
if (packageKind === "package") {
|
|
46
|
+
io.stdout("Plugin project has no build:web script; using its full build output for compatibility.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const target = (0, dev_1.preparePluginBrowserDebugTarget)({ ...options, projectDirectory }, packageKind);
|
|
50
|
+
const server = createPluginWebDevServer(target, io);
|
|
51
|
+
server.once("error", (error) => {
|
|
52
|
+
io.stderr("Plugin browser dev host failed: " + error.message);
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
});
|
|
55
|
+
server.listen(options.port, loopbackHost, () => {
|
|
56
|
+
const url = pluginWebDevServerUrl(server);
|
|
57
|
+
io.stdout("Plugin browser UI host: " + url
|
|
58
|
+
+ "\nPlugin package: " + target.packageDirectory
|
|
59
|
+
+ "\nContribution: " + target.surface + ":" + target.contributionId
|
|
60
|
+
+ "\nMain entry: disabled in browser UI mode");
|
|
61
|
+
if (options.openBrowser) {
|
|
62
|
+
openDefaultBrowser(url, io);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
exports.runPluginWebDev = runPluginWebDev;
|
|
68
|
+
function createPluginWebDevServer(target, io) {
|
|
69
|
+
const hostHtmlPath = path.join(resolvePackageRoot(), "dev-host", "host.html");
|
|
70
|
+
if (!fs.existsSync(hostHtmlPath) || !fs.statSync(hostHtmlPath).isFile()) {
|
|
71
|
+
throw new dev_1.PluginDevError("Plugin SDK browser host is missing: " + hostHtmlPath);
|
|
72
|
+
}
|
|
73
|
+
const runtime = { settingsValues: {} };
|
|
74
|
+
const server = http.createServer((request, response) => {
|
|
75
|
+
void handleBrowserRequest(server, target, runtime, hostHtmlPath, request, response, io).catch((error) => {
|
|
76
|
+
const statusCode = isBrowserRequestError(error) ? error.statusCode : 500;
|
|
77
|
+
if (!response.headersSent) {
|
|
78
|
+
sendJson(response, statusCode, {
|
|
79
|
+
error: statusCode === 500 ? "Browser host request failed." : errorMessage(error)
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
response.destroy();
|
|
84
|
+
}
|
|
85
|
+
if (statusCode === 500) {
|
|
86
|
+
io.stderr("Plugin browser host request failed: " + errorMessage(error));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
server.on("clientError", (_error, socket) => {
|
|
91
|
+
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
92
|
+
});
|
|
93
|
+
return server;
|
|
94
|
+
}
|
|
95
|
+
exports.createPluginWebDevServer = createPluginWebDevServer;
|
|
96
|
+
function pluginWebDevServerUrl(server) {
|
|
97
|
+
const address = server.address();
|
|
98
|
+
if (!address || typeof address === "string") {
|
|
99
|
+
throw new dev_1.PluginDevError("Plugin browser dev host is not listening on TCP.");
|
|
100
|
+
}
|
|
101
|
+
return "http://" + loopbackHost + ":" + String(address.port);
|
|
102
|
+
}
|
|
103
|
+
exports.pluginWebDevServerUrl = pluginWebDevServerUrl;
|
|
104
|
+
async function handleBrowserRequest(server, target, runtime, hostHtmlPath, request, response, io) {
|
|
105
|
+
if (!requestMatchesServer(request, server)) {
|
|
106
|
+
sendJson(response, 403, { error: "Unexpected Host header." });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const method = request.method || "GET";
|
|
110
|
+
const requestUrl = new URL(request.url || "/", pluginWebDevServerUrl(server));
|
|
111
|
+
const pathname = requestUrl.pathname;
|
|
112
|
+
if ((pathname === "/" || pathname === "/index.html") && (method === "GET" || method === "HEAD")) {
|
|
113
|
+
sendFile(response, hostHtmlPath, method === "HEAD", hostContentSecurityPolicy);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (pathname === "/favicon.ico" && (method === "GET" || method === "HEAD")) {
|
|
117
|
+
response.writeHead(204, commonHeaders());
|
|
118
|
+
response.end();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (pathname === browserApiPrefix + "/config" && method === "GET") {
|
|
122
|
+
sendJson(response, 200, createBrowserConfiguration(target, pluginWebDevServerUrl(server)));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (pathname === browserApiPrefix + "/invoke" && method === "POST") {
|
|
126
|
+
if (!requestOriginAllowed(request, pluginWebDevServerUrl(server))) {
|
|
127
|
+
sendJson(response, 403, { error: "Unexpected request origin." });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const requestBody = await readJsonBody(request);
|
|
131
|
+
sendJson(response, 200, invokeBrowserHost(target, runtime, requestBody, io));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (pathname.startsWith(pluginAssetPrefix) && (method === "GET" || method === "HEAD")) {
|
|
135
|
+
const rendererFile = resolveRendererFile(target, pathname.slice(pluginAssetPrefix.length));
|
|
136
|
+
if (!rendererFile) {
|
|
137
|
+
sendJson(response, 404, { error: "Plugin renderer asset was not found." });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
sendFile(response, rendererFile, method === "HEAD", pluginContentSecurityPolicy);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
sendJson(response, 404, { error: "Not found." });
|
|
144
|
+
}
|
|
145
|
+
function createBrowserConfiguration(target, origin) {
|
|
146
|
+
const encodedEntry = target.manifest.rendererEntry.split("/").map(encodeURIComponent).join("/");
|
|
147
|
+
const query = new URLSearchParams({
|
|
148
|
+
surface: target.surface,
|
|
149
|
+
contributionId: target.contributionId
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
pluginId: target.pluginId,
|
|
153
|
+
frameOrigin: origin,
|
|
154
|
+
frameUrl: origin + pluginAssetPrefix + encodedEntry + "?" + query.toString(),
|
|
155
|
+
supportedCultures: target.manifest.supportedCultures,
|
|
156
|
+
smoke: false,
|
|
157
|
+
context: pluginContext(target)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function pluginContext(target) {
|
|
161
|
+
return {
|
|
162
|
+
schemaVersion: plugin_framework_version_1.PLUGIN_MANIFEST_SCHEMA_VERSION,
|
|
163
|
+
pluginId: target.pluginId,
|
|
164
|
+
surface: target.surface,
|
|
165
|
+
contributionId: target.contributionId,
|
|
166
|
+
language: target.language,
|
|
167
|
+
themeMode: target.themeMode,
|
|
168
|
+
hostApiVersion: plugin_framework_version_1.PLUGIN_FRAMEWORK_VERSION
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function invokeBrowserHost(target, runtime, request, io) {
|
|
172
|
+
if (!isRecord(request) || typeof request.method !== "string") {
|
|
173
|
+
return fail("InvalidRequest", "Plugin renderer request is invalid.");
|
|
174
|
+
}
|
|
175
|
+
const params = isRecord(request.params) ? request.params : undefined;
|
|
176
|
+
if (request.method === "getContext") {
|
|
177
|
+
return ok(pluginContext(target));
|
|
178
|
+
}
|
|
179
|
+
if (request.method === "getSettings") {
|
|
180
|
+
if (!hasPermission(target, "settings")) {
|
|
181
|
+
return fail("PluginPermissionDenied", "Plugin does not declare settings permission.");
|
|
182
|
+
}
|
|
183
|
+
return ok(settingsDocument(target, runtime));
|
|
184
|
+
}
|
|
185
|
+
if (request.method === "updateSettings") {
|
|
186
|
+
if (!hasPermission(target, "settings")) {
|
|
187
|
+
return fail("PluginPermissionDenied", "Plugin does not declare settings permission.");
|
|
188
|
+
}
|
|
189
|
+
if (!params || !isRecord(params.values)) {
|
|
190
|
+
return fail("InvalidRequest", "Plugin renderer settings update requires values.");
|
|
191
|
+
}
|
|
192
|
+
runtime.settingsValues = { ...runtime.settingsValues, ...sanitizeJsonObject(params.values) };
|
|
193
|
+
return ok(settingsDocument(target, runtime));
|
|
194
|
+
}
|
|
195
|
+
if (request.method === "appendLog") {
|
|
196
|
+
if (!hasPermission(target, "businessLog")) {
|
|
197
|
+
return fail("PluginPermissionDenied", "Plugin does not declare businessLog permission.");
|
|
198
|
+
}
|
|
199
|
+
if (!params || !isLogLevel(params.level) || typeof params.message !== "string" || !params.message.trim()) {
|
|
200
|
+
return fail("InvalidRequest", "Plugin renderer log request is invalid.");
|
|
201
|
+
}
|
|
202
|
+
const category = "plugin:" + target.pluginId
|
|
203
|
+
+ (typeof params.category === "string" && params.category.trim() ? ":" + params.category.trim() : "");
|
|
204
|
+
const entry = {
|
|
205
|
+
timestamp: new Date().toISOString(),
|
|
206
|
+
level: params.level,
|
|
207
|
+
category,
|
|
208
|
+
message: params.message.trim(),
|
|
209
|
+
exception: typeof params.exception === "string" && params.exception.trim() ? params.exception.trim() : undefined
|
|
210
|
+
};
|
|
211
|
+
const output = "[nexus-plugin-web][" + entry.level + "][" + category + "] " + entry.message
|
|
212
|
+
+ (entry.exception ? "\n" + entry.exception : "");
|
|
213
|
+
if (entry.level === "warn" || entry.level === "error") {
|
|
214
|
+
io.stderr(output);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
io.stdout(output);
|
|
218
|
+
}
|
|
219
|
+
return ok(entry);
|
|
220
|
+
}
|
|
221
|
+
if (request.method === "invokeMain") {
|
|
222
|
+
return fail("PluginCapabilityUnsupported", "Plugin mainEntry is not executed by the browser UI debug host. Use nexus-plugin dev for Host API integration.", { operation: params && typeof params.operation === "string" ? params.operation : undefined });
|
|
223
|
+
}
|
|
224
|
+
return fail("InvalidRequest", "Plugin renderer request method is invalid.", { method: request.method });
|
|
225
|
+
}
|
|
226
|
+
function settingsDocument(target, runtime) {
|
|
227
|
+
return {
|
|
228
|
+
schemaVersion: plugin_framework_version_1.PLUGIN_MANIFEST_SCHEMA_VERSION,
|
|
229
|
+
pluginId: target.pluginId,
|
|
230
|
+
values: runtime.settingsValues
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function resolveRendererFile(target, encodedRelativePath) {
|
|
234
|
+
let relativePath;
|
|
235
|
+
try {
|
|
236
|
+
relativePath = decodeURIComponent(encodedRelativePath);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
if (!relativePath || relativePath.includes("\0")) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
const entryPath = path.resolve(target.packageDirectory, target.manifest.rendererEntry);
|
|
245
|
+
const rendererRoot = path.dirname(entryPath);
|
|
246
|
+
const requestedPath = path.resolve(target.packageDirectory, relativePath);
|
|
247
|
+
if (!isPathInside(rendererRoot, requestedPath)
|
|
248
|
+
|| !fs.existsSync(requestedPath)
|
|
249
|
+
|| !fs.statSync(requestedPath).isFile()
|
|
250
|
+
|| fs.lstatSync(requestedPath).isSymbolicLink()) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
const realRoot = fs.realpathSync(rendererRoot);
|
|
254
|
+
const realPath = fs.realpathSync(requestedPath);
|
|
255
|
+
return isPathInside(realRoot, realPath) ? realPath : undefined;
|
|
256
|
+
}
|
|
257
|
+
async function readJsonBody(request) {
|
|
258
|
+
const chunks = [];
|
|
259
|
+
let size = 0;
|
|
260
|
+
for await (const chunk of request) {
|
|
261
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
262
|
+
size += buffer.length;
|
|
263
|
+
if (size > defaultBodyLimit) {
|
|
264
|
+
throw browserRequestError(413, "Request body is too large.");
|
|
265
|
+
}
|
|
266
|
+
chunks.push(buffer);
|
|
267
|
+
}
|
|
268
|
+
if (chunks.length === 0) {
|
|
269
|
+
throw browserRequestError(400, "Request body is required.");
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
throw browserRequestError(400, "Request body is not valid JSON.");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function sendFile(response, filePath, headOnly, contentSecurityPolicy) {
|
|
279
|
+
response.writeHead(200, {
|
|
280
|
+
...commonHeaders(),
|
|
281
|
+
"Content-Type": mimeType(filePath),
|
|
282
|
+
"Content-Security-Policy": contentSecurityPolicy
|
|
283
|
+
});
|
|
284
|
+
if (headOnly) {
|
|
285
|
+
response.end();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const stream = fs.createReadStream(filePath);
|
|
289
|
+
stream.on("error", () => response.destroy());
|
|
290
|
+
stream.pipe(response);
|
|
291
|
+
}
|
|
292
|
+
function sendJson(response, statusCode, value) {
|
|
293
|
+
const body = JSON.stringify(value);
|
|
294
|
+
response.writeHead(statusCode, {
|
|
295
|
+
...commonHeaders(),
|
|
296
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
297
|
+
"Content-Length": Buffer.byteLength(body)
|
|
298
|
+
});
|
|
299
|
+
response.end(body);
|
|
300
|
+
}
|
|
301
|
+
function commonHeaders() {
|
|
302
|
+
return {
|
|
303
|
+
"Cache-Control": "no-store",
|
|
304
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
305
|
+
"X-Content-Type-Options": "nosniff"
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function requestMatchesServer(request, server) {
|
|
309
|
+
const expected = new URL(pluginWebDevServerUrl(server));
|
|
310
|
+
return request.headers.host === expected.host || request.headers.host === "localhost:" + expected.port;
|
|
311
|
+
}
|
|
312
|
+
function requestOriginAllowed(request, origin) {
|
|
313
|
+
return request.headers.origin === undefined || request.headers.origin === origin;
|
|
314
|
+
}
|
|
315
|
+
function hasPermission(target, permission) {
|
|
316
|
+
return target.manifest.permissions.includes(permission);
|
|
317
|
+
}
|
|
318
|
+
function sanitizeJsonObject(value) {
|
|
319
|
+
return JSON.parse(JSON.stringify(value));
|
|
320
|
+
}
|
|
321
|
+
function ok(value) {
|
|
322
|
+
return { ok: true, value };
|
|
323
|
+
}
|
|
324
|
+
function fail(code, message, details) {
|
|
325
|
+
return { ok: false, error: { code, message, details } };
|
|
326
|
+
}
|
|
327
|
+
function isRecord(value) {
|
|
328
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
329
|
+
}
|
|
330
|
+
function isLogLevel(value) {
|
|
331
|
+
return value === "debug" || value === "info" || value === "warn" || value === "error";
|
|
332
|
+
}
|
|
333
|
+
function isPathInside(rootDirectory, childPath) {
|
|
334
|
+
const relative = path.relative(path.resolve(rootDirectory), path.resolve(childPath));
|
|
335
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
336
|
+
}
|
|
337
|
+
function mimeType(filePath) {
|
|
338
|
+
const types = {
|
|
339
|
+
".html": "text/html; charset=utf-8",
|
|
340
|
+
".js": "text/javascript; charset=utf-8",
|
|
341
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
342
|
+
".css": "text/css; charset=utf-8",
|
|
343
|
+
".json": "application/json; charset=utf-8",
|
|
344
|
+
".svg": "image/svg+xml",
|
|
345
|
+
".png": "image/png",
|
|
346
|
+
".jpg": "image/jpeg",
|
|
347
|
+
".jpeg": "image/jpeg",
|
|
348
|
+
".gif": "image/gif",
|
|
349
|
+
".webp": "image/webp",
|
|
350
|
+
".ico": "image/x-icon",
|
|
351
|
+
".woff": "font/woff",
|
|
352
|
+
".woff2": "font/woff2",
|
|
353
|
+
".ttf": "font/ttf"
|
|
354
|
+
};
|
|
355
|
+
return types[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
|
356
|
+
}
|
|
357
|
+
function openDefaultBrowser(url, io) {
|
|
358
|
+
const command = process.platform === "win32" ? "rundll32.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
359
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
360
|
+
const child = (0, child_process_1.spawn)(command, args, { detached: true, stdio: "ignore" });
|
|
361
|
+
child.once("error", (error) => {
|
|
362
|
+
io.stderr("Default browser could not be opened: " + error.message + "\nOpen " + url + " manually.");
|
|
363
|
+
});
|
|
364
|
+
child.unref();
|
|
365
|
+
}
|
|
366
|
+
function browserRequestError(statusCode, message) {
|
|
367
|
+
const error = new Error(message);
|
|
368
|
+
error.statusCode = statusCode;
|
|
369
|
+
return error;
|
|
370
|
+
}
|
|
371
|
+
function isBrowserRequestError(value) {
|
|
372
|
+
return value instanceof Error && typeof value.statusCode === "number";
|
|
373
|
+
}
|
|
374
|
+
function resolvePackageRoot() {
|
|
375
|
+
return path.resolve(__dirname, "..", "..");
|
|
376
|
+
}
|
|
377
|
+
function errorMessage(error) {
|
|
378
|
+
return error instanceof Error ? error.message : String(error);
|
|
379
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiot-nexus/plugin-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Types, PluginFrame client, and CLI for AIoT Nexus independent plugins.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
@@ -34,8 +34,11 @@
|
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "node scripts/build.js",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
37
|
+
"verify:package": "node scripts/verify-package.js",
|
|
38
|
+
"test": "npm run build && node tests/package.test.js && node tests/cli.test.js && node tests/dev.test.js && node tests/frame-client.test.js && node tests/web-dev.test.js && npm run verify:package",
|
|
39
|
+
"prepack": "node scripts/prepack.js",
|
|
40
|
+
"pack:sdk": "npm run build && npm run verify:package && node scripts/pack.js",
|
|
41
|
+
"publish:sdk": "npm publish"
|
|
39
42
|
},
|
|
40
43
|
"dependencies": {
|
|
41
44
|
"ajv": "8.17.1"
|
|
@@ -6,6 +6,7 @@ AIoT Nexus independent PluginFrame plugin generated from the `minimal` template.
|
|
|
6
6
|
npm install
|
|
7
7
|
npm run typecheck
|
|
8
8
|
npm run build
|
|
9
|
+
npm run dev:web
|
|
9
10
|
npm run dev
|
|
10
11
|
```
|
|
11
12
|
|
|
@@ -13,10 +14,12 @@ The deployable plugin package is `dist/__PLUGIN_ID__/`. Copy that directory to t
|
|
|
13
14
|
|
|
14
15
|
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.
|
|
15
16
|
|
|
16
|
-
The renderer follows `context.language` from Nexus, subscribes to in-place context updates without reloading the iframe, and includes `en-US` and `zh-CN` message catalogs. Add or edit renderer messages in `src/renderer/app.ts`; edit host-shell titles and descriptions in `src/manifest.json` resources.
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
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.
|
|
17
20
|
|
|
18
21
|
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.
|
|
19
22
|
|
|
20
23
|
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.
|
|
21
24
|
|
|
22
|
-
The independent host stores data under `.nexus-plugin-dev/` and mirrors the PluginFrame protocol and current Host API surface. Run the deployable `dist/__PLUGIN_ID__/` in Nexus before delivery because the standalone host does not reproduce the full application shell or routing state.
|
|
25
|
+
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.
|
|
@@ -9,7 +9,10 @@ const ts = require("typescript");
|
|
|
9
9
|
const pluginId = "__PLUGIN_ID__";
|
|
10
10
|
const pluginRoot = path.resolve(__dirname, "..");
|
|
11
11
|
const buildDirectory = path.join(pluginRoot, ".build");
|
|
12
|
-
const
|
|
12
|
+
const rendererOnly = readRendererOnlyOption(process.argv.slice(2));
|
|
13
|
+
const outputDirectory = rendererOnly
|
|
14
|
+
? path.join(pluginRoot, ".nexus-plugin-dev", "web-package", pluginId)
|
|
15
|
+
: path.join(pluginRoot, "dist", pluginId);
|
|
13
16
|
const sdkModuleSpecifier = "@aiot-nexus/plugin-sdk/browser";
|
|
14
17
|
|
|
15
18
|
run();
|
|
@@ -17,12 +20,16 @@ run();
|
|
|
17
20
|
function run() {
|
|
18
21
|
fs.rmSync(buildDirectory, { recursive: true, force: true });
|
|
19
22
|
fs.rmSync(outputDirectory, { recursive: true, force: true });
|
|
20
|
-
|
|
23
|
+
if (!rendererOnly) {
|
|
24
|
+
runTypeScript("tsconfig.main.json");
|
|
25
|
+
}
|
|
21
26
|
runTypeScript("tsconfig.renderer.json");
|
|
22
27
|
|
|
23
28
|
fs.mkdirSync(path.join(outputDirectory, "renderer", "assets"), { recursive: true });
|
|
24
29
|
fs.copyFileSync(path.join(pluginRoot, "src", "manifest.json"), path.join(outputDirectory, "manifest.json"));
|
|
25
|
-
|
|
30
|
+
if (!rendererOnly) {
|
|
31
|
+
fs.copyFileSync(path.join(buildDirectory, "main", "main.js"), path.join(outputDirectory, "main.cjs"));
|
|
32
|
+
}
|
|
26
33
|
fs.copyFileSync(path.join(pluginRoot, "src", "renderer", "index.html"), path.join(outputDirectory, "renderer", "index.html"));
|
|
27
34
|
fs.copyFileSync(path.join(pluginRoot, "src", "renderer", "plugin.css"), path.join(outputDirectory, "renderer", "assets", "plugin.css"));
|
|
28
35
|
rewriteRendererSdkImport(
|
|
@@ -31,7 +38,17 @@ function run() {
|
|
|
31
38
|
);
|
|
32
39
|
copyBrowserSdk();
|
|
33
40
|
fs.rmSync(buildDirectory, { recursive: true, force: true });
|
|
34
|
-
console.log("[" + pluginId + "] built " + outputDirectory);
|
|
41
|
+
console.log("[" + pluginId + "] built " + (rendererOnly ? "browser UI package " : "") + outputDirectory);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readRendererOnlyOption(args) {
|
|
45
|
+
if (args.length === 0) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (args.length === 1 && args[0] === "--renderer-only") {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
throw new Error("Unknown build option: " + args.join(" "));
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
function runTypeScript(configName) {
|
|
@@ -6,6 +6,7 @@ interface EchoResponse {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
type SupportedLanguage = "en-US" | "zh-CN";
|
|
9
|
+
type ThemeMode = "light" | "dark" | "system";
|
|
9
10
|
|
|
10
11
|
const englishMessages = {
|
|
11
12
|
"document.title": "Plugin Echo",
|
|
@@ -58,8 +59,10 @@ const button = requireElement<HTMLButtonElement>("echoButton");
|
|
|
58
59
|
const resultOutput = requireElement<HTMLOutputElement>("echoResult");
|
|
59
60
|
const contextOutput = requireElement<HTMLElement>("pluginContext");
|
|
60
61
|
const connectionState = requireElement<HTMLElement>("connectionState");
|
|
62
|
+
const systemDarkMode = window.matchMedia("(prefers-color-scheme: dark)");
|
|
61
63
|
|
|
62
64
|
let language: SupportedLanguage = "en-US";
|
|
65
|
+
let themeMode: ThemeMode = "system";
|
|
63
66
|
let pluginId = "";
|
|
64
67
|
let surface = "";
|
|
65
68
|
let connectionStateKey: MessageKey = "status.waiting";
|
|
@@ -77,23 +80,26 @@ input.addEventListener("keydown", (event) => {
|
|
|
77
80
|
const stopContextUpdates = client.onContextChanged((context) => {
|
|
78
81
|
pluginId = context.pluginId;
|
|
79
82
|
surface = context.surface;
|
|
80
|
-
|
|
83
|
+
applyContext(context);
|
|
81
84
|
});
|
|
85
|
+
systemDarkMode.addEventListener("change", handleSystemThemeChange);
|
|
82
86
|
window.addEventListener("beforeunload", () => {
|
|
83
87
|
stopContextUpdates();
|
|
88
|
+
systemDarkMode.removeEventListener("change", handleSystemThemeChange);
|
|
84
89
|
client.dispose();
|
|
85
90
|
});
|
|
86
91
|
|
|
87
|
-
applyLanguage(language);
|
|
88
|
-
|
|
89
92
|
void client.waitForContext().then((context) => {
|
|
90
93
|
pluginId = context.pluginId;
|
|
91
94
|
surface = context.surface;
|
|
92
95
|
connectionStateKey = "status.ready";
|
|
93
|
-
|
|
96
|
+
applyContext(context);
|
|
94
97
|
}).catch((error: unknown) => {
|
|
95
98
|
connectionStateKey = "status.error";
|
|
96
99
|
outputState = { kind: "literal", value: errorMessage(error) };
|
|
100
|
+
applyLanguage(language);
|
|
101
|
+
applyTheme(themeMode);
|
|
102
|
+
revealPluginContent();
|
|
97
103
|
renderLocalizedState();
|
|
98
104
|
});
|
|
99
105
|
|
|
@@ -133,6 +139,33 @@ function applyLanguage(nextLanguage: SupportedLanguage): void {
|
|
|
133
139
|
client.notifyResize();
|
|
134
140
|
}
|
|
135
141
|
|
|
142
|
+
function applyContext(context: { language: string; themeMode: ThemeMode }): void {
|
|
143
|
+
applyLanguage(resolveLanguage(context.language));
|
|
144
|
+
applyTheme(context.themeMode);
|
|
145
|
+
revealPluginContent();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function revealPluginContent(): void {
|
|
149
|
+
document.documentElement.dataset.contextReady = "true";
|
|
150
|
+
document.body.hidden = false;
|
|
151
|
+
client.notifyResize();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function applyTheme(nextThemeMode: ThemeMode): void {
|
|
155
|
+
themeMode = nextThemeMode;
|
|
156
|
+
const resolvedTheme = themeMode === "system"
|
|
157
|
+
? (systemDarkMode.matches ? "dark" : "light")
|
|
158
|
+
: themeMode;
|
|
159
|
+
document.documentElement.dataset.theme = resolvedTheme;
|
|
160
|
+
document.documentElement.style.colorScheme = resolvedTheme;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function handleSystemThemeChange(): void {
|
|
164
|
+
if (themeMode === "system") {
|
|
165
|
+
applyTheme(themeMode);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
136
169
|
function renderLocalizedState(): void {
|
|
137
170
|
contextOutput.textContent = pluginId
|
|
138
171
|
? pluginId + " / " + localizedSurface(surface)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="en-US">
|
|
2
|
+
<html lang="en-US" data-context-ready="false">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title data-i18n="document.title">Plugin Echo</title>
|
|
7
7
|
<link rel="stylesheet" href="assets/plugin.css">
|
|
8
8
|
</head>
|
|
9
|
-
<body>
|
|
9
|
+
<body hidden>
|
|
10
10
|
<main class="plugin-shell">
|
|
11
11
|
<header class="plugin-header">
|
|
12
12
|
<div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
color-scheme: light
|
|
2
|
+
color-scheme: light;
|
|
3
3
|
font-family: "Segoe UI", Arial, sans-serif;
|
|
4
4
|
background: #f3f5f7;
|
|
5
5
|
color: #20252b;
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
box-sizing: border-box;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
:root:not([data-context-ready="true"]) .plugin-shell {
|
|
13
|
+
visibility: hidden;
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
body {
|
|
13
17
|
margin: 0;
|
|
14
18
|
min-height: 0;
|
|
@@ -130,35 +134,38 @@ output {
|
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
137
|
+
:root[data-theme="dark"] {
|
|
138
|
+
color-scheme: dark;
|
|
139
|
+
background: #16191d;
|
|
140
|
+
color: #edf0f2;
|
|
141
|
+
}
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
143
|
+
:root[data-theme="dark"] body {
|
|
144
|
+
background: #16191d;
|
|
145
|
+
color: #edf0f2;
|
|
146
|
+
}
|
|
145
147
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
:root[data-theme="dark"] .plugin-header,
|
|
149
|
+
:root[data-theme="dark"] .echo-panel {
|
|
150
|
+
border-color: #454b52;
|
|
151
|
+
background: #24282d;
|
|
152
|
+
}
|
|
150
153
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
154
|
+
:root[data-theme="dark"] p,
|
|
155
|
+
:root[data-theme="dark"] .result-row > span {
|
|
156
|
+
color: #aeb7c2;
|
|
157
|
+
}
|
|
156
158
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
:root[data-theme="dark"] input {
|
|
160
|
+
border-color: #59616a;
|
|
161
|
+
background: #191c20;
|
|
162
|
+
color: #edf0f2;
|
|
163
|
+
}
|
|
160
164
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
:root[data-theme="dark"] .result-row {
|
|
166
|
+
border-top-color: #454b52;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
:root[data-theme="dark"] .status {
|
|
170
|
+
color: #75d6a5;
|
|
164
171
|
}
|