@openvcs/sdk 0.2.1 → 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 +48 -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 -104
- package/lib/init.d.ts +30 -0
- package/lib/init.js +76 -41
- 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 -125
- package/src/lib/init.ts +86 -50
- 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 +65 -0
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ Run the local CLI through npm scripts:
|
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
npm run openvcs -- --help
|
|
47
|
+
npm run openvcs -- build --help
|
|
47
48
|
npm run openvcs -- init --help
|
|
48
49
|
npm run openvcs -- dist --help
|
|
49
50
|
```
|
|
@@ -57,6 +58,8 @@ openvcs init my-plugin
|
|
|
57
58
|
```
|
|
58
59
|
|
|
59
60
|
The generated module template includes TypeScript and Node typings (`@types/node`).
|
|
61
|
+
Plugin IDs entered during scaffold must not be `.`/`..` and must not contain path
|
|
62
|
+
separators (`/` or `\\`).
|
|
60
63
|
|
|
61
64
|
Interactive theme plugin scaffold:
|
|
62
65
|
|
|
@@ -64,23 +67,60 @@ Interactive theme plugin scaffold:
|
|
|
64
67
|
openvcs init --theme my-theme
|
|
65
68
|
```
|
|
66
69
|
|
|
70
|
+
## Build plugin assets
|
|
71
|
+
|
|
72
|
+
In a generated code plugin folder:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run build
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This runs `openvcs build`, which executes `scripts["build:plugin"]` and verifies
|
|
79
|
+
that `bin/<module.exec>` now exists.
|
|
80
|
+
|
|
81
|
+
Theme-only plugins can also run `npm run build`; the command exits successfully
|
|
82
|
+
without producing `bin/` output.
|
|
83
|
+
|
|
67
84
|
## Build a `.ovcsp` bundle
|
|
68
85
|
|
|
69
86
|
In a generated plugin folder:
|
|
70
87
|
|
|
71
88
|
```bash
|
|
72
|
-
npm run
|
|
89
|
+
npm run dist
|
|
73
90
|
```
|
|
74
91
|
|
|
75
92
|
This produces `dist/<plugin-id>.ovcsp`.
|
|
76
93
|
|
|
94
|
+
`openvcs dist` runs `openvcs build` first unless `--no-build` is provided.
|
|
95
|
+
Use `--no-build` when packaging prebuilt plugin assets.
|
|
96
|
+
|
|
97
|
+
Generated code plugin scripts use this split by default:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"scripts": {
|
|
102
|
+
"build:plugin": "tsc -p tsconfig.json",
|
|
103
|
+
"build": "openvcs build",
|
|
104
|
+
"dist": "openvcs dist --plugin-dir . --out dist"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
77
109
|
`.ovcsp` is a gzip-compressed tar archive (`tar.gz`) that contains a top-level
|
|
78
110
|
`<plugin-id>/` directory with `openvcs.plugin.json` and plugin runtime assets.
|
|
79
111
|
|
|
112
|
+
Bundle contents:
|
|
113
|
+
- `openvcs.plugin.json` (required)
|
|
114
|
+
- `icon.*` (optional, first found by extension priority)
|
|
115
|
+
- `bin/` (required for code plugins with `module.exec`)
|
|
116
|
+
- `entry` directory (required for UI plugins with top-level `entry` field; the entire directory containing the entry file is bundled)
|
|
117
|
+
- `themes/` (required for theme plugins)
|
|
118
|
+
- `node_modules/` (if npm dependencies are bundled)
|
|
119
|
+
|
|
80
120
|
Dependency behavior while packaging:
|
|
81
121
|
|
|
82
122
|
- npm dependency bundling is enabled by default when `package.json` exists.
|
|
83
|
-
- If `package-lock.json` is missing, SDK generates it in the plugin worktree.
|
|
123
|
+
- If `package-lock.json` is missing, SDK generates it in the staging area (not the plugin worktree).
|
|
84
124
|
- Dependencies are installed into the bundle staging dir with:
|
|
85
125
|
- `npm ci --omit=dev --ignore-scripts --no-bin-links --no-audit --no-fund`
|
|
86
126
|
- Disable npm dependency processing with `--no-npm-deps`.
|
|
@@ -91,15 +131,17 @@ Dependency behavior while packaging:
|
|
|
91
131
|
Package a plugin manually:
|
|
92
132
|
|
|
93
133
|
```bash
|
|
94
|
-
openvcs
|
|
134
|
+
npx openvcs build --plugin-dir /path/to/plugin
|
|
135
|
+
npx openvcs dist --plugin-dir /path/to/plugin --out /path/to/dist
|
|
95
136
|
```
|
|
96
137
|
|
|
97
138
|
Show command help:
|
|
98
139
|
|
|
99
140
|
```bash
|
|
100
|
-
openvcs --help
|
|
101
|
-
openvcs
|
|
102
|
-
openvcs
|
|
141
|
+
npx openvcs --help
|
|
142
|
+
npx openvcs build --help
|
|
143
|
+
npx openvcs dist --help
|
|
144
|
+
npx openvcs init --help
|
|
103
145
|
```
|
|
104
146
|
|
|
105
147
|
## Releases
|
package/lib/build.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** CLI arguments for `openvcs build`. */
|
|
2
|
+
export interface BuildArgs {
|
|
3
|
+
pluginDir: string;
|
|
4
|
+
verbose: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Trimmed manifest details used by build and dist flows. */
|
|
7
|
+
export interface ManifestInfo {
|
|
8
|
+
pluginId: string;
|
|
9
|
+
moduleExec: string | undefined;
|
|
10
|
+
entry: string | undefined;
|
|
11
|
+
manifestPath: string;
|
|
12
|
+
}
|
|
13
|
+
/** Returns the npm executable name for the current platform. */
|
|
14
|
+
export declare function npmExecutable(): string;
|
|
15
|
+
/** Formats help text for the build command. */
|
|
16
|
+
export declare function buildUsage(commandName?: string): string;
|
|
17
|
+
/** Parses `openvcs build` arguments. */
|
|
18
|
+
export declare function parseBuildArgs(args: string[]): BuildArgs;
|
|
19
|
+
/** Reads and validates the plugin manifest. */
|
|
20
|
+
export declare function readManifest(pluginDir: string): ManifestInfo;
|
|
21
|
+
/** Verifies that a declared module entry resolves to a real file under `bin/`. */
|
|
22
|
+
export declare function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void;
|
|
23
|
+
/** Returns whether the plugin repository has a `package.json`. */
|
|
24
|
+
export declare function hasPackageJson(pluginDir: string): boolean;
|
|
25
|
+
/** Runs a command in the given directory with optional verbose logging. */
|
|
26
|
+
export declare function runCommand(program: string, args: string[], cwd: string, verbose: boolean): void;
|
|
27
|
+
/** Builds a plugin's runtime assets when it declares a code module. */
|
|
28
|
+
export declare function buildPluginAssets(parsedArgs: BuildArgs): ManifestInfo;
|
package/lib/build.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright © 2025-2026 OpenVCS Contributors
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.npmExecutable = npmExecutable;
|
|
6
|
+
exports.buildUsage = buildUsage;
|
|
7
|
+
exports.parseBuildArgs = parseBuildArgs;
|
|
8
|
+
exports.readManifest = readManifest;
|
|
9
|
+
exports.validateDeclaredModuleExec = validateDeclaredModuleExec;
|
|
10
|
+
exports.hasPackageJson = hasPackageJson;
|
|
11
|
+
exports.runCommand = runCommand;
|
|
12
|
+
exports.buildPluginAssets = buildPluginAssets;
|
|
13
|
+
const fs = require("node:fs");
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
const node_child_process_1 = require("node:child_process");
|
|
16
|
+
const fs_utils_1 = require("./fs-utils");
|
|
17
|
+
/** Returns the npm executable name for the current platform. */
|
|
18
|
+
function npmExecutable() {
|
|
19
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
20
|
+
}
|
|
21
|
+
/** Formats help text for the build command. */
|
|
22
|
+
function buildUsage(commandName = "openvcs") {
|
|
23
|
+
return `${commandName} build [args]\n\n --plugin-dir <path> Plugin repository root (contains openvcs.plugin.json)\n -V, --verbose Enable verbose output\n`;
|
|
24
|
+
}
|
|
25
|
+
/** Parses `openvcs build` arguments. */
|
|
26
|
+
function parseBuildArgs(args) {
|
|
27
|
+
let pluginDir = process.cwd();
|
|
28
|
+
let verbose = false;
|
|
29
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
30
|
+
const arg = args[index];
|
|
31
|
+
if (arg === "--plugin-dir") {
|
|
32
|
+
index += 1;
|
|
33
|
+
if (index >= args.length) {
|
|
34
|
+
throw new Error("missing value for --plugin-dir");
|
|
35
|
+
}
|
|
36
|
+
pluginDir = args[index];
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (arg === "-V" || arg === "--verbose") {
|
|
40
|
+
verbose = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (arg === "--help") {
|
|
44
|
+
const error = new Error(buildUsage());
|
|
45
|
+
error.code = "USAGE";
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`unknown flag: ${arg}`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
pluginDir: path.resolve(pluginDir),
|
|
52
|
+
verbose,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Reads and validates the plugin manifest. */
|
|
56
|
+
function readManifest(pluginDir) {
|
|
57
|
+
const manifestPath = path.join(pluginDir, "openvcs.plugin.json");
|
|
58
|
+
let manifestRaw;
|
|
59
|
+
let manifestFd;
|
|
60
|
+
let manifest;
|
|
61
|
+
try {
|
|
62
|
+
manifestFd = fs.openSync(manifestPath, "r");
|
|
63
|
+
const manifestStat = fs.fstatSync(manifestFd);
|
|
64
|
+
if (!manifestStat.isFile()) {
|
|
65
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
66
|
+
}
|
|
67
|
+
manifestRaw = fs.readFileSync(manifestFd, "utf8");
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error.code === "ENOENT") {
|
|
71
|
+
throw new Error(`missing openvcs.plugin.json at ${manifestPath}`);
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
if (typeof manifestFd === "number") {
|
|
77
|
+
fs.closeSync(manifestFd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
manifest = JSON.parse(manifestRaw);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
85
|
+
throw new Error(`parse ${manifestPath}: ${detail}`);
|
|
86
|
+
}
|
|
87
|
+
const pluginId = typeof manifest.id === "string"
|
|
88
|
+
? manifest.id.trim()
|
|
89
|
+
: "";
|
|
90
|
+
if (!pluginId) {
|
|
91
|
+
throw new Error(`manifest ${manifestPath} is missing a string 'id'`);
|
|
92
|
+
}
|
|
93
|
+
if (pluginId === "." || pluginId === ".." || pluginId.includes("/") || pluginId.includes("\\")) {
|
|
94
|
+
throw new Error(`manifest id must not contain path separators: ${pluginId}`);
|
|
95
|
+
}
|
|
96
|
+
const moduleValue = manifest.module;
|
|
97
|
+
const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
|
|
98
|
+
const entryValue = manifest.entry;
|
|
99
|
+
const entry = typeof entryValue === "string" ? entryValue.trim() : undefined;
|
|
100
|
+
return {
|
|
101
|
+
pluginId,
|
|
102
|
+
moduleExec,
|
|
103
|
+
entry,
|
|
104
|
+
manifestPath,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Verifies that a declared module entry resolves to a real file under `bin/`. */
|
|
108
|
+
function validateDeclaredModuleExec(pluginDir, moduleExec) {
|
|
109
|
+
if (!moduleExec) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const normalizedExec = moduleExec.trim();
|
|
113
|
+
const lowered = normalizedExec.toLowerCase();
|
|
114
|
+
if (!lowered.endsWith(".js") && !lowered.endsWith(".mjs") && !lowered.endsWith(".cjs")) {
|
|
115
|
+
throw new Error(`manifest exec must end with .js/.mjs/.cjs (Node runtime): ${moduleExec}`);
|
|
116
|
+
}
|
|
117
|
+
if (path.isAbsolute(normalizedExec)) {
|
|
118
|
+
throw new Error(`manifest module.exec must be a relative path under bin/: ${moduleExec}`);
|
|
119
|
+
}
|
|
120
|
+
const binDir = path.resolve(pluginDir, "bin");
|
|
121
|
+
const targetPath = path.resolve(binDir, normalizedExec);
|
|
122
|
+
if (!(0, fs_utils_1.isPathInside)(binDir, targetPath) || targetPath === binDir) {
|
|
123
|
+
throw new Error(`manifest module.exec must point to a file under bin/: ${moduleExec}`);
|
|
124
|
+
}
|
|
125
|
+
if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
|
|
126
|
+
throw new Error(`module entrypoint not found at ${targetPath}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Returns whether the plugin repository has a `package.json`. */
|
|
130
|
+
function hasPackageJson(pluginDir) {
|
|
131
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
132
|
+
return fs.existsSync(packageJsonPath) && fs.lstatSync(packageJsonPath).isFile();
|
|
133
|
+
}
|
|
134
|
+
/** Runs a command in the given directory with optional verbose logging. */
|
|
135
|
+
function runCommand(program, args, cwd, verbose) {
|
|
136
|
+
if (verbose) {
|
|
137
|
+
process.stderr.write(`Running command in ${cwd}: ${program} ${args.join(" ")}\n`);
|
|
138
|
+
}
|
|
139
|
+
const result = (0, node_child_process_1.spawnSync)(program, args, {
|
|
140
|
+
cwd,
|
|
141
|
+
stdio: ["ignore", verbose ? "inherit" : "ignore", "inherit"],
|
|
142
|
+
});
|
|
143
|
+
if (result.error) {
|
|
144
|
+
throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
|
|
145
|
+
}
|
|
146
|
+
if (result.status === 0) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}`);
|
|
150
|
+
}
|
|
151
|
+
/** Reads `package.json` scripts for the plugin, if present. */
|
|
152
|
+
function readPackageScripts(pluginDir) {
|
|
153
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
154
|
+
let packageData;
|
|
155
|
+
try {
|
|
156
|
+
packageData = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error.code === "ENOENT") {
|
|
160
|
+
throw new Error(`code plugins must include package.json: ${packageJsonPath}`);
|
|
161
|
+
}
|
|
162
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
163
|
+
throw new Error(`parse ${packageJsonPath}: ${detail}`);
|
|
164
|
+
}
|
|
165
|
+
const scripts = packageData.scripts;
|
|
166
|
+
if (typeof scripts !== "object" || scripts === null) {
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
return scripts;
|
|
170
|
+
}
|
|
171
|
+
/** Builds a plugin's runtime assets when it declares a code module. */
|
|
172
|
+
function buildPluginAssets(parsedArgs) {
|
|
173
|
+
const manifest = readManifest(parsedArgs.pluginDir);
|
|
174
|
+
if (!manifest.moduleExec) {
|
|
175
|
+
return manifest;
|
|
176
|
+
}
|
|
177
|
+
if (!hasPackageJson(parsedArgs.pluginDir)) {
|
|
178
|
+
throw new Error(`code plugins must include package.json: ${path.join(parsedArgs.pluginDir, "package.json")}`);
|
|
179
|
+
}
|
|
180
|
+
const scripts = readPackageScripts(parsedArgs.pluginDir);
|
|
181
|
+
const buildScript = scripts["build:plugin"];
|
|
182
|
+
if (typeof buildScript !== "string" || buildScript.trim() === "") {
|
|
183
|
+
throw new Error(`code plugins must define scripts[\"build:plugin\"] in ${path.join(parsedArgs.pluginDir, "package.json")}`);
|
|
184
|
+
}
|
|
185
|
+
runCommand(npmExecutable(), ["run", "build:plugin"], parsedArgs.pluginDir, parsedArgs.verbose);
|
|
186
|
+
validateDeclaredModuleExec(parsedArgs.pluginDir, manifest.moduleExec);
|
|
187
|
+
return manifest;
|
|
188
|
+
}
|
package/lib/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runCli = runCli;
|
|
4
|
+
const build_1 = require("./build");
|
|
4
5
|
const dist_1 = require("./dist");
|
|
5
6
|
const init_1 = require("./init");
|
|
6
7
|
const packageJson = require("../package.json");
|
|
@@ -11,7 +12,7 @@ function hasCode(error, code) {
|
|
|
11
12
|
error.code === code);
|
|
12
13
|
}
|
|
13
14
|
function usage() {
|
|
14
|
-
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";
|
|
15
|
+
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";
|
|
15
16
|
}
|
|
16
17
|
async function runCli(args) {
|
|
17
18
|
if (args.length === 0) {
|
|
@@ -19,7 +20,7 @@ async function runCli(args) {
|
|
|
19
20
|
process.exitCode = 1;
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
|
-
if (args
|
|
23
|
+
if (args[0] === "-v" || args[0] === "--version") {
|
|
23
24
|
process.stdout.write(`openvcs ${packageJson.version}\n`);
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
@@ -46,6 +47,24 @@ async function runCli(args) {
|
|
|
46
47
|
throw error;
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
if (command === "build") {
|
|
51
|
+
if (rest.includes("--help")) {
|
|
52
|
+
process.stdout.write((0, build_1.buildUsage)());
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const parsed = (0, build_1.parseBuildArgs)(rest);
|
|
57
|
+
const manifest = (0, build_1.buildPluginAssets)(parsed);
|
|
58
|
+
process.stdout.write(`${manifest.pluginId}\n`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (hasCode(error, "USAGE")) {
|
|
63
|
+
throw new Error((0, build_1.buildUsage)());
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
49
68
|
if (command === "init") {
|
|
50
69
|
if (rest.includes("--help")) {
|
|
51
70
|
process.stdout.write((0, init_1.initUsage)());
|
package/lib/dist.d.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
+
import { readManifest, validateDeclaredModuleExec } from "./build";
|
|
1
2
|
interface DistArgs {
|
|
2
3
|
pluginDir: string;
|
|
3
4
|
outDir: string;
|
|
4
5
|
verbose: boolean;
|
|
5
6
|
noNpmDeps: boolean;
|
|
6
|
-
|
|
7
|
-
interface ManifestInfo {
|
|
8
|
-
pluginId: string;
|
|
9
|
-
moduleExec: string | undefined;
|
|
10
|
-
manifestPath: string;
|
|
7
|
+
noBuild: boolean;
|
|
11
8
|
}
|
|
12
9
|
export declare function distUsage(commandName?: string): string;
|
|
13
10
|
export declare function parseDistArgs(args: string[]): DistArgs;
|
|
14
|
-
declare function
|
|
15
|
-
declare function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void;
|
|
11
|
+
declare function validateManifestEntry(pluginDir: string, entry: string): void;
|
|
16
12
|
declare function rejectNativeAddonsRecursive(dirPath: string): void;
|
|
17
13
|
declare function copyIcon(pluginDir: string, bundleDir: string): void;
|
|
18
14
|
declare function uniqueStagingDir(outDir: string): string;
|
|
@@ -25,6 +21,7 @@ export declare const __private: {
|
|
|
25
21
|
rejectNativeAddonsRecursive: typeof rejectNativeAddonsRecursive;
|
|
26
22
|
uniqueStagingDir: typeof uniqueStagingDir;
|
|
27
23
|
validateDeclaredModuleExec: typeof validateDeclaredModuleExec;
|
|
24
|
+
validateManifestEntry: typeof validateManifestEntry;
|
|
28
25
|
writeTarGz: typeof writeTarGz;
|
|
29
26
|
};
|
|
30
27
|
export {};
|
package/lib/dist.js
CHANGED
|
@@ -6,21 +6,19 @@ exports.parseDistArgs = parseDistArgs;
|
|
|
6
6
|
exports.bundlePlugin = bundlePlugin;
|
|
7
7
|
const fs = require("node:fs");
|
|
8
8
|
const path = require("node:path");
|
|
9
|
-
const node_child_process_1 = require("node:child_process");
|
|
10
9
|
const tar = require("tar");
|
|
10
|
+
const build_1 = require("./build");
|
|
11
11
|
const fs_utils_1 = require("./fs-utils");
|
|
12
12
|
const ICON_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "avif", "svg"];
|
|
13
|
-
function npmExecutable() {
|
|
14
|
-
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
15
|
-
}
|
|
16
13
|
function distUsage(commandName = "openvcs") {
|
|
17
|
-
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`;
|
|
14
|
+
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`;
|
|
18
15
|
}
|
|
19
16
|
function parseDistArgs(args) {
|
|
20
17
|
let pluginDir = process.cwd();
|
|
21
18
|
let outDir = "dist";
|
|
22
19
|
let verbose = false;
|
|
23
20
|
let noNpmDeps = false;
|
|
21
|
+
let noBuild = false;
|
|
24
22
|
for (let index = 0; index < args.length; index += 1) {
|
|
25
23
|
const arg = args[index];
|
|
26
24
|
if (arg === "--plugin-dir") {
|
|
@@ -43,6 +41,10 @@ function parseDistArgs(args) {
|
|
|
43
41
|
noNpmDeps = true;
|
|
44
42
|
continue;
|
|
45
43
|
}
|
|
44
|
+
if (arg === "--no-build") {
|
|
45
|
+
noBuild = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
46
48
|
if (arg === "-V" || arg === "--verbose") {
|
|
47
49
|
verbose = true;
|
|
48
50
|
continue;
|
|
@@ -59,100 +61,44 @@ function parseDistArgs(args) {
|
|
|
59
61
|
outDir: path.resolve(outDir),
|
|
60
62
|
verbose,
|
|
61
63
|
noNpmDeps,
|
|
64
|
+
noBuild,
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
throw new Error(`
|
|
68
|
-
}
|
|
69
|
-
let manifest;
|
|
70
|
-
try {
|
|
71
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
67
|
+
function validateManifestEntry(pluginDir, entry) {
|
|
68
|
+
const normalized = entry.trim();
|
|
69
|
+
if (path.isAbsolute(normalized)) {
|
|
70
|
+
throw new Error(`manifest entry must be a relative path: ${entry}`);
|
|
72
71
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const pluginId = typeof manifest.id === "string"
|
|
78
|
-
? manifest.id.trim()
|
|
79
|
-
: "";
|
|
80
|
-
if (!pluginId) {
|
|
81
|
-
throw new Error(`manifest ${manifestPath} is missing a string 'id'`);
|
|
82
|
-
}
|
|
83
|
-
if (pluginId === "." || pluginId === ".." || pluginId.includes("/") || pluginId.includes("\\")) {
|
|
84
|
-
throw new Error(`manifest id must not contain path separators: ${pluginId}`);
|
|
85
|
-
}
|
|
86
|
-
const moduleValue = manifest.module;
|
|
87
|
-
const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
|
|
88
|
-
return {
|
|
89
|
-
pluginId,
|
|
90
|
-
moduleExec,
|
|
91
|
-
manifestPath,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function validateDeclaredModuleExec(pluginDir, moduleExec) {
|
|
95
|
-
if (!moduleExec) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const normalizedExec = moduleExec.trim();
|
|
99
|
-
const lowered = normalizedExec.toLowerCase();
|
|
100
|
-
if (!lowered.endsWith(".js") && !lowered.endsWith(".mjs") && !lowered.endsWith(".cjs")) {
|
|
101
|
-
throw new Error(`manifest exec must end with .js/.mjs/.cjs (Node runtime): ${moduleExec}`);
|
|
102
|
-
}
|
|
103
|
-
if (path.isAbsolute(normalizedExec)) {
|
|
104
|
-
throw new Error(`manifest module.exec must be a relative path under bin/: ${moduleExec}`);
|
|
105
|
-
}
|
|
106
|
-
const binDir = path.resolve(pluginDir, "bin");
|
|
107
|
-
const targetPath = path.resolve(binDir, normalizedExec);
|
|
108
|
-
if (!(0, fs_utils_1.isPathInside)(binDir, targetPath) || targetPath === binDir) {
|
|
109
|
-
throw new Error(`manifest module.exec must point to a file under bin/: ${moduleExec}`);
|
|
72
|
+
const targetPath = path.resolve(pluginDir, normalized);
|
|
73
|
+
const pluginDirResolved = path.resolve(pluginDir);
|
|
74
|
+
if (!(0, fs_utils_1.isPathInside)(pluginDirResolved, targetPath) || targetPath === pluginDirResolved) {
|
|
75
|
+
throw new Error(`manifest entry must point to a file under the plugin directory: ${entry}`);
|
|
110
76
|
}
|
|
111
77
|
if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
|
|
112
|
-
throw new Error(`
|
|
78
|
+
throw new Error(`manifest entry file not found: ${entry}`);
|
|
113
79
|
}
|
|
114
80
|
}
|
|
115
|
-
function
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
process.stderr.write(`Running command in ${cwd}: ${program} ${args.join(" ")}\n`);
|
|
122
|
-
}
|
|
123
|
-
const result = (0, node_child_process_1.spawnSync)(program, args, {
|
|
124
|
-
cwd,
|
|
125
|
-
encoding: "utf8",
|
|
126
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
127
|
-
});
|
|
128
|
-
if (result.error) {
|
|
129
|
-
throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
|
|
130
|
-
}
|
|
131
|
-
if (result.status === 0) {
|
|
132
|
-
if (verbose) {
|
|
133
|
-
if (result.stdout?.trim()) {
|
|
134
|
-
process.stderr.write(`${result.stdout.trim()}\n`);
|
|
135
|
-
}
|
|
136
|
-
if (result.stderr?.trim()) {
|
|
137
|
-
process.stderr.write(`${result.stderr.trim()}\n`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
throw new Error(`command failed (${program} ${args.join(" ")}), exit code ${result.status}, stdout='${(result.stdout || "").trim()}', stderr='${(result.stderr || "").trim()}'`);
|
|
81
|
+
function copyEntryDirectory(pluginDir, bundleDir, entry) {
|
|
82
|
+
const normalized = entry.trim();
|
|
83
|
+
const entryDir = path.dirname(normalized);
|
|
84
|
+
const sourceDir = path.join(pluginDir, entryDir);
|
|
85
|
+
const destDir = path.join(bundleDir, entryDir);
|
|
86
|
+
(0, fs_utils_1.copyDirectoryRecursiveStrict)(sourceDir, destDir);
|
|
143
87
|
}
|
|
144
|
-
function ensurePackageLock(pluginDir, verbose) {
|
|
145
|
-
if (!hasPackageJson(pluginDir)) {
|
|
88
|
+
function ensurePackageLock(pluginDir, bundleDir, verbose) {
|
|
89
|
+
if (!(0, build_1.hasPackageJson)(pluginDir)) {
|
|
146
90
|
return;
|
|
147
91
|
}
|
|
148
92
|
const lockPath = path.join(pluginDir, "package-lock.json");
|
|
149
93
|
if (fs.existsSync(lockPath) && fs.lstatSync(lockPath).isFile()) {
|
|
150
94
|
return;
|
|
151
95
|
}
|
|
96
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
97
|
+
(0, fs_utils_1.copyFileStrict)(packageJsonPath, path.join(bundleDir, "package.json"));
|
|
152
98
|
if (verbose) {
|
|
153
|
-
process.stderr.write(`Generating package-lock.json in
|
|
99
|
+
process.stderr.write(`Generating package-lock.json in staging\n`);
|
|
154
100
|
}
|
|
155
|
-
runCommand(npmExecutable(), ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
101
|
+
(0, build_1.runCommand)((0, build_1.npmExecutable)(), ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"], bundleDir, verbose);
|
|
156
102
|
}
|
|
157
103
|
function copyNpmFilesToStaging(pluginDir, bundleDir) {
|
|
158
104
|
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
@@ -160,11 +106,14 @@ function copyNpmFilesToStaging(pluginDir, bundleDir) {
|
|
|
160
106
|
if (!fs.existsSync(packageJsonPath) || !fs.lstatSync(packageJsonPath).isFile()) {
|
|
161
107
|
throw new Error(`missing package.json at ${packageJsonPath}`);
|
|
162
108
|
}
|
|
163
|
-
if (!fs.existsSync(lockPath) || !fs.lstatSync(lockPath).isFile()) {
|
|
164
|
-
throw new Error(`missing package-lock.json at ${lockPath}`);
|
|
165
|
-
}
|
|
166
109
|
(0, fs_utils_1.copyFileStrict)(packageJsonPath, path.join(bundleDir, "package.json"));
|
|
167
|
-
|
|
110
|
+
const stagedLockPath = path.join(bundleDir, "package-lock.json");
|
|
111
|
+
if (fs.existsSync(stagedLockPath) && fs.lstatSync(stagedLockPath).isFile()) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (fs.existsSync(lockPath) && fs.lstatSync(lockPath).isFile()) {
|
|
115
|
+
(0, fs_utils_1.copyFileStrict)(lockPath, path.join(bundleDir, "package-lock.json"));
|
|
116
|
+
}
|
|
168
117
|
}
|
|
169
118
|
function rejectNativeAddonsRecursive(dirPath) {
|
|
170
119
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
@@ -188,7 +137,7 @@ function rejectNativeAddonsRecursive(dirPath) {
|
|
|
188
137
|
}
|
|
189
138
|
function installNpmDependencies(pluginDir, bundleDir, verbose) {
|
|
190
139
|
copyNpmFilesToStaging(pluginDir, bundleDir);
|
|
191
|
-
runCommand(npmExecutable(), ["ci", "--omit=dev", "--ignore-scripts", "--no-bin-links", "--no-audit", "--no-fund"], bundleDir, verbose);
|
|
140
|
+
(0, build_1.runCommand)((0, build_1.npmExecutable)(), ["ci", "--omit=dev", "--ignore-scripts", "--no-bin-links", "--no-audit", "--no-fund"], bundleDir, verbose);
|
|
192
141
|
const nodeModulesPath = path.join(bundleDir, "node_modules");
|
|
193
142
|
if (!fs.existsSync(nodeModulesPath)) {
|
|
194
143
|
return;
|
|
@@ -199,14 +148,19 @@ function installNpmDependencies(pluginDir, bundleDir, verbose) {
|
|
|
199
148
|
rejectNativeAddonsRecursive(nodeModulesPath);
|
|
200
149
|
}
|
|
201
150
|
function copyIcon(pluginDir, bundleDir) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
151
|
+
const entries = fs.readdirSync(pluginDir, { withFileTypes: true });
|
|
152
|
+
const iconEntries = entries.filter((e) => {
|
|
153
|
+
if (!e.isFile())
|
|
154
|
+
return false;
|
|
155
|
+
const name = e.name.toLowerCase();
|
|
156
|
+
return name.startsWith("icon.") && ICON_EXTENSIONS.includes(name.slice(5));
|
|
157
|
+
});
|
|
158
|
+
for (const ext of ICON_EXTENSIONS) {
|
|
159
|
+
const found = iconEntries.find((e) => e.name.toLowerCase() === `icon.${ext}`);
|
|
160
|
+
if (found) {
|
|
161
|
+
(0, fs_utils_1.copyFileStrict)(path.join(pluginDir, found.name), path.join(bundleDir, found.name));
|
|
162
|
+
return;
|
|
207
163
|
}
|
|
208
|
-
(0, fs_utils_1.copyFileStrict)(sourcePath, path.join(bundleDir, fileName));
|
|
209
|
-
return;
|
|
210
164
|
}
|
|
211
165
|
}
|
|
212
166
|
function uniqueStagingDir(outDir) {
|
|
@@ -226,17 +180,22 @@ async function writeTarGz(outPath, baseDir, folderName) {
|
|
|
226
180
|
}, [folderName]);
|
|
227
181
|
}
|
|
228
182
|
async function bundlePlugin(parsedArgs) {
|
|
229
|
-
const { pluginDir, outDir, verbose, noNpmDeps } = parsedArgs;
|
|
183
|
+
const { pluginDir, outDir, verbose, noNpmDeps, noBuild } = parsedArgs;
|
|
230
184
|
if (verbose) {
|
|
231
185
|
process.stderr.write(`Bundling plugin from: ${pluginDir}\n`);
|
|
232
186
|
}
|
|
233
|
-
const { pluginId, moduleExec, manifestPath } =
|
|
187
|
+
const { pluginId, moduleExec, entry, manifestPath } = noBuild
|
|
188
|
+
? (0, build_1.readManifest)(pluginDir)
|
|
189
|
+
: (0, build_1.buildPluginAssets)({ pluginDir, verbose });
|
|
234
190
|
const themesPath = path.join(pluginDir, "themes");
|
|
235
191
|
const hasThemes = fs.existsSync(themesPath) && fs.lstatSync(themesPath).isDirectory();
|
|
236
|
-
if (
|
|
237
|
-
|
|
192
|
+
if (entry) {
|
|
193
|
+
validateManifestEntry(pluginDir, entry);
|
|
238
194
|
}
|
|
239
|
-
|
|
195
|
+
if (!moduleExec && !hasThemes && !entry) {
|
|
196
|
+
throw new Error("manifest has no module.exec, entry, or themes/");
|
|
197
|
+
}
|
|
198
|
+
(0, build_1.validateDeclaredModuleExec)(pluginDir, moduleExec);
|
|
240
199
|
(0, fs_utils_1.ensureDirectory)(outDir);
|
|
241
200
|
const stagingRoot = uniqueStagingDir(outDir);
|
|
242
201
|
const bundleDir = path.join(stagingRoot, pluginId);
|
|
@@ -244,6 +203,9 @@ async function bundlePlugin(parsedArgs) {
|
|
|
244
203
|
try {
|
|
245
204
|
(0, fs_utils_1.copyFileStrict)(manifestPath, path.join(bundleDir, "openvcs.plugin.json"));
|
|
246
205
|
copyIcon(pluginDir, bundleDir);
|
|
206
|
+
if (entry) {
|
|
207
|
+
copyEntryDirectory(pluginDir, bundleDir, entry);
|
|
208
|
+
}
|
|
247
209
|
const sourceBinDir = path.join(pluginDir, "bin");
|
|
248
210
|
if (fs.existsSync(sourceBinDir) && fs.lstatSync(sourceBinDir).isDirectory()) {
|
|
249
211
|
(0, fs_utils_1.copyDirectoryRecursiveStrict)(sourceBinDir, path.join(bundleDir, "bin"));
|
|
@@ -251,8 +213,8 @@ async function bundlePlugin(parsedArgs) {
|
|
|
251
213
|
if (hasThemes) {
|
|
252
214
|
(0, fs_utils_1.copyDirectoryRecursiveStrict)(themesPath, path.join(bundleDir, "themes"));
|
|
253
215
|
}
|
|
254
|
-
if (!noNpmDeps && hasPackageJson(pluginDir)) {
|
|
255
|
-
ensurePackageLock(pluginDir, verbose);
|
|
216
|
+
if (!noNpmDeps && (0, build_1.hasPackageJson)(pluginDir)) {
|
|
217
|
+
ensurePackageLock(pluginDir, bundleDir, verbose);
|
|
256
218
|
installNpmDependencies(pluginDir, bundleDir, verbose);
|
|
257
219
|
}
|
|
258
220
|
const outPath = path.join(outDir, `${pluginId}.ovcsp`);
|
|
@@ -269,9 +231,10 @@ async function bundlePlugin(parsedArgs) {
|
|
|
269
231
|
exports.__private = {
|
|
270
232
|
ICON_EXTENSIONS,
|
|
271
233
|
copyIcon,
|
|
272
|
-
readManifest,
|
|
234
|
+
readManifest: build_1.readManifest,
|
|
273
235
|
rejectNativeAddonsRecursive,
|
|
274
236
|
uniqueStagingDir,
|
|
275
|
-
validateDeclaredModuleExec,
|
|
237
|
+
validateDeclaredModuleExec: build_1.validateDeclaredModuleExec,
|
|
238
|
+
validateManifestEntry,
|
|
276
239
|
writeTarGz,
|
|
277
240
|
};
|