@openvcs/sdk 0.2.2 → 0.2.3
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 +46 -6
- 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.js +7 -5
- package/package.json +3 -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 +7 -5
- package/test/build.test.js +95 -0
- package/test/cli.test.js +37 -0
- package/test/dist.test.js +239 -15
package/lib/init.js
CHANGED
|
@@ -192,9 +192,10 @@ function writeModuleTemplate(answers) {
|
|
|
192
192
|
private: true,
|
|
193
193
|
type: "module",
|
|
194
194
|
scripts: {
|
|
195
|
-
"build:
|
|
196
|
-
build: "
|
|
197
|
-
|
|
195
|
+
"build:plugin": "tsc -p tsconfig.json",
|
|
196
|
+
build: "openvcs build",
|
|
197
|
+
dist: "openvcs dist --plugin-dir . --out dist",
|
|
198
|
+
test: "openvcs dist --plugin-dir . --out dist --no-build --no-npm-deps",
|
|
198
199
|
},
|
|
199
200
|
devDependencies: {
|
|
200
201
|
"@openvcs/sdk": `^${packageJson.version}`,
|
|
@@ -224,8 +225,9 @@ function writeThemeTemplate(answers) {
|
|
|
224
225
|
version: answers.pluginVersion,
|
|
225
226
|
private: true,
|
|
226
227
|
scripts: {
|
|
227
|
-
build: "openvcs
|
|
228
|
-
|
|
228
|
+
build: "openvcs build",
|
|
229
|
+
dist: "openvcs dist --plugin-dir . --out dist",
|
|
230
|
+
test: "openvcs dist --plugin-dir . --out dist --no-build --no-npm-deps",
|
|
229
231
|
},
|
|
230
232
|
devDependencies: {
|
|
231
233
|
"@openvcs/sdk": `^${packageJson.version}`,
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openvcs/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "OpenVCS SDK CLI for plugin scaffolding and .ovcsp tar.gz packaging",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
|
-
"homepage": "https://
|
|
6
|
+
"homepage": "https://openvcs.app/",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/Open-VCS/OpenVCS-SDK"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"openvcs": "node bin/openvcs.js"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^
|
|
28
|
+
"@types/node": "^25.3.3",
|
|
29
29
|
"typescript": "^5.8.2"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
package/src/lib/build.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Copyright © 2025-2026 OpenVCS Contributors
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
import { isPathInside } from "./fs-utils";
|
|
9
|
+
|
|
10
|
+
type UsageError = Error & { code?: string };
|
|
11
|
+
|
|
12
|
+
/** CLI arguments for `openvcs build`. */
|
|
13
|
+
export interface BuildArgs {
|
|
14
|
+
pluginDir: string;
|
|
15
|
+
verbose: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Trimmed manifest details used by build and dist flows. */
|
|
19
|
+
export interface ManifestInfo {
|
|
20
|
+
pluginId: string;
|
|
21
|
+
moduleExec: string | undefined;
|
|
22
|
+
entry: string | undefined;
|
|
23
|
+
manifestPath: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CommandResult {
|
|
27
|
+
status: number | null;
|
|
28
|
+
error?: Error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PackageScripts {
|
|
32
|
+
[scriptName: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Returns the npm executable name for the current platform. */
|
|
36
|
+
export function npmExecutable(): string {
|
|
37
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Formats help text for the build command. */
|
|
41
|
+
export function buildUsage(commandName = "openvcs"): string {
|
|
42
|
+
return `${commandName} build [args]\n\n --plugin-dir <path> Plugin repository root (contains openvcs.plugin.json)\n -V, --verbose Enable verbose output\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Parses `openvcs build` arguments. */
|
|
46
|
+
export function parseBuildArgs(args: string[]): BuildArgs {
|
|
47
|
+
let pluginDir = process.cwd();
|
|
48
|
+
let verbose = false;
|
|
49
|
+
|
|
50
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
51
|
+
const arg = args[index];
|
|
52
|
+
if (arg === "--plugin-dir") {
|
|
53
|
+
index += 1;
|
|
54
|
+
if (index >= args.length) {
|
|
55
|
+
throw new Error("missing value for --plugin-dir");
|
|
56
|
+
}
|
|
57
|
+
pluginDir = args[index] as string;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (arg === "-V" || arg === "--verbose") {
|
|
61
|
+
verbose = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (arg === "--help") {
|
|
65
|
+
const error = new Error(buildUsage()) as UsageError;
|
|
66
|
+
error.code = "USAGE";
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`unknown flag: ${arg}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
pluginDir: path.resolve(pluginDir),
|
|
74
|
+
verbose,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Reads and validates the plugin manifest. */
|
|
79
|
+
export function readManifest(pluginDir: string): ManifestInfo {
|
|
80
|
+
const manifestPath = path.join(pluginDir, "openvcs.plugin.json");
|
|
81
|
+
let manifestRaw: string;
|
|
82
|
+
let manifestFd: number | undefined;
|
|
83
|
+
let manifest: unknown;
|
|
84
|
+
try {
|
|
85
|
+
manifestFd = fs.openSync(manifestPath, "r");
|
|
86
|
+
const manifestStat = fs.fstatSync(manifestFd);
|
|
87
|
+
if (!manifestStat.isFile()) {
|
|
88
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
89
|
+
}
|
|
90
|
+
manifestRaw = fs.readFileSync(manifestFd, "utf8");
|
|
91
|
+
} catch (error: unknown) {
|
|
92
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
93
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
} finally {
|
|
97
|
+
if (typeof manifestFd === "number") {
|
|
98
|
+
fs.closeSync(manifestFd);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
manifest = JSON.parse(manifestRaw);
|
|
104
|
+
} catch (error: unknown) {
|
|
105
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
106
|
+
throw new Error(`parse ${manifestPath}: ${detail}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const pluginId =
|
|
110
|
+
typeof (manifest as { id?: unknown }).id === "string"
|
|
111
|
+
? ((manifest as { id: string }).id.trim() as string)
|
|
112
|
+
: "";
|
|
113
|
+
if (!pluginId) {
|
|
114
|
+
throw new Error(`manifest ${manifestPath} is missing a string 'id'`);
|
|
115
|
+
}
|
|
116
|
+
if (pluginId === "." || pluginId === ".." || pluginId.includes("/") || pluginId.includes("\\")) {
|
|
117
|
+
throw new Error(`manifest id must not contain path separators: ${pluginId}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const moduleValue = (manifest as { module?: { exec?: unknown } }).module;
|
|
121
|
+
const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
|
|
122
|
+
|
|
123
|
+
const entryValue = (manifest as { entry?: unknown }).entry;
|
|
124
|
+
const entry = typeof entryValue === "string" ? entryValue.trim() : undefined;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
pluginId,
|
|
128
|
+
moduleExec,
|
|
129
|
+
entry,
|
|
130
|
+
manifestPath,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Verifies that a declared module entry resolves to a real file under `bin/`. */
|
|
135
|
+
export function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void {
|
|
136
|
+
if (!moduleExec) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const normalizedExec = moduleExec.trim();
|
|
141
|
+
const lowered = normalizedExec.toLowerCase();
|
|
142
|
+
if (!lowered.endsWith(".js") && !lowered.endsWith(".mjs") && !lowered.endsWith(".cjs")) {
|
|
143
|
+
throw new Error(`manifest exec must end with .js/.mjs/.cjs (Node runtime): ${moduleExec}`);
|
|
144
|
+
}
|
|
145
|
+
if (path.isAbsolute(normalizedExec)) {
|
|
146
|
+
throw new Error(`manifest module.exec must be a relative path under bin/: ${moduleExec}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const binDir = path.resolve(pluginDir, "bin");
|
|
150
|
+
const targetPath = path.resolve(binDir, normalizedExec);
|
|
151
|
+
if (!isPathInside(binDir, targetPath) || targetPath === binDir) {
|
|
152
|
+
throw new Error(`manifest module.exec must point to a file under bin/: ${moduleExec}`);
|
|
153
|
+
}
|
|
154
|
+
if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
|
|
155
|
+
throw new Error(`module entrypoint not found at ${targetPath}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Returns whether the plugin repository has a `package.json`. */
|
|
160
|
+
export function hasPackageJson(pluginDir: string): boolean {
|
|
161
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
162
|
+
return fs.existsSync(packageJsonPath) && fs.lstatSync(packageJsonPath).isFile();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Runs a command in the given directory with optional verbose logging. */
|
|
166
|
+
export function runCommand(program: string, args: string[], cwd: string, verbose: boolean): void {
|
|
167
|
+
if (verbose) {
|
|
168
|
+
process.stderr.write(`Running command in ${cwd}: ${program} ${args.join(" ")}\n`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result = spawnSync(program, args, {
|
|
172
|
+
cwd,
|
|
173
|
+
stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
|
|
174
|
+
}) as CommandResult;
|
|
175
|
+
|
|
176
|
+
if (result.error) {
|
|
177
|
+
throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
|
|
178
|
+
}
|
|
179
|
+
if (result.status === 0) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Reads `package.json` scripts for the plugin, if present. */
|
|
187
|
+
function readPackageScripts(pluginDir: string): PackageScripts {
|
|
188
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
189
|
+
let packageData: unknown;
|
|
190
|
+
try {
|
|
191
|
+
packageData = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
192
|
+
} catch (error: unknown) {
|
|
193
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
194
|
+
throw new Error(`code plugins must include package.json: ${packageJsonPath}`);
|
|
195
|
+
}
|
|
196
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
197
|
+
throw new Error(`parse ${packageJsonPath}: ${detail}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const scripts = (packageData as { scripts?: unknown }).scripts;
|
|
201
|
+
if (typeof scripts !== "object" || scripts === null) {
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
return scripts as PackageScripts;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Builds a plugin's runtime assets when it declares a code module. */
|
|
208
|
+
export function buildPluginAssets(parsedArgs: BuildArgs): ManifestInfo {
|
|
209
|
+
const manifest = readManifest(parsedArgs.pluginDir);
|
|
210
|
+
if (!manifest.moduleExec) {
|
|
211
|
+
return manifest;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!hasPackageJson(parsedArgs.pluginDir)) {
|
|
215
|
+
throw new Error(`code plugins must include package.json: ${path.join(parsedArgs.pluginDir, "package.json")}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const scripts = readPackageScripts(parsedArgs.pluginDir);
|
|
219
|
+
const buildScript = scripts["build:plugin"];
|
|
220
|
+
if (typeof buildScript !== "string" || buildScript.trim() === "") {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`code plugins must define scripts[\"build:plugin\"] in ${path.join(parsedArgs.pluginDir, "package.json")}`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
runCommand(npmExecutable(), ["run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
|
|
227
|
+
validateDeclaredModuleExec(parsedArgs.pluginDir, manifest.moduleExec);
|
|
228
|
+
return manifest;
|
|
229
|
+
}
|
package/src/lib/cli.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildPluginAssets, buildUsage, parseBuildArgs } from "./build";
|
|
1
2
|
import { bundlePlugin, distUsage, parseDistArgs } from "./dist";
|
|
2
3
|
import { initUsage, runInitCommand } from "./init";
|
|
3
4
|
|
|
@@ -17,7 +18,7 @@ function hasCode(error: unknown, code: string): boolean {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function usage(): string {
|
|
20
|
-
return "Usage: openvcs <command> [options]\n\nCommands:\n dist [args] Package plugin into .ovcsp\n init [--theme] [dir] Interactively scaffold a plugin project\n -v, --version Show version information\n\nDist args:\n --plugin-dir <path> Plugin root containing openvcs.plugin.json\n --out <path> Output directory (default: ./dist)\n --no-npm-deps Skip npm dependency bundling\n -V, --verbose Verbose output\n";
|
|
21
|
+
return "Usage: openvcs <command> [options]\n\nCommands:\n build [args] Build plugin runtime assets\n dist [args] Package plugin into .ovcsp\n init [--theme] [dir] Interactively scaffold a plugin project\n -v, --version Show version information\n\nBuild args:\n --plugin-dir <path> Plugin root containing openvcs.plugin.json\n -V, --verbose Verbose output\n\nDist args:\n --plugin-dir <path> Plugin root containing openvcs.plugin.json\n --out <path> Output directory (default: ./dist)\n --no-build Skip plugin build before packaging\n --no-npm-deps Skip npm dependency bundling\n -V, --verbose Verbose output\n";
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export async function runCli(args: string[]): Promise<void> {
|
|
@@ -27,7 +28,7 @@ export async function runCli(args: string[]): Promise<void> {
|
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
if (args
|
|
31
|
+
if (args[0] === "-v" || args[0] === "--version") {
|
|
31
32
|
process.stdout.write(`openvcs ${packageJson.version}\n`);
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
@@ -56,6 +57,24 @@ export async function runCli(args: string[]): Promise<void> {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
if (command === "build") {
|
|
61
|
+
if (rest.includes("--help")) {
|
|
62
|
+
process.stdout.write(buildUsage());
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const parsed = parseBuildArgs(rest);
|
|
67
|
+
const manifest = buildPluginAssets(parsed);
|
|
68
|
+
process.stdout.write(`${manifest.pluginId}\n`);
|
|
69
|
+
return;
|
|
70
|
+
} catch (error: unknown) {
|
|
71
|
+
if (hasCode(error, "USAGE")) {
|
|
72
|
+
throw new Error(buildUsage());
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
if (command === "init") {
|
|
60
79
|
if (rest.includes("--help")) {
|
|
61
80
|
process.stdout.write(initUsage());
|
package/src/lib/dist.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
3
|
import tar = require("tar");
|
|
4
|
+
import {
|
|
5
|
+
buildPluginAssets,
|
|
6
|
+
hasPackageJson,
|
|
7
|
+
ManifestInfo,
|
|
8
|
+
npmExecutable,
|
|
9
|
+
readManifest,
|
|
10
|
+
runCommand,
|
|
11
|
+
validateDeclaredModuleExec,
|
|
12
|
+
} from "./build";
|
|
5
13
|
import {
|
|
6
14
|
copyDirectoryRecursiveStrict,
|
|
7
15
|
copyFileStrict,
|
|
@@ -19,25 +27,11 @@ interface DistArgs {
|
|
|
19
27
|
outDir: string;
|
|
20
28
|
verbose: boolean;
|
|
21
29
|
noNpmDeps: boolean;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
interface ManifestInfo {
|
|
25
|
-
pluginId: string;
|
|
26
|
-
moduleExec: string | undefined;
|
|
27
|
-
manifestPath: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface CommandResult {
|
|
31
|
-
status: number | null;
|
|
32
|
-
error?: Error;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function npmExecutable(): string {
|
|
36
|
-
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
30
|
+
noBuild: boolean;
|
|
37
31
|
}
|
|
38
32
|
|
|
39
33
|
export function distUsage(commandName = "openvcs"): string {
|
|
40
|
-
return `${commandName} dist [args]\n\n --plugin-dir <path> Plugin repository root (contains openvcs.plugin.json)\n --out <path> Output directory (default: ./dist)\n --no-npm-deps Disable npm dependency bundling (enabled by default)\n -V, --verbose Enable verbose output\n`;
|
|
34
|
+
return `${commandName} dist [args]\n\n --plugin-dir <path> Plugin repository root (contains openvcs.plugin.json)\n --out <path> Output directory (default: ./dist)\n --no-build Skip the plugin build step before packaging\n --no-npm-deps Disable npm dependency bundling (enabled by default)\n -V, --verbose Enable verbose output\n`;
|
|
41
35
|
}
|
|
42
36
|
|
|
43
37
|
export function parseDistArgs(args: string[]): DistArgs {
|
|
@@ -45,6 +39,7 @@ export function parseDistArgs(args: string[]): DistArgs {
|
|
|
45
39
|
let outDir = "dist";
|
|
46
40
|
let verbose = false;
|
|
47
41
|
let noNpmDeps = false;
|
|
42
|
+
let noBuild = false;
|
|
48
43
|
|
|
49
44
|
for (let index = 0; index < args.length; index += 1) {
|
|
50
45
|
const arg = args[index];
|
|
@@ -68,6 +63,10 @@ export function parseDistArgs(args: string[]): DistArgs {
|
|
|
68
63
|
noNpmDeps = true;
|
|
69
64
|
continue;
|
|
70
65
|
}
|
|
66
|
+
if (arg === "--no-build") {
|
|
67
|
+
noBuild = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
71
70
|
if (arg === "-V" || arg === "--verbose") {
|
|
72
71
|
verbose = true;
|
|
73
72
|
continue;
|
|
@@ -85,110 +84,34 @@ export function parseDistArgs(args: string[]): DistArgs {
|
|
|
85
84
|
outDir: path.resolve(outDir),
|
|
86
85
|
verbose,
|
|
87
86
|
noNpmDeps,
|
|
87
|
+
noBuild,
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
function
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
let manifest: unknown;
|
|
96
|
-
try {
|
|
97
|
-
manifestFd = fs.openSync(manifestPath, "r");
|
|
98
|
-
const manifestStat = fs.fstatSync(manifestFd);
|
|
99
|
-
if (!manifestStat.isFile()) {
|
|
100
|
-
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
101
|
-
}
|
|
102
|
-
manifestRaw = fs.readFileSync(manifestFd, "utf8");
|
|
103
|
-
} catch (error: unknown) {
|
|
104
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
105
|
-
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
106
|
-
}
|
|
107
|
-
throw error;
|
|
108
|
-
} finally {
|
|
109
|
-
if (typeof manifestFd === "number") {
|
|
110
|
-
fs.closeSync(manifestFd);
|
|
111
|
-
}
|
|
91
|
+
function validateManifestEntry(pluginDir: string, entry: string): void {
|
|
92
|
+
const normalized = entry.trim();
|
|
93
|
+
if (path.isAbsolute(normalized)) {
|
|
94
|
+
throw new Error(`manifest entry must be a relative path: ${entry}`);
|
|
112
95
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
118
|
-
throw new Error(`parse ${manifestPath}: ${detail}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const pluginId =
|
|
122
|
-
typeof (manifest as { id?: unknown }).id === "string"
|
|
123
|
-
? ((manifest as { id: string }).id.trim() as string)
|
|
124
|
-
: "";
|
|
125
|
-
if (!pluginId) {
|
|
126
|
-
throw new Error(`manifest ${manifestPath} is missing a string 'id'`);
|
|
127
|
-
}
|
|
128
|
-
if (pluginId === "." || pluginId === ".." || pluginId.includes("/") || pluginId.includes("\\")) {
|
|
129
|
-
throw new Error(`manifest id must not contain path separators: ${pluginId}`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const moduleValue = (manifest as { module?: { exec?: unknown } }).module;
|
|
133
|
-
const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
pluginId,
|
|
137
|
-
moduleExec,
|
|
138
|
-
manifestPath,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void {
|
|
143
|
-
if (!moduleExec) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const normalizedExec = moduleExec.trim();
|
|
148
|
-
const lowered = normalizedExec.toLowerCase();
|
|
149
|
-
if (!lowered.endsWith(".js") && !lowered.endsWith(".mjs") && !lowered.endsWith(".cjs")) {
|
|
150
|
-
throw new Error(`manifest exec must end with .js/.mjs/.cjs (Node runtime): ${moduleExec}`);
|
|
151
|
-
}
|
|
152
|
-
if (path.isAbsolute(normalizedExec)) {
|
|
153
|
-
throw new Error(`manifest module.exec must be a relative path under bin/: ${moduleExec}`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const binDir = path.resolve(pluginDir, "bin");
|
|
157
|
-
const targetPath = path.resolve(binDir, normalizedExec);
|
|
158
|
-
if (!isPathInside(binDir, targetPath) || targetPath === binDir) {
|
|
159
|
-
throw new Error(`manifest module.exec must point to a file under bin/: ${moduleExec}`);
|
|
96
|
+
const targetPath = path.resolve(pluginDir, normalized);
|
|
97
|
+
const pluginDirResolved = path.resolve(pluginDir);
|
|
98
|
+
if (!isPathInside(pluginDirResolved, targetPath) || targetPath === pluginDirResolved) {
|
|
99
|
+
throw new Error(`manifest entry must point to a file under the plugin directory: ${entry}`);
|
|
160
100
|
}
|
|
161
101
|
if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
|
|
162
|
-
throw new Error(`
|
|
102
|
+
throw new Error(`manifest entry file not found: ${entry}`);
|
|
163
103
|
}
|
|
164
104
|
}
|
|
165
105
|
|
|
166
|
-
function
|
|
167
|
-
const
|
|
168
|
-
|
|
106
|
+
function copyEntryDirectory(pluginDir: string, bundleDir: string, entry: string): void {
|
|
107
|
+
const normalized = entry.trim();
|
|
108
|
+
const entryDir = path.dirname(normalized);
|
|
109
|
+
const sourceDir = path.join(pluginDir, entryDir);
|
|
110
|
+
const destDir = path.join(bundleDir, entryDir);
|
|
111
|
+
copyDirectoryRecursiveStrict(sourceDir, destDir);
|
|
169
112
|
}
|
|
170
113
|
|
|
171
|
-
function
|
|
172
|
-
if (verbose) {
|
|
173
|
-
process.stderr.write(`Running command in ${cwd}: ${program} ${args.join(" ")}\n`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const result = spawnSync(program, args, {
|
|
177
|
-
cwd,
|
|
178
|
-
stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
|
|
179
|
-
}) as CommandResult;
|
|
180
|
-
|
|
181
|
-
if (result.error) {
|
|
182
|
-
throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
|
|
183
|
-
}
|
|
184
|
-
if (result.status === 0) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function ensurePackageLock(pluginDir: string, verbose: boolean): void {
|
|
114
|
+
function ensurePackageLock(pluginDir: string, bundleDir: string, verbose: boolean): void {
|
|
192
115
|
if (!hasPackageJson(pluginDir)) {
|
|
193
116
|
return;
|
|
194
117
|
}
|
|
@@ -197,13 +120,16 @@ function ensurePackageLock(pluginDir: string, verbose: boolean): void {
|
|
|
197
120
|
return;
|
|
198
121
|
}
|
|
199
122
|
|
|
123
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
124
|
+
copyFileStrict(packageJsonPath, path.join(bundleDir, "package.json"));
|
|
125
|
+
|
|
200
126
|
if (verbose) {
|
|
201
|
-
process.stderr.write(`Generating package-lock.json in
|
|
127
|
+
process.stderr.write(`Generating package-lock.json in staging\n`);
|
|
202
128
|
}
|
|
203
129
|
runCommand(
|
|
204
130
|
npmExecutable(),
|
|
205
131
|
["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
206
|
-
|
|
132
|
+
bundleDir,
|
|
207
133
|
verbose
|
|
208
134
|
);
|
|
209
135
|
}
|
|
@@ -215,12 +141,17 @@ function copyNpmFilesToStaging(pluginDir: string, bundleDir: string): void {
|
|
|
215
141
|
if (!fs.existsSync(packageJsonPath) || !fs.lstatSync(packageJsonPath).isFile()) {
|
|
216
142
|
throw new Error(`missing package.json at ${packageJsonPath}`);
|
|
217
143
|
}
|
|
218
|
-
if (!fs.existsSync(lockPath) || !fs.lstatSync(lockPath).isFile()) {
|
|
219
|
-
throw new Error(`missing package-lock.json at ${lockPath}`);
|
|
220
|
-
}
|
|
221
144
|
|
|
222
145
|
copyFileStrict(packageJsonPath, path.join(bundleDir, "package.json"));
|
|
223
|
-
|
|
146
|
+
|
|
147
|
+
const stagedLockPath = path.join(bundleDir, "package-lock.json");
|
|
148
|
+
if (fs.existsSync(stagedLockPath) && fs.lstatSync(stagedLockPath).isFile()) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (fs.existsSync(lockPath) && fs.lstatSync(lockPath).isFile()) {
|
|
153
|
+
copyFileStrict(lockPath, path.join(bundleDir, "package-lock.json"));
|
|
154
|
+
}
|
|
224
155
|
}
|
|
225
156
|
|
|
226
157
|
function rejectNativeAddonsRecursive(dirPath: string): void {
|
|
@@ -264,14 +195,21 @@ function installNpmDependencies(pluginDir: string, bundleDir: string, verbose: b
|
|
|
264
195
|
}
|
|
265
196
|
|
|
266
197
|
function copyIcon(pluginDir: string, bundleDir: string): void {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
198
|
+
const entries = fs.readdirSync(pluginDir, { withFileTypes: true });
|
|
199
|
+
const iconEntries = entries.filter((e) => {
|
|
200
|
+
if (!e.isFile()) return false;
|
|
201
|
+
const name = e.name.toLowerCase();
|
|
202
|
+
return name.startsWith("icon.") && ICON_EXTENSIONS.includes(name.slice(5));
|
|
203
|
+
});
|
|
204
|
+
for (const ext of ICON_EXTENSIONS) {
|
|
205
|
+
const found = iconEntries.find((e) => e.name.toLowerCase() === `icon.${ext}`);
|
|
206
|
+
if (found) {
|
|
207
|
+
copyFileStrict(
|
|
208
|
+
path.join(pluginDir, found.name),
|
|
209
|
+
path.join(bundleDir, found.name)
|
|
210
|
+
);
|
|
211
|
+
return;
|
|
272
212
|
}
|
|
273
|
-
copyFileStrict(sourcePath, path.join(bundleDir, fileName));
|
|
274
|
-
return;
|
|
275
213
|
}
|
|
276
214
|
}
|
|
277
215
|
|
|
@@ -297,16 +235,21 @@ async function writeTarGz(outPath: string, baseDir: string, folderName: string):
|
|
|
297
235
|
}
|
|
298
236
|
|
|
299
237
|
export async function bundlePlugin(parsedArgs: DistArgs): Promise<string> {
|
|
300
|
-
const { pluginDir, outDir, verbose, noNpmDeps } = parsedArgs;
|
|
238
|
+
const { pluginDir, outDir, verbose, noNpmDeps, noBuild } = parsedArgs;
|
|
301
239
|
if (verbose) {
|
|
302
240
|
process.stderr.write(`Bundling plugin from: ${pluginDir}\n`);
|
|
303
241
|
}
|
|
304
242
|
|
|
305
|
-
const { pluginId, moduleExec, manifestPath } =
|
|
243
|
+
const { pluginId, moduleExec, entry, manifestPath } = noBuild
|
|
244
|
+
? readManifest(pluginDir)
|
|
245
|
+
: buildPluginAssets({ pluginDir, verbose });
|
|
306
246
|
const themesPath = path.join(pluginDir, "themes");
|
|
307
247
|
const hasThemes = fs.existsSync(themesPath) && fs.lstatSync(themesPath).isDirectory();
|
|
308
|
-
if (
|
|
309
|
-
|
|
248
|
+
if (entry) {
|
|
249
|
+
validateManifestEntry(pluginDir, entry);
|
|
250
|
+
}
|
|
251
|
+
if (!moduleExec && !hasThemes && !entry) {
|
|
252
|
+
throw new Error("manifest has no module.exec, entry, or themes/");
|
|
310
253
|
}
|
|
311
254
|
validateDeclaredModuleExec(pluginDir, moduleExec);
|
|
312
255
|
|
|
@@ -320,6 +263,10 @@ export async function bundlePlugin(parsedArgs: DistArgs): Promise<string> {
|
|
|
320
263
|
copyFileStrict(manifestPath, path.join(bundleDir, "openvcs.plugin.json"));
|
|
321
264
|
copyIcon(pluginDir, bundleDir);
|
|
322
265
|
|
|
266
|
+
if (entry) {
|
|
267
|
+
copyEntryDirectory(pluginDir, bundleDir, entry);
|
|
268
|
+
}
|
|
269
|
+
|
|
323
270
|
const sourceBinDir = path.join(pluginDir, "bin");
|
|
324
271
|
if (fs.existsSync(sourceBinDir) && fs.lstatSync(sourceBinDir).isDirectory()) {
|
|
325
272
|
copyDirectoryRecursiveStrict(sourceBinDir, path.join(bundleDir, "bin"));
|
|
@@ -329,7 +276,7 @@ export async function bundlePlugin(parsedArgs: DistArgs): Promise<string> {
|
|
|
329
276
|
}
|
|
330
277
|
|
|
331
278
|
if (!noNpmDeps && hasPackageJson(pluginDir)) {
|
|
332
|
-
ensurePackageLock(pluginDir, verbose);
|
|
279
|
+
ensurePackageLock(pluginDir, bundleDir, verbose);
|
|
333
280
|
installNpmDependencies(pluginDir, bundleDir, verbose);
|
|
334
281
|
}
|
|
335
282
|
|
|
@@ -352,5 +299,6 @@ export const __private = {
|
|
|
352
299
|
rejectNativeAddonsRecursive,
|
|
353
300
|
uniqueStagingDir,
|
|
354
301
|
validateDeclaredModuleExec,
|
|
302
|
+
validateManifestEntry,
|
|
355
303
|
writeTarGz,
|
|
356
304
|
};
|
package/src/lib/init.ts
CHANGED
|
@@ -236,9 +236,10 @@ function writeModuleTemplate(answers: InitAnswers): void {
|
|
|
236
236
|
private: true,
|
|
237
237
|
type: "module",
|
|
238
238
|
scripts: {
|
|
239
|
-
"build:
|
|
240
|
-
build: "
|
|
241
|
-
|
|
239
|
+
"build:plugin": "tsc -p tsconfig.json",
|
|
240
|
+
build: "openvcs build",
|
|
241
|
+
dist: "openvcs dist --plugin-dir . --out dist",
|
|
242
|
+
test: "openvcs dist --plugin-dir . --out dist --no-build --no-npm-deps",
|
|
242
243
|
},
|
|
243
244
|
devDependencies: {
|
|
244
245
|
"@openvcs/sdk": `^${packageJson.version}`,
|
|
@@ -272,8 +273,9 @@ function writeThemeTemplate(answers: InitAnswers): void {
|
|
|
272
273
|
version: answers.pluginVersion,
|
|
273
274
|
private: true,
|
|
274
275
|
scripts: {
|
|
275
|
-
build: "openvcs
|
|
276
|
-
|
|
276
|
+
build: "openvcs build",
|
|
277
|
+
dist: "openvcs dist --plugin-dir . --out dist",
|
|
278
|
+
test: "openvcs dist --plugin-dir . --out dist --no-build --no-npm-deps",
|
|
277
279
|
},
|
|
278
280
|
devDependencies: {
|
|
279
281
|
"@openvcs/sdk": `^${packageJson.version}`,
|