@openvcs/sdk 0.2.2 → 0.2.4
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 +76 -7
- package/lib/build.d.ts +28 -0
- package/lib/build.js +188 -0
- package/lib/cli.js +21 -2
- package/lib/dist.d.ts +4 -7
- package/lib/dist.js +67 -113
- package/lib/init.d.ts +2 -0
- package/lib/init.js +13 -8
- package/lib/runtime/contracts.d.ts +45 -0
- package/lib/runtime/contracts.js +4 -0
- package/lib/runtime/dispatcher.d.ts +16 -0
- package/lib/runtime/dispatcher.js +133 -0
- package/lib/runtime/errors.d.ts +5 -0
- package/lib/runtime/errors.js +26 -0
- package/lib/runtime/host.d.ts +10 -0
- package/lib/runtime/host.js +48 -0
- package/lib/runtime/index.d.ts +9 -0
- package/lib/runtime/index.js +166 -0
- package/lib/runtime/transport.d.ts +14 -0
- package/lib/runtime/transport.js +72 -0
- package/lib/types/host.d.ts +57 -0
- package/lib/types/host.js +4 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.js +22 -0
- package/lib/types/plugin.d.ts +56 -0
- package/lib/types/plugin.js +4 -0
- package/lib/types/protocol.d.ts +77 -0
- package/lib/types/protocol.js +13 -0
- package/lib/types/vcs.d.ts +459 -0
- package/lib/types/vcs.js +4 -0
- package/package.json +16 -3
- package/src/lib/build.ts +229 -0
- package/src/lib/cli.ts +21 -2
- package/src/lib/dist.ts +76 -128
- package/src/lib/init.ts +13 -8
- package/src/lib/runtime/contracts.ts +52 -0
- package/src/lib/runtime/dispatcher.ts +185 -0
- package/src/lib/runtime/errors.ts +27 -0
- package/src/lib/runtime/host.ts +72 -0
- package/src/lib/runtime/index.ts +201 -0
- package/src/lib/runtime/transport.ts +93 -0
- package/src/lib/types/host.ts +71 -0
- package/src/lib/types/index.ts +7 -0
- package/src/lib/types/plugin.ts +110 -0
- package/src/lib/types/protocol.ts +97 -0
- package/src/lib/types/vcs.ts +579 -0
- package/test/build.test.js +95 -0
- package/test/cli.test.js +37 -0
- package/test/dist.test.js +239 -15
- package/test/init.test.js +25 -0
- package/test/runtime.test.js +118 -0
package/test/cli.test.js
CHANGED
|
@@ -27,6 +27,7 @@ test("openvcs --help prints usage", () => {
|
|
|
27
27
|
const result = runCli(["--help"]);
|
|
28
28
|
assert.equal(result.status, 0);
|
|
29
29
|
assert.match(result.stdout, /Usage: openvcs <command>/);
|
|
30
|
+
assert.match(result.stdout, /build \[args\]/);
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
test("openvcs with no args exits non-zero", () => {
|
|
@@ -41,6 +42,12 @@ test("openvcs dist --help prints dist usage", () => {
|
|
|
41
42
|
assert.match(result.stdout, /openvcs dist \[args\]/);
|
|
42
43
|
});
|
|
43
44
|
|
|
45
|
+
test("openvcs build --help prints build usage", () => {
|
|
46
|
+
const result = runCli(["build", "--help"]);
|
|
47
|
+
assert.equal(result.status, 0);
|
|
48
|
+
assert.match(result.stdout, /openvcs build \[args\]/);
|
|
49
|
+
});
|
|
50
|
+
|
|
44
51
|
test("openvcs init --help prints init usage", () => {
|
|
45
52
|
const result = runCli(["init", "--help"]);
|
|
46
53
|
assert.equal(result.status, 0);
|
|
@@ -70,6 +77,7 @@ test("openvcs dist command creates bundle", () => {
|
|
|
70
77
|
pluginDir,
|
|
71
78
|
"--out",
|
|
72
79
|
outDir,
|
|
80
|
+
"--no-build",
|
|
73
81
|
"--no-npm-deps",
|
|
74
82
|
]);
|
|
75
83
|
|
|
@@ -85,3 +93,32 @@ test("openvcs dist reports argument errors", () => {
|
|
|
85
93
|
assert.equal(result.status, 1);
|
|
86
94
|
assert.match(result.stderr, /missing value for --plugin-dir/);
|
|
87
95
|
});
|
|
96
|
+
|
|
97
|
+
test("openvcs build command builds code plugin assets", () => {
|
|
98
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
99
|
+
const pluginDir = path.join(root, "plugin");
|
|
100
|
+
|
|
101
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
102
|
+
id: "build-plugin",
|
|
103
|
+
module: { exec: "plugin.js" },
|
|
104
|
+
});
|
|
105
|
+
writeJson(path.join(pluginDir, "package.json"), {
|
|
106
|
+
name: "build-plugin",
|
|
107
|
+
private: true,
|
|
108
|
+
scripts: {
|
|
109
|
+
"build:plugin": "node ./scripts/build-plugin.js",
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
writeText(
|
|
113
|
+
path.join(pluginDir, "scripts", "build-plugin.js"),
|
|
114
|
+
"const fs = require('node:fs');\nconst path = require('node:path');\nconst out = path.join(process.cwd(), 'bin', 'plugin.js');\nfs.mkdirSync(path.dirname(out), { recursive: true });\nfs.writeFileSync(out, 'export {};\\n', 'utf8');\n"
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const result = runCli(["build", "--plugin-dir", pluginDir]);
|
|
118
|
+
|
|
119
|
+
assert.equal(result.status, 0);
|
|
120
|
+
assert.equal(result.stdout.trim(), "build-plugin");
|
|
121
|
+
assert.equal(fs.existsSync(path.join(pluginDir, "bin", "plugin.js")), true);
|
|
122
|
+
|
|
123
|
+
cleanupTempDir(root);
|
|
124
|
+
});
|
package/test/dist.test.js
CHANGED
|
@@ -26,11 +26,13 @@ test("parseDistArgs parses known flags", () => {
|
|
|
26
26
|
"some/plugin",
|
|
27
27
|
"--out",
|
|
28
28
|
"some/out",
|
|
29
|
+
"--no-build",
|
|
29
30
|
"--no-npm-deps",
|
|
30
31
|
"--verbose",
|
|
31
32
|
]);
|
|
32
33
|
assert.equal(parsed.pluginDir, path.resolve("some/plugin"));
|
|
33
34
|
assert.equal(parsed.outDir, path.resolve("some/out"));
|
|
35
|
+
assert.equal(parsed.noBuild, true);
|
|
34
36
|
assert.equal(parsed.noNpmDeps, true);
|
|
35
37
|
assert.equal(parsed.verbose, true);
|
|
36
38
|
});
|
|
@@ -237,7 +239,13 @@ test("bundlePlugin writes a gzip .ovcsp for themes-only plugin", async () => {
|
|
|
237
239
|
writeText(path.join(pluginDir, "themes", "default", "theme.json"), '{"name":"test"}\n');
|
|
238
240
|
writeText(path.join(pluginDir, "icon.png"), "icon-bytes");
|
|
239
241
|
|
|
240
|
-
const outPath = await bundlePlugin({
|
|
242
|
+
const outPath = await bundlePlugin({
|
|
243
|
+
pluginDir,
|
|
244
|
+
outDir,
|
|
245
|
+
verbose: false,
|
|
246
|
+
noBuild: true,
|
|
247
|
+
noNpmDeps: true,
|
|
248
|
+
});
|
|
241
249
|
const entries = await readBundleEntries(outPath);
|
|
242
250
|
|
|
243
251
|
assert.equal(path.basename(outPath), "ui-only.ovcsp");
|
|
@@ -248,15 +256,15 @@ test("bundlePlugin writes a gzip .ovcsp for themes-only plugin", async () => {
|
|
|
248
256
|
cleanupTempDir(root);
|
|
249
257
|
});
|
|
250
258
|
|
|
251
|
-
test("bundlePlugin rejects manifest with no module
|
|
259
|
+
test("bundlePlugin rejects manifest with no module, entry, or themes", async () => {
|
|
252
260
|
const root = makeTempDir("openvcs-sdk-test");
|
|
253
261
|
const pluginDir = path.join(root, "plugin");
|
|
254
262
|
const outDir = path.join(root, "out");
|
|
255
263
|
writeJson(path.join(pluginDir, "openvcs.plugin.json"), { id: "x" });
|
|
256
264
|
|
|
257
265
|
await assert.rejects(
|
|
258
|
-
() => bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true }),
|
|
259
|
-
/manifest has no module\.exec or themes\//
|
|
266
|
+
() => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
|
|
267
|
+
/manifest has no module\.exec, entry, or themes\//
|
|
260
268
|
);
|
|
261
269
|
|
|
262
270
|
cleanupTempDir(root);
|
|
@@ -274,7 +282,13 @@ test("bundlePlugin trims module.exec and includes extra bin files", async () =>
|
|
|
274
282
|
writeText(path.join(pluginDir, "bin", "module.mjs"), "export {};\n");
|
|
275
283
|
writeText(path.join(pluginDir, "bin", "helpers", "util.mjs"), "export const x = 1;\n");
|
|
276
284
|
|
|
277
|
-
const outPath = await bundlePlugin({
|
|
285
|
+
const outPath = await bundlePlugin({
|
|
286
|
+
pluginDir,
|
|
287
|
+
outDir,
|
|
288
|
+
verbose: false,
|
|
289
|
+
noBuild: true,
|
|
290
|
+
noNpmDeps: true,
|
|
291
|
+
});
|
|
278
292
|
const entries = await readBundleEntries(outPath);
|
|
279
293
|
|
|
280
294
|
assert.equal(entries.has("x/bin/module.mjs"), true);
|
|
@@ -282,6 +296,81 @@ test("bundlePlugin trims module.exec and includes extra bin files", async () =>
|
|
|
282
296
|
cleanupTempDir(root);
|
|
283
297
|
});
|
|
284
298
|
|
|
299
|
+
test("bundlePlugin builds code plugins before packaging", async () => {
|
|
300
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
301
|
+
const pluginDir = path.join(root, "plugin");
|
|
302
|
+
const outDir = path.join(root, "out");
|
|
303
|
+
|
|
304
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
305
|
+
id: "builder",
|
|
306
|
+
module: { exec: "plugin.js" },
|
|
307
|
+
});
|
|
308
|
+
writeJson(path.join(pluginDir, "package.json"), {
|
|
309
|
+
name: "builder",
|
|
310
|
+
private: true,
|
|
311
|
+
scripts: {
|
|
312
|
+
"build:plugin": "node ./scripts/build-plugin.js",
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
writeText(
|
|
316
|
+
path.join(pluginDir, "scripts", "build-plugin.js"),
|
|
317
|
+
"const fs = require('node:fs');\nconst path = require('node:path');\nconst out = path.join(process.cwd(), 'bin', 'plugin.js');\nfs.mkdirSync(path.dirname(out), { recursive: true });\nfs.writeFileSync(out, 'export {};\\n', 'utf8');\n"
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const outPath = await bundlePlugin({
|
|
321
|
+
pluginDir,
|
|
322
|
+
outDir,
|
|
323
|
+
verbose: false,
|
|
324
|
+
noBuild: false,
|
|
325
|
+
noNpmDeps: true,
|
|
326
|
+
});
|
|
327
|
+
const entries = await readBundleEntries(outPath);
|
|
328
|
+
|
|
329
|
+
assert.equal(entries.has("builder/bin/plugin.js"), true);
|
|
330
|
+
cleanupTempDir(root);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("bundlePlugin with no-build requires prebuilt module entrypoint", async () => {
|
|
334
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
335
|
+
const pluginDir = path.join(root, "plugin");
|
|
336
|
+
const outDir = path.join(root, "out");
|
|
337
|
+
|
|
338
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
339
|
+
id: "prebuilt",
|
|
340
|
+
module: { exec: "plugin.js" },
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
await assert.rejects(
|
|
344
|
+
() => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
|
|
345
|
+
/module entrypoint not found/
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
cleanupTempDir(root);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("bundlePlugin errors when code plugin lacks build:plugin", async () => {
|
|
352
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
353
|
+
const pluginDir = path.join(root, "plugin");
|
|
354
|
+
const outDir = path.join(root, "out");
|
|
355
|
+
|
|
356
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
357
|
+
id: "missing-script",
|
|
358
|
+
module: { exec: "plugin.js" },
|
|
359
|
+
});
|
|
360
|
+
writeJson(path.join(pluginDir, "package.json"), {
|
|
361
|
+
name: "missing-script",
|
|
362
|
+
private: true,
|
|
363
|
+
scripts: {},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
await assert.rejects(
|
|
367
|
+
() => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: false, noNpmDeps: true }),
|
|
368
|
+
/build:plugin/
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
cleanupTempDir(root);
|
|
372
|
+
});
|
|
373
|
+
|
|
285
374
|
test("bundlePlugin rejects module.exec path traversal", async () => {
|
|
286
375
|
const root = makeTempDir("openvcs-sdk-test");
|
|
287
376
|
const pluginDir = path.join(root, "plugin");
|
|
@@ -294,7 +383,7 @@ test("bundlePlugin rejects module.exec path traversal", async () => {
|
|
|
294
383
|
writeText(path.join(pluginDir, "secret.js"), "console.log('secret')\n");
|
|
295
384
|
|
|
296
385
|
await assert.rejects(
|
|
297
|
-
() => bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true }),
|
|
386
|
+
() => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
|
|
298
387
|
/module\.exec/
|
|
299
388
|
);
|
|
300
389
|
|
|
@@ -313,7 +402,7 @@ test("bundlePlugin rejects non-node module.exec extension", async () => {
|
|
|
313
402
|
writeText(path.join(pluginDir, "bin", "plugin.ts"), "export {};\n");
|
|
314
403
|
|
|
315
404
|
await assert.rejects(
|
|
316
|
-
() => bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true }),
|
|
405
|
+
() => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
|
|
317
406
|
/must end with .js\/.mjs\/.cjs/
|
|
318
407
|
);
|
|
319
408
|
|
|
@@ -332,7 +421,7 @@ test("bundlePlugin rejects plugin id with path separators", async () => {
|
|
|
332
421
|
writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
|
|
333
422
|
|
|
334
423
|
await assert.rejects(
|
|
335
|
-
() => bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true }),
|
|
424
|
+
() => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
|
|
336
425
|
/must not contain path separators/
|
|
337
426
|
);
|
|
338
427
|
|
|
@@ -357,7 +446,7 @@ test("bundlePlugin rejects symlink in bin", async () => {
|
|
|
357
446
|
fs.symlinkSync(path.join(pluginDir, "bin", "target.js"), path.join(pluginDir, "bin", "link.js"));
|
|
358
447
|
|
|
359
448
|
await assert.rejects(
|
|
360
|
-
() => bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true }),
|
|
449
|
+
() => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
|
|
361
450
|
/symlink/
|
|
362
451
|
);
|
|
363
452
|
|
|
@@ -375,7 +464,13 @@ test("bundlePlugin output archive keeps plugin-id root directory", async () => {
|
|
|
375
464
|
});
|
|
376
465
|
writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
|
|
377
466
|
|
|
378
|
-
const outPath = await bundlePlugin({
|
|
467
|
+
const outPath = await bundlePlugin({
|
|
468
|
+
pluginDir,
|
|
469
|
+
outDir,
|
|
470
|
+
verbose: false,
|
|
471
|
+
noBuild: true,
|
|
472
|
+
noNpmDeps: true,
|
|
473
|
+
});
|
|
379
474
|
const entries = await readBundleEntries(outPath);
|
|
380
475
|
for (const key of entries.keys()) {
|
|
381
476
|
assert.equal(key.startsWith("root-check/"), true);
|
|
@@ -399,14 +494,20 @@ test("bundlePlugin overwrites existing bundle file", async () => {
|
|
|
399
494
|
const existingPath = path.join(outDir, "replace.ovcsp");
|
|
400
495
|
writeText(existingPath, "not-a-tar");
|
|
401
496
|
|
|
402
|
-
const outPath = await bundlePlugin({
|
|
497
|
+
const outPath = await bundlePlugin({
|
|
498
|
+
pluginDir,
|
|
499
|
+
outDir,
|
|
500
|
+
verbose: false,
|
|
501
|
+
noBuild: true,
|
|
502
|
+
noNpmDeps: true,
|
|
503
|
+
});
|
|
403
504
|
const entries = await readBundleEntries(outPath);
|
|
404
505
|
assert.equal(entries.has("replace/openvcs.plugin.json"), true);
|
|
405
506
|
|
|
406
507
|
cleanupTempDir(root);
|
|
407
508
|
});
|
|
408
509
|
|
|
409
|
-
test("bundlePlugin generates package-lock
|
|
510
|
+
test("bundlePlugin generates package-lock in staging, not pluginDir", async () => {
|
|
410
511
|
const root = makeTempDir("openvcs-sdk-test");
|
|
411
512
|
const pluginDir = path.join(root, "plugin");
|
|
412
513
|
const outDir = path.join(root, "out");
|
|
@@ -422,10 +523,16 @@ test("bundlePlugin generates package-lock when package.json exists", async () =>
|
|
|
422
523
|
private: true,
|
|
423
524
|
});
|
|
424
525
|
|
|
425
|
-
const outPath = await bundlePlugin({
|
|
526
|
+
const outPath = await bundlePlugin({
|
|
527
|
+
pluginDir,
|
|
528
|
+
outDir,
|
|
529
|
+
verbose: false,
|
|
530
|
+
noBuild: true,
|
|
531
|
+
noNpmDeps: false,
|
|
532
|
+
});
|
|
426
533
|
const entries = await readBundleEntries(outPath);
|
|
427
534
|
|
|
428
|
-
assert.equal(fs.existsSync(path.join(pluginDir, "package-lock.json")),
|
|
535
|
+
assert.equal(fs.existsSync(path.join(pluginDir, "package-lock.json")), false);
|
|
429
536
|
assert.equal(entries.has("npm-plugin/package.json"), true);
|
|
430
537
|
assert.equal(entries.has("npm-plugin/package-lock.json"), true);
|
|
431
538
|
|
|
@@ -448,9 +555,126 @@ test("bundlePlugin with --no-npm-deps does not generate lockfile", async () => {
|
|
|
448
555
|
private: true,
|
|
449
556
|
});
|
|
450
557
|
|
|
451
|
-
await bundlePlugin({ pluginDir, outDir, verbose: false, noNpmDeps: true });
|
|
558
|
+
await bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true });
|
|
452
559
|
|
|
453
560
|
assert.equal(fs.existsSync(path.join(pluginDir, "package-lock.json")), false);
|
|
454
561
|
|
|
455
562
|
cleanupTempDir(root);
|
|
456
563
|
});
|
|
564
|
+
|
|
565
|
+
test("bundlePlugin bundles manifest entry file with sibling assets", async () => {
|
|
566
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
567
|
+
const pluginDir = path.join(root, "plugin");
|
|
568
|
+
const outDir = path.join(root, "out");
|
|
569
|
+
|
|
570
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
571
|
+
id: "ui-plugin",
|
|
572
|
+
entry: "ui/index.html",
|
|
573
|
+
});
|
|
574
|
+
writeText(path.join(pluginDir, "ui", "index.html"), "<html></html>\n");
|
|
575
|
+
writeText(path.join(pluginDir, "ui", "app.js"), "console.log('ui');\n");
|
|
576
|
+
writeText(path.join(pluginDir, "ui", "styles.css"), "body {}\n");
|
|
577
|
+
writeText(path.join(pluginDir, "icon.png"), "icon-bytes");
|
|
578
|
+
|
|
579
|
+
const outPath = await bundlePlugin({
|
|
580
|
+
pluginDir,
|
|
581
|
+
outDir,
|
|
582
|
+
verbose: false,
|
|
583
|
+
noBuild: true,
|
|
584
|
+
noNpmDeps: true,
|
|
585
|
+
});
|
|
586
|
+
const entries = await readBundleEntries(outPath);
|
|
587
|
+
|
|
588
|
+
assert.equal(entries.has("ui-plugin/openvcs.plugin.json"), true);
|
|
589
|
+
assert.equal(entries.has("ui-plugin/ui/index.html"), true);
|
|
590
|
+
assert.equal(entries.has("ui-plugin/ui/app.js"), true);
|
|
591
|
+
assert.equal(entries.has("ui-plugin/ui/styles.css"), true);
|
|
592
|
+
assert.equal(entries.has("ui-plugin/icon.png"), true);
|
|
593
|
+
|
|
594
|
+
cleanupTempDir(root);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test("bundlePlugin bundles root-level entry with sibling assets", async () => {
|
|
598
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
599
|
+
const pluginDir = path.join(root, "plugin");
|
|
600
|
+
const outDir = path.join(root, "out");
|
|
601
|
+
|
|
602
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
603
|
+
id: "root-ui",
|
|
604
|
+
entry: "index.html",
|
|
605
|
+
});
|
|
606
|
+
writeText(path.join(pluginDir, "index.html"), "<html></html>\n");
|
|
607
|
+
writeText(path.join(pluginDir, "app.js"), "console.log('ui');\n");
|
|
608
|
+
writeText(path.join(pluginDir, "styles.css"), "body {}\n");
|
|
609
|
+
|
|
610
|
+
const outPath = await bundlePlugin({
|
|
611
|
+
pluginDir,
|
|
612
|
+
outDir,
|
|
613
|
+
verbose: false,
|
|
614
|
+
noBuild: true,
|
|
615
|
+
noNpmDeps: true,
|
|
616
|
+
});
|
|
617
|
+
const entries = await readBundleEntries(outPath);
|
|
618
|
+
|
|
619
|
+
assert.equal(entries.has("root-ui/openvcs.plugin.json"), true);
|
|
620
|
+
assert.equal(entries.has("root-ui/index.html"), true);
|
|
621
|
+
assert.equal(entries.has("root-ui/app.js"), true);
|
|
622
|
+
assert.equal(entries.has("root-ui/styles.css"), true);
|
|
623
|
+
|
|
624
|
+
cleanupTempDir(root);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("bundlePlugin rejects manifest entry path traversal", async () => {
|
|
628
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
629
|
+
const pluginDir = path.join(root, "plugin");
|
|
630
|
+
const outDir = path.join(root, "out");
|
|
631
|
+
|
|
632
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
633
|
+
id: "bad-entry",
|
|
634
|
+
entry: "../secret.txt",
|
|
635
|
+
});
|
|
636
|
+
writeText(path.join(pluginDir, "secret.txt"), "secret");
|
|
637
|
+
|
|
638
|
+
await assert.rejects(
|
|
639
|
+
() => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
|
|
640
|
+
/manifest entry must point to a file under the plugin directory/
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
cleanupTempDir(root);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test("bundlePlugin rejects manifest entry absolute path", async () => {
|
|
647
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
648
|
+
const pluginDir = path.join(root, "plugin");
|
|
649
|
+
const outDir = path.join(root, "out");
|
|
650
|
+
|
|
651
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
652
|
+
id: "abs-entry",
|
|
653
|
+
entry: "/tmp/secret.txt",
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
await assert.rejects(
|
|
657
|
+
() => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
|
|
658
|
+
/manifest entry must be a relative path/
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
cleanupTempDir(root);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test("bundlePlugin rejects missing manifest entry file", async () => {
|
|
665
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
666
|
+
const pluginDir = path.join(root, "plugin");
|
|
667
|
+
const outDir = path.join(root, "out");
|
|
668
|
+
|
|
669
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
670
|
+
id: "missing-entry",
|
|
671
|
+
entry: "nonexistent.html",
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
await assert.rejects(
|
|
675
|
+
() => bundlePlugin({ pluginDir, outDir, verbose: false, noBuild: true, noNpmDeps: true }),
|
|
676
|
+
/manifest entry file not found/
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
cleanupTempDir(root);
|
|
680
|
+
});
|
package/test/init.test.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
const assert = require("node:assert/strict");
|
|
2
|
+
const fs = require("node:fs");
|
|
2
3
|
const test = require("node:test");
|
|
3
4
|
const path = require("node:path");
|
|
4
5
|
|
|
5
6
|
const { __private } = require("../lib/init");
|
|
7
|
+
const { cleanupTempDir, makeTempDir } = require("./helpers");
|
|
6
8
|
|
|
7
9
|
test("validatePluginId accepts regular ids", () => {
|
|
8
10
|
assert.equal(__private.validatePluginId("my.plugin"), undefined);
|
|
@@ -63,3 +65,26 @@ test("collectAnswers re-prompts invalid plugin id", async () => {
|
|
|
63
65
|
assert.equal(answers.runNpmInstall, false);
|
|
64
66
|
assert.equal(messages.some((message) => message.includes("must not contain path separators")), true);
|
|
65
67
|
});
|
|
68
|
+
|
|
69
|
+
test("writeModuleTemplate scaffolds SDK runtime entrypoint", () => {
|
|
70
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
71
|
+
const targetDir = path.join(root, "plugin");
|
|
72
|
+
|
|
73
|
+
__private.writeModuleTemplate({
|
|
74
|
+
targetDir,
|
|
75
|
+
kind: "module",
|
|
76
|
+
pluginId: "example.plugin",
|
|
77
|
+
pluginName: "Example Plugin",
|
|
78
|
+
pluginVersion: "0.1.0",
|
|
79
|
+
defaultEnabled: true,
|
|
80
|
+
runNpmInstall: false,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const pluginSource = fs.readFileSync(path.join(targetDir, "src", "plugin.ts"), "utf8");
|
|
84
|
+
|
|
85
|
+
assert.match(pluginSource, /createPluginRuntime/);
|
|
86
|
+
assert.match(pluginSource, /startPluginRuntime/);
|
|
87
|
+
assert.match(pluginSource, /context\.host\.info\('OpenVCS plugin started'\)/);
|
|
88
|
+
|
|
89
|
+
cleanupTempDir(root);
|
|
90
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const { EventEmitter } = require("node:events");
|
|
3
|
+
const test = require("node:test");
|
|
4
|
+
|
|
5
|
+
const { createPluginRuntime } = require("../lib/runtime");
|
|
6
|
+
const {
|
|
7
|
+
parseFramedMessages,
|
|
8
|
+
serializeFramedMessage,
|
|
9
|
+
} = require("../lib/runtime/transport");
|
|
10
|
+
|
|
11
|
+
function createRuntimeHarness(options) {
|
|
12
|
+
const stdin = new EventEmitter();
|
|
13
|
+
const chunks = [];
|
|
14
|
+
const stdout = {
|
|
15
|
+
write(chunk) {
|
|
16
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
17
|
+
return true;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
const runtime = createPluginRuntime(options);
|
|
21
|
+
runtime.start({ stdin, stdout });
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
async request(message) {
|
|
25
|
+
stdin.emit("data", serializeFramedMessage(message));
|
|
26
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
27
|
+
const parsed = parseFramedMessages(Buffer.concat(chunks));
|
|
28
|
+
chunks.length = 0;
|
|
29
|
+
return parsed.messages;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test("createPluginRuntime answers plugin.initialize with inferred capabilities", async () => {
|
|
35
|
+
const harness = createRuntimeHarness({});
|
|
36
|
+
|
|
37
|
+
const messages = await harness.request({
|
|
38
|
+
jsonrpc: "2.0",
|
|
39
|
+
id: 1,
|
|
40
|
+
method: "plugin.initialize",
|
|
41
|
+
params: {},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
assert.deepEqual(messages, [
|
|
45
|
+
{
|
|
46
|
+
jsonrpc: "2.0",
|
|
47
|
+
id: 1,
|
|
48
|
+
result: {
|
|
49
|
+
protocol_version: 1,
|
|
50
|
+
implements: {
|
|
51
|
+
plugin: true,
|
|
52
|
+
vcs: false,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("createPluginRuntime uses plugin defaults and host notifications", async () => {
|
|
60
|
+
const harness = createRuntimeHarness({
|
|
61
|
+
plugin: {
|
|
62
|
+
async "plugin.init"(_params, context) {
|
|
63
|
+
context.host.info("ready");
|
|
64
|
+
return null;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const messages = await harness.request({
|
|
70
|
+
jsonrpc: "2.0",
|
|
71
|
+
id: 2,
|
|
72
|
+
method: "plugin.init",
|
|
73
|
+
params: {},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
assert.deepEqual(messages, [
|
|
77
|
+
{
|
|
78
|
+
jsonrpc: "2.0",
|
|
79
|
+
method: "host.log",
|
|
80
|
+
params: {
|
|
81
|
+
level: "info",
|
|
82
|
+
target: "openvcs.plugin",
|
|
83
|
+
message: "ready",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
jsonrpc: "2.0",
|
|
88
|
+
id: 2,
|
|
89
|
+
result: null,
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("createPluginRuntime reports missing methods as plugin failures", async () => {
|
|
95
|
+
const harness = createRuntimeHarness({});
|
|
96
|
+
|
|
97
|
+
const messages = await harness.request({
|
|
98
|
+
jsonrpc: "2.0",
|
|
99
|
+
id: 3,
|
|
100
|
+
method: "vcs.get_caps",
|
|
101
|
+
params: {},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
assert.deepEqual(messages, [
|
|
105
|
+
{
|
|
106
|
+
jsonrpc: "2.0",
|
|
107
|
+
id: 3,
|
|
108
|
+
error: {
|
|
109
|
+
code: -32001,
|
|
110
|
+
message: "method 'vcs.get_caps' is not implemented",
|
|
111
|
+
data: {
|
|
112
|
+
code: "rpc-method-not-found",
|
|
113
|
+
message: "method 'vcs.get_caps' is not implemented",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
]);
|
|
118
|
+
});
|