@droposs/plugin-cli 0.6.1 → 0.7.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/bin/drop-plugin.js +29 -5
- package/dist/builder.d.ts +11 -2
- package/dist/builder.js +81 -7
- package/dist/devServer.d.ts +15 -0
- package/dist/devServer.js +94 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/scaffolder.d.ts +10 -0
- package/dist/scaffolder.js +159 -5
- package/dist/signer.d.ts +8 -1
- package/dist/signer.js +81 -2
- package/package.json +4 -2
package/bin/drop-plugin.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
initPlugin,
|
|
8
8
|
validateManifest,
|
|
9
9
|
verifyPlugin,
|
|
10
|
+
startDevServer,
|
|
10
11
|
} from "../dist/index.js";
|
|
11
12
|
import { readFile } from "node:fs/promises";
|
|
12
13
|
import path from "node:path";
|
|
@@ -52,7 +53,11 @@ function printUsage() {
|
|
|
52
53
|
console.log(`Drop Plugin CLI (drop-plugin)
|
|
53
54
|
|
|
54
55
|
Usage:
|
|
55
|
-
drop-plugin init [dir] Initialize a new plugin from
|
|
56
|
+
drop-plugin init [dir] Initialize a new plugin from template
|
|
57
|
+
(--template <starter|client-ui|metadata|store|runner|fullstack>)
|
|
58
|
+
(--id <id> --name <name> --author <author>)
|
|
59
|
+
drop-plugin dev [dir] Start hot-reloading dev server for live Drop testing
|
|
60
|
+
(--port <number>)
|
|
56
61
|
drop-plugin build [dir] Bundle server/client entry points with esbuild and sign
|
|
57
62
|
(--out-manifest <path> keeps the source manifest clean)
|
|
58
63
|
drop-plugin sign [dir] Calculate SHA-256 digests and sign drop-plugin.json
|
|
@@ -85,7 +90,7 @@ async function main() {
|
|
|
85
90
|
switch (command) {
|
|
86
91
|
case "sign": {
|
|
87
92
|
const outManifest = readFlagValue(args, "--out-manifest");
|
|
88
|
-
const dir = positionalArg(args, [outManifest]) || ".";
|
|
93
|
+
const dir = positionalArg(args, [outManifest].filter(Boolean)) || ".";
|
|
89
94
|
const res = await signPlugin(dir, undefined, true, { outManifest });
|
|
90
95
|
console.log(
|
|
91
96
|
`Signed bundle at ${dir}: ${res.fileCount} files verified (signature: ${res.signed ? "yes" : "no"})` +
|
|
@@ -104,21 +109,40 @@ async function main() {
|
|
|
104
109
|
}
|
|
105
110
|
case "build": {
|
|
106
111
|
const outManifest = readFlagValue(args, "--out-manifest");
|
|
107
|
-
const dir = positionalArg(args, [outManifest]) || ".";
|
|
112
|
+
const dir = positionalArg(args, [outManifest].filter(Boolean)) || ".";
|
|
108
113
|
const res = await buildPlugin(dir, { outManifest });
|
|
109
114
|
console.log(
|
|
110
115
|
`Built plugin at ${dir} (server: ${res.serverBuilt ? "yes" : "no"}, client: ${res.clientBuilt ? "yes" : "no"})`,
|
|
111
116
|
);
|
|
112
117
|
break;
|
|
113
118
|
}
|
|
119
|
+
case "dev": {
|
|
120
|
+
const portStr = readFlagValue(args, "--port");
|
|
121
|
+
const port = portStr ? parseInt(portStr, 10) : undefined;
|
|
122
|
+
const dir = positionalArg(args, [portStr].filter(Boolean)) || ".";
|
|
123
|
+
const server = await startDevServer(dir, { port });
|
|
124
|
+
console.log(`Drop plugin dev server running at ${server.url}`);
|
|
125
|
+
console.log(`Serving manifest: ${server.manifestUrl}`);
|
|
126
|
+
console.log(`Press Ctrl+C to stop.`);
|
|
127
|
+
await new Promise(() => {});
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
114
130
|
case "test": {
|
|
115
131
|
const dir = args[0] || ".";
|
|
116
132
|
await testPlugin(dir);
|
|
117
133
|
break;
|
|
118
134
|
}
|
|
119
135
|
case "init": {
|
|
120
|
-
const
|
|
121
|
-
const
|
|
136
|
+
const template = readFlagValue(args, "--template");
|
|
137
|
+
const id = readFlagValue(args, "--id");
|
|
138
|
+
const name = readFlagValue(args, "--name");
|
|
139
|
+
const author = readFlagValue(args, "--author");
|
|
140
|
+
const dir =
|
|
141
|
+
positionalArg(
|
|
142
|
+
args,
|
|
143
|
+
[template, id, name, author].filter(Boolean),
|
|
144
|
+
) || (id ? id : "my-drop-plugin");
|
|
145
|
+
const res = await initPlugin(dir, { template, id, name, author });
|
|
122
146
|
console.log(
|
|
123
147
|
`Initialized new Drop plugin '${res.id}' at ${res.targetPath}`,
|
|
124
148
|
);
|
package/dist/builder.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as esbuild from "esbuild";
|
|
1
2
|
export interface BuildOptions {
|
|
2
3
|
minify?: boolean;
|
|
3
4
|
sourcemap?: boolean;
|
|
@@ -5,8 +6,16 @@ export interface BuildOptions {
|
|
|
5
6
|
signingKey?: string;
|
|
6
7
|
/** Write derived manifest fields here instead of the source manifest. */
|
|
7
8
|
outManifest?: string;
|
|
9
|
+
/** Watch for changes and rebuild incrementally. */
|
|
10
|
+
watch?: boolean;
|
|
11
|
+
/** Callback invoked on incremental rebuild completion (in watch mode). */
|
|
12
|
+
onRebuild?: (result: {
|
|
13
|
+
error?: Error;
|
|
14
|
+
}) => void;
|
|
8
15
|
}
|
|
9
|
-
export
|
|
16
|
+
export interface BuildResult {
|
|
10
17
|
serverBuilt: boolean;
|
|
11
18
|
clientBuilt: boolean;
|
|
12
|
-
|
|
19
|
+
contexts?: esbuild.BuildContext[];
|
|
20
|
+
}
|
|
21
|
+
export declare function buildPlugin(targetDir?: string, options?: BuildOptions): Promise<BuildResult>;
|
package/dist/builder.js
CHANGED
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
import * as esbuild from "esbuild";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { readFile, writeFile, stat } from "node:fs/promises";
|
|
4
|
+
import vue from "unplugin-vue/esbuild";
|
|
4
5
|
import { signPlugin } from "./signer.js";
|
|
6
|
+
/**
|
|
7
|
+
* Global shim so bundled Vue SFCs and imports from "vue" resolve to globalThis.Vue (set by Drop Desktop)
|
|
8
|
+
* at runtime without requiring browser import maps.
|
|
9
|
+
*/
|
|
10
|
+
const vueGlobalShimPlugin = {
|
|
11
|
+
name: "vue-global-shim",
|
|
12
|
+
setup(build) {
|
|
13
|
+
build.onResolve({ filter: /^vue$/ }, () => {
|
|
14
|
+
return { path: "virtual:vue", namespace: "vue-shim" };
|
|
15
|
+
});
|
|
16
|
+
build.onLoad({ filter: /.*/, namespace: "vue-shim" }, () => {
|
|
17
|
+
return {
|
|
18
|
+
contents: `
|
|
19
|
+
const v = globalThis.Vue || (typeof window !== "undefined" ? window.Vue : {});
|
|
20
|
+
export default v;
|
|
21
|
+
export const {
|
|
22
|
+
ref, reactive, computed, watch, watchEffect,
|
|
23
|
+
onMounted, onUnmounted, onUpdated, onBeforeMount, onBeforeUnmount,
|
|
24
|
+
h, defineComponent, nextTick, shallowRef, shallowReactive,
|
|
25
|
+
toRef, toRefs, isRef, isReactive, unref,
|
|
26
|
+
openBlock, createElementBlock, createBlock, createVNode, createCommentVNode,
|
|
27
|
+
createTextVNode, createElementVNode, toDisplayString, withDirectives,
|
|
28
|
+
vShow, vModelText, vModelCheckbox, vModelRadio, vModelSelect,
|
|
29
|
+
Fragment, Static, Comment, Text, Teleport, Suspense, KeepAlive,
|
|
30
|
+
renderSlot, resolveComponent, resolveDirective, normalizeClass, normalizeStyle,
|
|
31
|
+
withCtx, withModifiers, withKeys, renderList, pushScopeId, popScopeId
|
|
32
|
+
} = new Proxy(v, {
|
|
33
|
+
get: (target, prop) => {
|
|
34
|
+
const root = globalThis.Vue || (typeof window !== "undefined" ? window.Vue : undefined);
|
|
35
|
+
return root ? root[prop] : target[prop];
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
`,
|
|
39
|
+
loader: "js",
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
5
44
|
export async function buildPlugin(targetDir = ".", options = {}) {
|
|
6
45
|
const dir = path.resolve(process.cwd(), targetDir);
|
|
7
46
|
const manifestPath = path.join(dir, "drop-plugin.json");
|
|
8
47
|
const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
|
|
9
48
|
let serverBuilt = false;
|
|
10
49
|
let clientBuilt = false;
|
|
50
|
+
const contexts = [];
|
|
11
51
|
// 1. Build Server Entry if source exists
|
|
12
52
|
const serverSourceCandidates = [
|
|
13
53
|
manifest.server?.source,
|
|
@@ -25,7 +65,7 @@ export async function buildPlugin(targetDir = ".", options = {}) {
|
|
|
25
65
|
}
|
|
26
66
|
const serverOutFile = path.resolve(dir, manifest.server?.entry ?? manifest.entry ?? "dist/src/index.js");
|
|
27
67
|
if (serverEntrySource) {
|
|
28
|
-
|
|
68
|
+
const serverConfig = {
|
|
29
69
|
entryPoints: [serverEntrySource],
|
|
30
70
|
outfile: serverOutFile,
|
|
31
71
|
bundle: true,
|
|
@@ -41,7 +81,15 @@ export async function buildPlugin(targetDir = ".", options = {}) {
|
|
|
41
81
|
"h3",
|
|
42
82
|
"pino",
|
|
43
83
|
],
|
|
44
|
-
}
|
|
84
|
+
};
|
|
85
|
+
if (options.watch) {
|
|
86
|
+
const serverCtx = await esbuild.context(serverConfig);
|
|
87
|
+
await serverCtx.watch();
|
|
88
|
+
contexts.push(serverCtx);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
await esbuild.build(serverConfig);
|
|
92
|
+
}
|
|
45
93
|
serverBuilt = true;
|
|
46
94
|
}
|
|
47
95
|
// 2. Build Client Entry if source exists
|
|
@@ -60,7 +108,14 @@ export async function buildPlugin(targetDir = ".", options = {}) {
|
|
|
60
108
|
}
|
|
61
109
|
const clientOutFile = path.resolve(dir, manifest.client?.entry ?? manifest.clientEntry ?? "dist/src/client.js");
|
|
62
110
|
if (clientEntrySource) {
|
|
63
|
-
|
|
111
|
+
const clientPlugins = [
|
|
112
|
+
vueGlobalShimPlugin,
|
|
113
|
+
vue({
|
|
114
|
+
isProduction: true,
|
|
115
|
+
sourceMap: false,
|
|
116
|
+
}),
|
|
117
|
+
];
|
|
118
|
+
const clientConfig = {
|
|
64
119
|
entryPoints: [clientEntrySource],
|
|
65
120
|
outfile: clientOutFile,
|
|
66
121
|
bundle: true,
|
|
@@ -69,14 +124,33 @@ export async function buildPlugin(targetDir = ".", options = {}) {
|
|
|
69
124
|
format: "esm",
|
|
70
125
|
sourcemap: options.sourcemap ?? true,
|
|
71
126
|
minify: options.minify ?? false,
|
|
127
|
+
plugins: clientPlugins,
|
|
72
128
|
external: [
|
|
73
|
-
"vue",
|
|
74
129
|
"@droposs/plugin-sdk",
|
|
75
130
|
"@droposs/plugin-sdk",
|
|
76
131
|
"@drop/plugin-sdk",
|
|
77
132
|
],
|
|
78
|
-
}
|
|
133
|
+
};
|
|
134
|
+
if (options.watch) {
|
|
135
|
+
const clientCtx = await esbuild.context(clientConfig);
|
|
136
|
+
await clientCtx.watch();
|
|
137
|
+
contexts.push(clientCtx);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
await esbuild.build(clientConfig);
|
|
141
|
+
}
|
|
79
142
|
clientBuilt = true;
|
|
143
|
+
// Check if CSS was emitted and update manifest.client.css if necessary
|
|
144
|
+
const clientCssCandidate = clientOutFile.replace(/\.[^.]+$/, ".css");
|
|
145
|
+
if (await stat(clientCssCandidate).catch(() => null)) {
|
|
146
|
+
const relCss = path
|
|
147
|
+
.relative(dir, clientCssCandidate)
|
|
148
|
+
.replaceAll("\\", "/");
|
|
149
|
+
if (manifest.client && manifest.client.css !== relCss) {
|
|
150
|
+
manifest.client.css = relCss;
|
|
151
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
80
154
|
}
|
|
81
155
|
// 3. Automatically re-sign the plugin bundle after building
|
|
82
156
|
if (options.sign !== false) {
|
|
@@ -84,5 +158,5 @@ export async function buildPlugin(targetDir = ".", options = {}) {
|
|
|
84
158
|
outManifest: options.outManifest,
|
|
85
159
|
});
|
|
86
160
|
}
|
|
87
|
-
return { serverBuilt, clientBuilt };
|
|
161
|
+
return { serverBuilt, clientBuilt, contexts: contexts.length > 0 ? contexts : undefined };
|
|
88
162
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
export interface DevServerOptions {
|
|
3
|
+
port?: number;
|
|
4
|
+
host?: string;
|
|
5
|
+
sign?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface DevServerInstance {
|
|
8
|
+
server: http.Server;
|
|
9
|
+
port: number;
|
|
10
|
+
url: string;
|
|
11
|
+
manifestUrl: string;
|
|
12
|
+
close: () => Promise<void>;
|
|
13
|
+
stop: () => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare function startDevServer(targetDir?: string, options?: DevServerOptions): Promise<DevServerInstance>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { buildPlugin } from "./builder.js";
|
|
5
|
+
const MIME_TYPES = {
|
|
6
|
+
".js": "application/javascript",
|
|
7
|
+
".mjs": "application/javascript",
|
|
8
|
+
".css": "text/css",
|
|
9
|
+
".json": "application/json",
|
|
10
|
+
".html": "text/html",
|
|
11
|
+
".png": "image/png",
|
|
12
|
+
".jpg": "image/jpeg",
|
|
13
|
+
".svg": "image/svg+xml",
|
|
14
|
+
};
|
|
15
|
+
export async function startDevServer(targetDir = ".", options = {}) {
|
|
16
|
+
const dir = path.resolve(process.cwd(), targetDir);
|
|
17
|
+
const manifestPath = path.join(dir, "drop-plugin.json");
|
|
18
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
|
|
19
|
+
const port = options.port ?? 4567;
|
|
20
|
+
const host = options.host ?? "localhost";
|
|
21
|
+
// 1. Start incremental watch build
|
|
22
|
+
const buildResult = await buildPlugin(dir, {
|
|
23
|
+
watch: true,
|
|
24
|
+
sign: options.sign ?? true,
|
|
25
|
+
});
|
|
26
|
+
// 2. Start HTTP static server with CORS
|
|
27
|
+
const server = http.createServer(async (req, res) => {
|
|
28
|
+
// CORS headers for Drop Desktop webview
|
|
29
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
30
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
|
31
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
32
|
+
if (req.method === "OPTIONS") {
|
|
33
|
+
res.writeHead(204);
|
|
34
|
+
res.end();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const urlPath = (req.url || "/").split("?")[0] || "/";
|
|
38
|
+
const filePath = path.join(dir, urlPath);
|
|
39
|
+
// Prevent directory traversal
|
|
40
|
+
const rel = path.relative(dir, filePath);
|
|
41
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
42
|
+
res.writeHead(403);
|
|
43
|
+
res.end("Forbidden");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const fileStat = await stat(filePath).catch(() => null);
|
|
47
|
+
if (!fileStat || fileStat.isDirectory()) {
|
|
48
|
+
res.writeHead(404);
|
|
49
|
+
res.end("File not found");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
53
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
54
|
+
try {
|
|
55
|
+
const content = await readFile(filePath);
|
|
56
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
57
|
+
res.end(content);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
res.writeHead(500);
|
|
61
|
+
res.end("Internal Server Error");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
await new Promise((resolve, reject) => {
|
|
65
|
+
server.listen(port, host, () => resolve());
|
|
66
|
+
server.on("error", reject);
|
|
67
|
+
});
|
|
68
|
+
const address = server.address();
|
|
69
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
70
|
+
const url = `http://${host}:${actualPort}`;
|
|
71
|
+
const manifestUrl = `${url}/drop-plugin.json`;
|
|
72
|
+
const clientEntry = manifest.client?.entry ?? "dist/src/client.js";
|
|
73
|
+
console.log(`[drop-plugin] Dev server running at ${url}/`);
|
|
74
|
+
console.log(`[drop-plugin] Client bundle URL: ${url}/${clientEntry}`);
|
|
75
|
+
console.log(`[drop-plugin] In Drop Desktop, test this plugin via Extension Settings -> Load Dev Plugin:`);
|
|
76
|
+
console.log(` ${url}/${clientEntry}`);
|
|
77
|
+
console.log(`[drop-plugin] Watching for file changes... (Press Ctrl+C to stop)`);
|
|
78
|
+
const close = async () => {
|
|
79
|
+
if (buildResult.contexts) {
|
|
80
|
+
for (const ctx of buildResult.contexts) {
|
|
81
|
+
await ctx.dispose();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
server,
|
|
88
|
+
port: actualPort,
|
|
89
|
+
url,
|
|
90
|
+
manifestUrl,
|
|
91
|
+
close,
|
|
92
|
+
stop: close,
|
|
93
|
+
};
|
|
94
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/scaffolder.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
export type TemplateType = "starter" | "client-ui" | "metadata" | "store" | "runner" | "fullstack";
|
|
1
2
|
export interface InitOptions {
|
|
2
3
|
id?: string;
|
|
3
4
|
name?: string;
|
|
4
5
|
author?: string;
|
|
6
|
+
template?: string;
|
|
5
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Rewrites a freshly copied template so its `@drop-oss` monorepo specifiers
|
|
10
|
+
* and `workspace:*` dependencies point at the scope/version declared in the
|
|
11
|
+
* template's `.sdk-scope.json`. Without this a scaffolded repo keeps
|
|
12
|
+
* `workspace:*` and cannot install outside the SDK monorepo.
|
|
13
|
+
*/
|
|
14
|
+
export declare function applySdkScope(targetPath: string): Promise<boolean>;
|
|
15
|
+
export declare function resolveTemplateFolder(templateName?: string): string;
|
|
6
16
|
export declare function initPlugin(targetDir: string, options?: InitOptions): Promise<{
|
|
7
17
|
targetPath: string;
|
|
8
18
|
id: string;
|
package/dist/scaffolder.js
CHANGED
|
@@ -1,12 +1,163 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { cp, mkdir, readFile, writeFile, stat } from "node:fs/promises";
|
|
2
|
+
import { cp, mkdir, readFile, writeFile, stat, readdir, realpath, } from "node:fs/promises";
|
|
3
|
+
/** npm scopes the templates may resolve the SDK/CLI from. */
|
|
4
|
+
const SDK_SCOPES = ["@drop-oss", "@droposs", "@drop"];
|
|
5
|
+
const SKIP_DIRS = new Set([
|
|
6
|
+
"node_modules",
|
|
7
|
+
".git",
|
|
8
|
+
"dist",
|
|
9
|
+
"dist-package",
|
|
10
|
+
"dist-packages",
|
|
11
|
+
]);
|
|
12
|
+
const TEXT_EXT = new Set([
|
|
13
|
+
".ts",
|
|
14
|
+
".tsx",
|
|
15
|
+
".js",
|
|
16
|
+
".mjs",
|
|
17
|
+
".cjs",
|
|
18
|
+
".json",
|
|
19
|
+
".md",
|
|
20
|
+
".yml",
|
|
21
|
+
".yaml",
|
|
22
|
+
]);
|
|
23
|
+
function parseSdkScope(text) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(text);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const { sdk, sdkVersion, cliVersion } = parsed;
|
|
32
|
+
if (typeof sdk !== "string" ||
|
|
33
|
+
typeof sdkVersion !== "string" ||
|
|
34
|
+
typeof cliVersion !== "string" ||
|
|
35
|
+
!SDK_SCOPES.includes(sdk)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return { sdk, sdkVersion, cliVersion };
|
|
39
|
+
}
|
|
40
|
+
function rewriteSpecifiers(text, scope) {
|
|
41
|
+
let out = text;
|
|
42
|
+
for (const other of SDK_SCOPES) {
|
|
43
|
+
if (other === scope)
|
|
44
|
+
continue;
|
|
45
|
+
out = out
|
|
46
|
+
.replaceAll(`${other}/plugin-sdk`, `${scope}/plugin-sdk`)
|
|
47
|
+
.replaceAll(`${other}/plugin-cli`, `${scope}/plugin-cli`);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function rewritePackageDeps(text, config) {
|
|
52
|
+
let pkg;
|
|
53
|
+
try {
|
|
54
|
+
pkg = JSON.parse(text);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
59
|
+
for (const sectionName of [
|
|
60
|
+
"dependencies",
|
|
61
|
+
"devDependencies",
|
|
62
|
+
"peerDependencies",
|
|
63
|
+
"optionalDependencies",
|
|
64
|
+
]) {
|
|
65
|
+
const section = pkg[sectionName];
|
|
66
|
+
if (!section)
|
|
67
|
+
continue;
|
|
68
|
+
for (const dep of Object.keys(section)) {
|
|
69
|
+
const kind = dep.endsWith("/plugin-sdk")
|
|
70
|
+
? "plugin-sdk"
|
|
71
|
+
: dep.endsWith("/plugin-cli")
|
|
72
|
+
? "plugin-cli"
|
|
73
|
+
: null;
|
|
74
|
+
if (!kind)
|
|
75
|
+
continue;
|
|
76
|
+
delete section[dep];
|
|
77
|
+
section[`${config.sdk}/${kind}`] =
|
|
78
|
+
kind === "plugin-sdk" ? config.sdkVersion : config.cliVersion;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
82
|
+
}
|
|
83
|
+
async function walkFiles(dir, files = [], visited = new Set()) {
|
|
84
|
+
const real = await realpath(dir).catch(() => dir);
|
|
85
|
+
if (visited.has(real))
|
|
86
|
+
return files;
|
|
87
|
+
visited.add(real);
|
|
88
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
89
|
+
if (SKIP_DIRS.has(entry.name))
|
|
90
|
+
continue;
|
|
91
|
+
if (entry.isSymbolicLink())
|
|
92
|
+
continue;
|
|
93
|
+
const full = path.join(dir, entry.name);
|
|
94
|
+
if (entry.isDirectory())
|
|
95
|
+
await walkFiles(full, files, visited);
|
|
96
|
+
else
|
|
97
|
+
files.push(full);
|
|
98
|
+
}
|
|
99
|
+
return files;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Rewrites a freshly copied template so its `@drop-oss` monorepo specifiers
|
|
103
|
+
* and `workspace:*` dependencies point at the scope/version declared in the
|
|
104
|
+
* template's `.sdk-scope.json`. Without this a scaffolded repo keeps
|
|
105
|
+
* `workspace:*` and cannot install outside the SDK monorepo.
|
|
106
|
+
*/
|
|
107
|
+
export async function applySdkScope(targetPath) {
|
|
108
|
+
const raw = await readFile(path.join(targetPath, ".sdk-scope.json"), "utf-8").catch(() => null);
|
|
109
|
+
if (raw === null)
|
|
110
|
+
return false;
|
|
111
|
+
const config = parseSdkScope(raw);
|
|
112
|
+
if (!config)
|
|
113
|
+
return false;
|
|
114
|
+
for (const file of await walkFiles(targetPath)) {
|
|
115
|
+
if (path.basename(file).match(/lock\.(json|yaml|lockb)$/))
|
|
116
|
+
continue;
|
|
117
|
+
if (!TEXT_EXT.has(path.extname(file)))
|
|
118
|
+
continue;
|
|
119
|
+
const before = await readFile(file, "utf-8");
|
|
120
|
+
let after = rewriteSpecifiers(before, config.sdk);
|
|
121
|
+
if (path.basename(file) === "package.json") {
|
|
122
|
+
after = rewritePackageDeps(after, config);
|
|
123
|
+
}
|
|
124
|
+
if (after !== before)
|
|
125
|
+
await writeFile(file, after);
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
export function resolveTemplateFolder(templateName = "starter") {
|
|
130
|
+
const norm = templateName.replace(/^template-/, "").toLowerCase();
|
|
131
|
+
switch (norm) {
|
|
132
|
+
case "client-ui":
|
|
133
|
+
case "ui":
|
|
134
|
+
case "client":
|
|
135
|
+
return "template-client-ui";
|
|
136
|
+
case "metadata":
|
|
137
|
+
return "template-metadata";
|
|
138
|
+
case "store":
|
|
139
|
+
return "template-store";
|
|
140
|
+
case "runner":
|
|
141
|
+
return "template-runner";
|
|
142
|
+
case "fullstack":
|
|
143
|
+
case "full":
|
|
144
|
+
return "template-fullstack";
|
|
145
|
+
case "starter":
|
|
146
|
+
case "default":
|
|
147
|
+
return "starter-plugin";
|
|
148
|
+
default:
|
|
149
|
+
throw new Error(`Unknown template "${templateName}". Available templates: starter, client-ui, metadata, store, runner, fullstack`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
3
152
|
export async function initPlugin(targetDir, options = {}) {
|
|
4
153
|
const targetPath = path.resolve(process.cwd(), targetDir);
|
|
5
154
|
await mkdir(targetPath, { recursive: true });
|
|
155
|
+
const folder = resolveTemplateFolder(options.template);
|
|
6
156
|
const candidates = [
|
|
7
|
-
path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../templates
|
|
8
|
-
path.resolve(path.dirname(new URL(import.meta.url).pathname), "
|
|
9
|
-
path.resolve(
|
|
157
|
+
path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../templates", folder),
|
|
158
|
+
path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../templates", folder),
|
|
159
|
+
path.resolve(path.dirname(new URL(import.meta.url).pathname), "../templates", folder),
|
|
160
|
+
path.resolve(process.cwd(), "templates", folder),
|
|
10
161
|
];
|
|
11
162
|
let templateDir = null;
|
|
12
163
|
for (const cand of candidates) {
|
|
@@ -16,7 +167,7 @@ export async function initPlugin(targetDir, options = {}) {
|
|
|
16
167
|
}
|
|
17
168
|
}
|
|
18
169
|
if (!templateDir) {
|
|
19
|
-
throw new Error(
|
|
170
|
+
throw new Error(`Plugin template directory for "${folder}" not found`);
|
|
20
171
|
}
|
|
21
172
|
await cp(templateDir, targetPath, {
|
|
22
173
|
recursive: true,
|
|
@@ -25,6 +176,9 @@ export async function initPlugin(targetDir, options = {}) {
|
|
|
25
176
|
return basename !== "node_modules" && basename !== "dist";
|
|
26
177
|
},
|
|
27
178
|
});
|
|
179
|
+
// Point the scaffolded repo at the configured npm scope/version instead of
|
|
180
|
+
// the monorepo's `workspace:*` links.
|
|
181
|
+
await applySdkScope(targetPath);
|
|
28
182
|
const pluginId = options.id ||
|
|
29
183
|
path
|
|
30
184
|
.basename(targetPath)
|
package/dist/signer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SIGNATURE_VERSION } from "@drop-oss/plugin-sdk";
|
|
1
|
+
import { SIGNATURE_VERSION, type PluginManifest } from "@drop-oss/plugin-sdk";
|
|
2
2
|
/** Current signature scheme; re-exported for backwards compatibility. */
|
|
3
3
|
export { SIGNATURE_VERSION };
|
|
4
4
|
/**
|
|
@@ -39,6 +39,13 @@ export interface VerifyResult {
|
|
|
39
39
|
export declare function verifyPlugin(targetDir: string, signingKey?: string, options?: {
|
|
40
40
|
allowUnsigned?: boolean;
|
|
41
41
|
}): Promise<VerifyResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Validate the `client.sidecars` declaration against the bundle contents:
|
|
44
|
+
* each target path must resolve inside the bundle, point to a present regular
|
|
45
|
+
* file covered by the `files` checksums, and cite a matching SHA-256 of the
|
|
46
|
+
* binary. Every sidecar name must be allowlisted in `client.commands`, and
|
|
47
|
+
*/
|
|
48
|
+
export declare function verifySidecars(bundleDir: string, manifest: PluginManifest & Record<string, unknown>, files: string[], present: Set<string>, errors: string[]): Promise<void>;
|
|
42
49
|
export declare function packPlugin(targetDir: string, outputDir?: string): Promise<{
|
|
43
50
|
packagePath: string;
|
|
44
51
|
id: string;
|
package/dist/signer.js
CHANGED
|
@@ -91,7 +91,6 @@ let cachedErrors = [];
|
|
|
91
91
|
export async function validateManifest(manifest) {
|
|
92
92
|
if (!cachedValidator) {
|
|
93
93
|
const schema = await loadSchema();
|
|
94
|
-
// @ts-ignore
|
|
95
94
|
const AjvClass = Ajv.default ?? Ajv;
|
|
96
95
|
const ajv = new AjvClass({ allErrors: true, strict: false });
|
|
97
96
|
const compiled = ajv.compile(schema);
|
|
@@ -189,6 +188,13 @@ export async function signPlugin(targetDir, signingKey, validate = true, options
|
|
|
189
188
|
if (!validation.valid) {
|
|
190
189
|
throw new Error(`Manifest validation failed against schema:\n ${validation.errors.join("\n ")}`);
|
|
191
190
|
}
|
|
191
|
+
const files = await listFiles(bundleDir);
|
|
192
|
+
const present = new Set(files);
|
|
193
|
+
const sidecarErrors = [];
|
|
194
|
+
await verifySidecars(bundleDir, manifest, files, present, sidecarErrors);
|
|
195
|
+
if (sidecarErrors.length > 0) {
|
|
196
|
+
throw new Error(`Sidecar validation failed:\n ${sidecarErrors.join("\n ")}`);
|
|
197
|
+
}
|
|
192
198
|
}
|
|
193
199
|
const derived = await deriveManifest(bundleDir, manifest, signingKey);
|
|
194
200
|
const outPath = options.outManifest
|
|
@@ -246,6 +252,7 @@ export async function verifyPlugin(targetDir, signingKey, options = {}) {
|
|
|
246
252
|
errors.push("bundle contains multiple code files but no 'files' checksums; refusing unverified imports");
|
|
247
253
|
}
|
|
248
254
|
const entry = manifest.entry ?? manifest.server?.entry ?? manifest.client?.entry;
|
|
255
|
+
await verifySidecars(bundleDir, manifest, files, present, errors);
|
|
249
256
|
if (entry) {
|
|
250
257
|
const entryPath = path.resolve(bundleDir, entry);
|
|
251
258
|
if (!isInside(bundleDir, entryPath)) {
|
|
@@ -320,6 +327,72 @@ async function resolveSignedPayload(bundleDir, manifest, filesAggregate, entry)
|
|
|
320
327
|
error: "legacy signature has neither file checksums nor an entry checksum",
|
|
321
328
|
};
|
|
322
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Validate the `client.sidecars` declaration against the bundle contents:
|
|
332
|
+
* each target path must resolve inside the bundle, point to a present regular
|
|
333
|
+
* file covered by the `files` checksums, and cite a matching SHA-256 of the
|
|
334
|
+
* binary. Every sidecar name must be allowlisted in `client.commands`, and
|
|
335
|
+
*/
|
|
336
|
+
export async function verifySidecars(bundleDir, manifest, files, present, errors) {
|
|
337
|
+
const sidecars = manifest.client?.sidecars;
|
|
338
|
+
if (sidecars === undefined)
|
|
339
|
+
return;
|
|
340
|
+
if (!Array.isArray(sidecars) || sidecars.length === 0) {
|
|
341
|
+
errors.push("client.sidecars must be a non-empty array when declared");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const commands = new Set(Array.isArray(manifest.client?.commands) ? manifest.client.commands : []);
|
|
345
|
+
for (const [idx, sidecar] of sidecars.entries()) {
|
|
346
|
+
const label = `client.sidecars[${idx}]`;
|
|
347
|
+
if (typeof sidecar !== "object" ||
|
|
348
|
+
sidecar === null ||
|
|
349
|
+
typeof sidecar.name !== "string" ||
|
|
350
|
+
!Array.isArray(sidecar.targets)) {
|
|
351
|
+
errors.push(`${label}: expected { name: string, targets: array }`);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const { name, targets } = sidecar;
|
|
355
|
+
if (!commands.has(name)) {
|
|
356
|
+
errors.push(`${label}: sidecar name '${name}' must be allowlisted in client.commands`);
|
|
357
|
+
}
|
|
358
|
+
const seenTargets = new Set();
|
|
359
|
+
for (const [tIdx, target] of targets.entries()) {
|
|
360
|
+
const tLabel = `${label}.targets[${tIdx}]`;
|
|
361
|
+
if (!target || typeof target !== "object") {
|
|
362
|
+
errors.push(`${tLabel}: expected target object`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (typeof target.os !== "string" || typeof target.arch !== "string") {
|
|
366
|
+
errors.push(`${tLabel}: expected string os and arch`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const key = `${target.os}-${target.arch}`;
|
|
370
|
+
if (seenTargets.has(key)) {
|
|
371
|
+
errors.push(`${tLabel}: duplicate target '${key}' (only one binary per os+arch for sidecar '${name}')`);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
seenTargets.add(key);
|
|
375
|
+
if (typeof target.path !== "string" ||
|
|
376
|
+
path.isAbsolute(target.path) ||
|
|
377
|
+
!isInside(bundleDir, path.resolve(bundleDir, target.path))) {
|
|
378
|
+
errors.push(`${tLabel}: path must be a bundle-relative path`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (isBundleCodeFile(target.path)) {
|
|
382
|
+
errors.push(`${tLabel}: 'sidecars' paths must not be JavaScript code files`);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (!present.has(target.path)) {
|
|
386
|
+
errors.push(`${tLabel}: declared sidecar file missing: ${target.path}`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const digest = sha256Hex(await readFile(path.join(bundleDir, target.path)));
|
|
390
|
+
if (target.sha256 !== digest) {
|
|
391
|
+
errors.push(`${tLabel}: sha256 mismatch for ${target.path} (declared ${target.sha256}, actual ${digest})`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
323
396
|
export async function packPlugin(targetDir, outputDir) {
|
|
324
397
|
const resolvedPath = path.resolve(process.cwd(), targetDir);
|
|
325
398
|
const bundleDir = await realpath(resolvedPath).catch(() => null);
|
|
@@ -332,6 +405,13 @@ export async function packPlugin(targetDir, outputDir) {
|
|
|
332
405
|
if (!validation.valid) {
|
|
333
406
|
throw new Error(`Manifest validation failed against schema:\n ${validation.errors.join("\n ")}`);
|
|
334
407
|
}
|
|
408
|
+
const files = await listFiles(bundleDir);
|
|
409
|
+
const present = new Set(files);
|
|
410
|
+
const sidecarErrors = [];
|
|
411
|
+
await verifySidecars(bundleDir, rawManifest, files, present, sidecarErrors);
|
|
412
|
+
if (sidecarErrors.length > 0) {
|
|
413
|
+
throw new Error(`Sidecar validation failed:\n ${sidecarErrors.join("\n ")}`);
|
|
414
|
+
}
|
|
335
415
|
// Derive the signed manifest in memory: packing must not dirty the source
|
|
336
416
|
// tree, the derived fields live in the archive only.
|
|
337
417
|
const { manifest } = await deriveManifest(bundleDir, rawManifest);
|
|
@@ -344,7 +424,6 @@ export async function packPlugin(targetDir, outputDir) {
|
|
|
344
424
|
? path.resolve(process.cwd(), outputDir)
|
|
345
425
|
: path.join(bundleDir, "dist-package");
|
|
346
426
|
await mkdir(outDir, { recursive: true });
|
|
347
|
-
const files = await listFiles(bundleDir);
|
|
348
427
|
const bundleMap = {};
|
|
349
428
|
for (const rel of files) {
|
|
350
429
|
const content = await readFile(path.join(bundleDir, rel));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@droposs/plugin-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Drop Plugin build, test, signing, and packaging CLI for Drop OSS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"ajv": "^8.20.0",
|
|
52
52
|
"esbuild": "^0.28.2",
|
|
53
|
-
"
|
|
53
|
+
"unplugin-vue": "^8.0.0",
|
|
54
|
+
"vue": "^3.5.42",
|
|
55
|
+
"@droposs/plugin-sdk": "0.7.0"
|
|
54
56
|
}
|
|
55
57
|
}
|