@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/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,27 +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
|
-
stdout?: string | null;
|
|
34
|
-
stderr?: string | null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function npmExecutable(): string {
|
|
38
|
-
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
30
|
+
noBuild: boolean;
|
|
39
31
|
}
|
|
40
32
|
|
|
41
33
|
export function distUsage(commandName = "openvcs"): string {
|
|
42
|
-
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`;
|
|
43
35
|
}
|
|
44
36
|
|
|
45
37
|
export function parseDistArgs(args: string[]): DistArgs {
|
|
@@ -47,6 +39,7 @@ export function parseDistArgs(args: string[]): DistArgs {
|
|
|
47
39
|
let outDir = "dist";
|
|
48
40
|
let verbose = false;
|
|
49
41
|
let noNpmDeps = false;
|
|
42
|
+
let noBuild = false;
|
|
50
43
|
|
|
51
44
|
for (let index = 0; index < args.length; index += 1) {
|
|
52
45
|
const arg = args[index];
|
|
@@ -70,6 +63,10 @@ export function parseDistArgs(args: string[]): DistArgs {
|
|
|
70
63
|
noNpmDeps = true;
|
|
71
64
|
continue;
|
|
72
65
|
}
|
|
66
|
+
if (arg === "--no-build") {
|
|
67
|
+
noBuild = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
73
70
|
if (arg === "-V" || arg === "--verbose") {
|
|
74
71
|
verbose = true;
|
|
75
72
|
continue;
|
|
@@ -87,105 +84,34 @@ export function parseDistArgs(args: string[]): DistArgs {
|
|
|
87
84
|
outDir: path.resolve(outDir),
|
|
88
85
|
verbose,
|
|
89
86
|
noNpmDeps,
|
|
87
|
+
noBuild,
|
|
90
88
|
};
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
function
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
throw new Error(`
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let manifest: unknown;
|
|
100
|
-
try {
|
|
101
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
102
|
-
} catch (error: unknown) {
|
|
103
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
104
|
-
throw new Error(`parse ${manifestPath}: ${detail}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const pluginId =
|
|
108
|
-
typeof (manifest as { id?: unknown }).id === "string"
|
|
109
|
-
? ((manifest as { id: string }).id.trim() as string)
|
|
110
|
-
: "";
|
|
111
|
-
if (!pluginId) {
|
|
112
|
-
throw new Error(`manifest ${manifestPath} is missing a string 'id'`);
|
|
113
|
-
}
|
|
114
|
-
if (pluginId === "." || pluginId === ".." || pluginId.includes("/") || pluginId.includes("\\")) {
|
|
115
|
-
throw new Error(`manifest id must not contain path separators: ${pluginId}`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const moduleValue = (manifest as { module?: { exec?: unknown } }).module;
|
|
119
|
-
const moduleExec = typeof moduleValue?.exec === "string" ? moduleValue.exec.trim() : undefined;
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
pluginId,
|
|
123
|
-
moduleExec,
|
|
124
|
-
manifestPath,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function validateDeclaredModuleExec(pluginDir: string, moduleExec: string | undefined): void {
|
|
129
|
-
if (!moduleExec) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const normalizedExec = moduleExec.trim();
|
|
134
|
-
const lowered = normalizedExec.toLowerCase();
|
|
135
|
-
if (!lowered.endsWith(".js") && !lowered.endsWith(".mjs") && !lowered.endsWith(".cjs")) {
|
|
136
|
-
throw new Error(`manifest exec must end with .js/.mjs/.cjs (Node runtime): ${moduleExec}`);
|
|
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}`);
|
|
137
95
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const binDir = path.resolve(pluginDir, "bin");
|
|
143
|
-
const targetPath = path.resolve(binDir, normalizedExec);
|
|
144
|
-
if (!isPathInside(binDir, targetPath) || targetPath === binDir) {
|
|
145
|
-
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}`);
|
|
146
100
|
}
|
|
147
101
|
if (!fs.existsSync(targetPath) || !fs.lstatSync(targetPath).isFile()) {
|
|
148
|
-
throw new Error(`
|
|
102
|
+
throw new Error(`manifest entry file not found: ${entry}`);
|
|
149
103
|
}
|
|
150
104
|
}
|
|
151
105
|
|
|
152
|
-
function
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (verbose) {
|
|
159
|
-
process.stderr.write(`Running command in ${cwd}: ${program} ${args.join(" ")}\n`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const result = spawnSync(program, args, {
|
|
163
|
-
cwd,
|
|
164
|
-
encoding: "utf8",
|
|
165
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
166
|
-
}) as CommandResult;
|
|
167
|
-
|
|
168
|
-
if (result.error) {
|
|
169
|
-
throw new Error(`failed to spawn '${program}' in ${cwd}: ${result.error.message}`);
|
|
170
|
-
}
|
|
171
|
-
if (result.status === 0) {
|
|
172
|
-
if (verbose) {
|
|
173
|
-
if (result.stdout?.trim()) {
|
|
174
|
-
process.stderr.write(`${result.stdout.trim()}\n`);
|
|
175
|
-
}
|
|
176
|
-
if (result.stderr?.trim()) {
|
|
177
|
-
process.stderr.write(`${result.stderr.trim()}\n`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
throw new Error(
|
|
184
|
-
`command failed (${program} ${args.join(" ")}), exit code ${result.status}, stdout='${(result.stdout || "").trim()}', stderr='${(result.stderr || "").trim()}'`
|
|
185
|
-
);
|
|
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);
|
|
186
112
|
}
|
|
187
113
|
|
|
188
|
-
function ensurePackageLock(pluginDir: string, verbose: boolean): void {
|
|
114
|
+
function ensurePackageLock(pluginDir: string, bundleDir: string, verbose: boolean): void {
|
|
189
115
|
if (!hasPackageJson(pluginDir)) {
|
|
190
116
|
return;
|
|
191
117
|
}
|
|
@@ -194,13 +120,16 @@ function ensurePackageLock(pluginDir: string, verbose: boolean): void {
|
|
|
194
120
|
return;
|
|
195
121
|
}
|
|
196
122
|
|
|
123
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
124
|
+
copyFileStrict(packageJsonPath, path.join(bundleDir, "package.json"));
|
|
125
|
+
|
|
197
126
|
if (verbose) {
|
|
198
|
-
process.stderr.write(`Generating package-lock.json in
|
|
127
|
+
process.stderr.write(`Generating package-lock.json in staging\n`);
|
|
199
128
|
}
|
|
200
129
|
runCommand(
|
|
201
130
|
npmExecutable(),
|
|
202
131
|
["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
203
|
-
|
|
132
|
+
bundleDir,
|
|
204
133
|
verbose
|
|
205
134
|
);
|
|
206
135
|
}
|
|
@@ -212,12 +141,17 @@ function copyNpmFilesToStaging(pluginDir: string, bundleDir: string): void {
|
|
|
212
141
|
if (!fs.existsSync(packageJsonPath) || !fs.lstatSync(packageJsonPath).isFile()) {
|
|
213
142
|
throw new Error(`missing package.json at ${packageJsonPath}`);
|
|
214
143
|
}
|
|
215
|
-
if (!fs.existsSync(lockPath) || !fs.lstatSync(lockPath).isFile()) {
|
|
216
|
-
throw new Error(`missing package-lock.json at ${lockPath}`);
|
|
217
|
-
}
|
|
218
144
|
|
|
219
145
|
copyFileStrict(packageJsonPath, path.join(bundleDir, "package.json"));
|
|
220
|
-
|
|
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
|
+
}
|
|
221
155
|
}
|
|
222
156
|
|
|
223
157
|
function rejectNativeAddonsRecursive(dirPath: string): void {
|
|
@@ -261,14 +195,21 @@ function installNpmDependencies(pluginDir: string, bundleDir: string, verbose: b
|
|
|
261
195
|
}
|
|
262
196
|
|
|
263
197
|
function copyIcon(pluginDir: string, bundleDir: string): void {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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;
|
|
269
212
|
}
|
|
270
|
-
copyFileStrict(sourcePath, path.join(bundleDir, fileName));
|
|
271
|
-
return;
|
|
272
213
|
}
|
|
273
214
|
}
|
|
274
215
|
|
|
@@ -294,16 +235,21 @@ async function writeTarGz(outPath: string, baseDir: string, folderName: string):
|
|
|
294
235
|
}
|
|
295
236
|
|
|
296
237
|
export async function bundlePlugin(parsedArgs: DistArgs): Promise<string> {
|
|
297
|
-
const { pluginDir, outDir, verbose, noNpmDeps } = parsedArgs;
|
|
238
|
+
const { pluginDir, outDir, verbose, noNpmDeps, noBuild } = parsedArgs;
|
|
298
239
|
if (verbose) {
|
|
299
240
|
process.stderr.write(`Bundling plugin from: ${pluginDir}\n`);
|
|
300
241
|
}
|
|
301
242
|
|
|
302
|
-
const { pluginId, moduleExec, manifestPath } =
|
|
243
|
+
const { pluginId, moduleExec, entry, manifestPath } = noBuild
|
|
244
|
+
? readManifest(pluginDir)
|
|
245
|
+
: buildPluginAssets({ pluginDir, verbose });
|
|
303
246
|
const themesPath = path.join(pluginDir, "themes");
|
|
304
247
|
const hasThemes = fs.existsSync(themesPath) && fs.lstatSync(themesPath).isDirectory();
|
|
305
|
-
if (
|
|
306
|
-
|
|
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/");
|
|
307
253
|
}
|
|
308
254
|
validateDeclaredModuleExec(pluginDir, moduleExec);
|
|
309
255
|
|
|
@@ -317,6 +263,10 @@ export async function bundlePlugin(parsedArgs: DistArgs): Promise<string> {
|
|
|
317
263
|
copyFileStrict(manifestPath, path.join(bundleDir, "openvcs.plugin.json"));
|
|
318
264
|
copyIcon(pluginDir, bundleDir);
|
|
319
265
|
|
|
266
|
+
if (entry) {
|
|
267
|
+
copyEntryDirectory(pluginDir, bundleDir, entry);
|
|
268
|
+
}
|
|
269
|
+
|
|
320
270
|
const sourceBinDir = path.join(pluginDir, "bin");
|
|
321
271
|
if (fs.existsSync(sourceBinDir) && fs.lstatSync(sourceBinDir).isDirectory()) {
|
|
322
272
|
copyDirectoryRecursiveStrict(sourceBinDir, path.join(bundleDir, "bin"));
|
|
@@ -326,7 +276,7 @@ export async function bundlePlugin(parsedArgs: DistArgs): Promise<string> {
|
|
|
326
276
|
}
|
|
327
277
|
|
|
328
278
|
if (!noNpmDeps && hasPackageJson(pluginDir)) {
|
|
329
|
-
ensurePackageLock(pluginDir, verbose);
|
|
279
|
+
ensurePackageLock(pluginDir, bundleDir, verbose);
|
|
330
280
|
installNpmDependencies(pluginDir, bundleDir, verbose);
|
|
331
281
|
}
|
|
332
282
|
|
|
@@ -349,5 +299,6 @@ export const __private = {
|
|
|
349
299
|
rejectNativeAddonsRecursive,
|
|
350
300
|
uniqueStagingDir,
|
|
351
301
|
validateDeclaredModuleExec,
|
|
302
|
+
validateManifestEntry,
|
|
352
303
|
writeTarGz,
|
|
353
304
|
};
|
package/src/lib/init.ts
CHANGED
|
@@ -23,6 +23,12 @@ interface CollectAnswersOptions {
|
|
|
23
23
|
targetHint?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
interface PromptDriver {
|
|
27
|
+
promptText(label: string, defaultValue?: string): Promise<string>;
|
|
28
|
+
promptBoolean(label: string, defaultValue: boolean): Promise<boolean>;
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
interface InitCommandError {
|
|
27
33
|
code?: string;
|
|
28
34
|
}
|
|
@@ -68,37 +74,49 @@ function defaultPluginNameFromId(pluginId: string): string {
|
|
|
68
74
|
return words.length > 0 ? words.join(" ") : "OpenVCS Plugin";
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
function validatePluginId(pluginId: string): string | undefined {
|
|
78
|
+
if (!pluginId) {
|
|
79
|
+
return "Plugin id is required.";
|
|
80
|
+
}
|
|
81
|
+
if (pluginId === "." || pluginId === "..") {
|
|
82
|
+
return "Plugin id must not be '.' or '..'.";
|
|
83
|
+
}
|
|
84
|
+
if (pluginId.includes("/") || pluginId.includes("\\")) {
|
|
85
|
+
return "Plugin id must not contain path separators (/ or \\).";
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
rl
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
function createReadlinePromptDriver(output: NodeJS.WritableStream = process.stderr): PromptDriver {
|
|
91
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
92
|
+
return {
|
|
93
|
+
async promptText(label: string, defaultValue = ""): Promise<string> {
|
|
94
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
95
|
+
const answer = await rl.question(`${label}${suffix}: `);
|
|
96
|
+
const trimmed = answer.trim();
|
|
97
|
+
return trimmed || defaultValue;
|
|
98
|
+
},
|
|
99
|
+
async promptBoolean(label: string, defaultValue: boolean): Promise<boolean> {
|
|
100
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
101
|
+
while (true) {
|
|
102
|
+
const answer = await rl.question(`${label} (${suffix}): `);
|
|
103
|
+
const normalized = answer.trim().toLowerCase();
|
|
104
|
+
if (!normalized) {
|
|
105
|
+
return defaultValue;
|
|
106
|
+
}
|
|
107
|
+
if (normalized === "y" || normalized === "yes") {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (normalized === "n" || normalized === "no") {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
output.write("Please answer yes or no.\n");
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
close(): void {
|
|
117
|
+
rl.close();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
function writeJson(filePath: string, value: unknown): void {
|
|
@@ -116,11 +134,14 @@ function directoryHasEntries(targetDir: string): boolean {
|
|
|
116
134
|
return entries.length > 0;
|
|
117
135
|
}
|
|
118
136
|
|
|
119
|
-
async function collectAnswers(
|
|
137
|
+
async function collectAnswers(
|
|
138
|
+
{ forceTheme, targetHint }: CollectAnswersOptions,
|
|
139
|
+
promptDriver: PromptDriver = createReadlinePromptDriver(),
|
|
140
|
+
output: NodeJS.WritableStream = process.stderr
|
|
141
|
+
): Promise<InitAnswers> {
|
|
120
142
|
const defaultTarget = targetHint || path.join(process.cwd(), "openvcs-plugin");
|
|
121
|
-
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
122
143
|
try {
|
|
123
|
-
const targetText = await promptText(
|
|
144
|
+
const targetText = await promptDriver.promptText("Target directory", defaultTarget);
|
|
124
145
|
const targetDir = path.resolve(targetText);
|
|
125
146
|
|
|
126
147
|
let kind: "module" | "theme" = "module";
|
|
@@ -128,7 +149,7 @@ async function collectAnswers({ forceTheme, targetHint }: CollectAnswersOptions)
|
|
|
128
149
|
kind = "theme";
|
|
129
150
|
} else {
|
|
130
151
|
while (true) {
|
|
131
|
-
const value = (await promptText(
|
|
152
|
+
const value = (await promptDriver.promptText("Template type (module/theme)", "module"))
|
|
132
153
|
.trim()
|
|
133
154
|
.toLowerCase();
|
|
134
155
|
if (value === "module" || value === "m") {
|
|
@@ -139,29 +160,35 @@ async function collectAnswers({ forceTheme, targetHint }: CollectAnswersOptions)
|
|
|
139
160
|
kind = "theme";
|
|
140
161
|
break;
|
|
141
162
|
}
|
|
142
|
-
|
|
163
|
+
output.write("Please choose 'module' or 'theme'.\n");
|
|
143
164
|
}
|
|
144
165
|
}
|
|
145
166
|
|
|
146
167
|
const defaultId = defaultPluginIdFromDir(targetDir);
|
|
147
|
-
let pluginId
|
|
168
|
+
let pluginId: string | undefined;
|
|
148
169
|
while (!pluginId) {
|
|
149
|
-
|
|
170
|
+
const candidateId = (await promptDriver.promptText("Plugin id", defaultId)).trim();
|
|
171
|
+
const validationError = validatePluginId(candidateId);
|
|
172
|
+
if (!validationError) {
|
|
173
|
+
pluginId = candidateId;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
output.write(`${validationError}\n`);
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
const defaultName = defaultPluginNameFromId(pluginId);
|
|
153
180
|
let pluginName = "";
|
|
154
181
|
while (!pluginName) {
|
|
155
|
-
pluginName = (await promptText(
|
|
182
|
+
pluginName = (await promptDriver.promptText("Plugin name", defaultName)).trim();
|
|
156
183
|
}
|
|
157
184
|
|
|
158
185
|
let pluginVersion = "";
|
|
159
186
|
while (!pluginVersion) {
|
|
160
|
-
pluginVersion = (await promptText(
|
|
187
|
+
pluginVersion = (await promptDriver.promptText("Version", "0.1.0")).trim();
|
|
161
188
|
}
|
|
162
189
|
|
|
163
|
-
const defaultEnabled = await promptBoolean(
|
|
164
|
-
const runNpmInstall = await promptBoolean(
|
|
190
|
+
const defaultEnabled = await promptDriver.promptBoolean("Default enabled", true);
|
|
191
|
+
const runNpmInstall = await promptDriver.promptBoolean("Run npm install now", true);
|
|
165
192
|
|
|
166
193
|
return {
|
|
167
194
|
targetDir,
|
|
@@ -173,7 +200,7 @@ async function collectAnswers({ forceTheme, targetHint }: CollectAnswersOptions)
|
|
|
173
200
|
runNpmInstall,
|
|
174
201
|
};
|
|
175
202
|
} finally {
|
|
176
|
-
|
|
203
|
+
promptDriver.close();
|
|
177
204
|
}
|
|
178
205
|
}
|
|
179
206
|
|
|
@@ -209,9 +236,10 @@ function writeModuleTemplate(answers: InitAnswers): void {
|
|
|
209
236
|
private: true,
|
|
210
237
|
type: "module",
|
|
211
238
|
scripts: {
|
|
212
|
-
"build:
|
|
213
|
-
build: "
|
|
214
|
-
|
|
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",
|
|
215
243
|
},
|
|
216
244
|
devDependencies: {
|
|
217
245
|
"@openvcs/sdk": `^${packageJson.version}`,
|
|
@@ -245,8 +273,9 @@ function writeThemeTemplate(answers: InitAnswers): void {
|
|
|
245
273
|
version: answers.pluginVersion,
|
|
246
274
|
private: true,
|
|
247
275
|
scripts: {
|
|
248
|
-
build: "openvcs
|
|
249
|
-
|
|
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",
|
|
250
279
|
},
|
|
251
280
|
devDependencies: {
|
|
252
281
|
"@openvcs/sdk": `^${packageJson.version}`,
|
|
@@ -292,10 +321,9 @@ export async function runInitCommand(args: string[]): Promise<string> {
|
|
|
292
321
|
} else if (!fs.lstatSync(answers.targetDir).isDirectory()) {
|
|
293
322
|
throw new Error(`target path exists but is not a directory: ${answers.targetDir}`);
|
|
294
323
|
} else if (directoryHasEntries(answers.targetDir)) {
|
|
295
|
-
const
|
|
324
|
+
const promptDriver = createReadlinePromptDriver();
|
|
296
325
|
try {
|
|
297
|
-
const proceed = await promptBoolean(
|
|
298
|
-
rl,
|
|
326
|
+
const proceed = await promptDriver.promptBoolean(
|
|
299
327
|
`Directory ${answers.targetDir} is not empty. Continue and overwrite known files`,
|
|
300
328
|
false
|
|
301
329
|
);
|
|
@@ -303,7 +331,7 @@ export async function runInitCommand(args: string[]): Promise<string> {
|
|
|
303
331
|
throw new Error("aborted by user");
|
|
304
332
|
}
|
|
305
333
|
} finally {
|
|
306
|
-
|
|
334
|
+
promptDriver.close();
|
|
307
335
|
}
|
|
308
336
|
}
|
|
309
337
|
|
|
@@ -328,3 +356,11 @@ export function isUsageError(error: unknown): error is InitCommandError {
|
|
|
328
356
|
typeof (error as InitCommandError).code === "string"
|
|
329
357
|
);
|
|
330
358
|
}
|
|
359
|
+
|
|
360
|
+
export const __private = {
|
|
361
|
+
collectAnswers,
|
|
362
|
+
createReadlinePromptDriver,
|
|
363
|
+
defaultPluginIdFromDir,
|
|
364
|
+
sanitizeIdToken,
|
|
365
|
+
validatePluginId,
|
|
366
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const test = require("node:test");
|
|
5
|
+
|
|
6
|
+
const { buildPluginAssets, parseBuildArgs, readManifest, validateDeclaredModuleExec } = require("../lib/build");
|
|
7
|
+
const { cleanupTempDir, makeTempDir, writeJson, writeText } = require("./helpers");
|
|
8
|
+
|
|
9
|
+
test("parseBuildArgs uses defaults", () => {
|
|
10
|
+
const parsed = parseBuildArgs([]);
|
|
11
|
+
assert.equal(parsed.pluginDir, process.cwd());
|
|
12
|
+
assert.equal(parsed.verbose, false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("parseBuildArgs parses known flags", () => {
|
|
16
|
+
const parsed = parseBuildArgs(["--plugin-dir", "some/plugin", "--verbose"]);
|
|
17
|
+
assert.equal(parsed.pluginDir, path.resolve("some/plugin"));
|
|
18
|
+
assert.equal(parsed.verbose, true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("parseBuildArgs help returns usage error", () => {
|
|
22
|
+
assert.throws(() => parseBuildArgs(["--help"]), /openvcs build \[args\]/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("buildPluginAssets no-ops for theme-only plugins", () => {
|
|
26
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
27
|
+
const pluginDir = path.join(root, "plugin");
|
|
28
|
+
|
|
29
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), { id: "theme-only" });
|
|
30
|
+
const manifest = buildPluginAssets({ pluginDir, verbose: false });
|
|
31
|
+
|
|
32
|
+
assert.equal(manifest.pluginId, "theme-only");
|
|
33
|
+
cleanupTempDir(root);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("buildPluginAssets requires package.json for code plugins", () => {
|
|
37
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
38
|
+
const pluginDir = path.join(root, "plugin");
|
|
39
|
+
|
|
40
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
41
|
+
id: "missing-package",
|
|
42
|
+
module: { exec: "plugin.js" },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
assert.throws(
|
|
46
|
+
() => buildPluginAssets({ pluginDir, verbose: false }),
|
|
47
|
+
/code plugins must include package\.json/
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
cleanupTempDir(root);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("buildPluginAssets runs build:plugin and validates output", () => {
|
|
54
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
55
|
+
const pluginDir = path.join(root, "plugin");
|
|
56
|
+
|
|
57
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
58
|
+
id: "builder",
|
|
59
|
+
module: { exec: "plugin.js" },
|
|
60
|
+
});
|
|
61
|
+
writeJson(path.join(pluginDir, "package.json"), {
|
|
62
|
+
name: "builder",
|
|
63
|
+
private: true,
|
|
64
|
+
scripts: {
|
|
65
|
+
"build:plugin": "node ./scripts/build-plugin.js",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
writeText(
|
|
69
|
+
path.join(pluginDir, "scripts", "build-plugin.js"),
|
|
70
|
+
"const fs = require('node:fs');\nconst path = require('node:path');\nconst out = path.join(process.cwd(), 'bin', 'plugin.js');\nfs.mkdirSync(path.dirname(out), { recursive: true });\nfs.writeFileSync(out, 'export {};\\n', 'utf8');\n"
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const manifest = buildPluginAssets({ pluginDir, verbose: false });
|
|
74
|
+
|
|
75
|
+
assert.equal(manifest.pluginId, "builder");
|
|
76
|
+
assert.equal(fs.existsSync(path.join(pluginDir, "bin", "plugin.js")), true);
|
|
77
|
+
cleanupTempDir(root);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("readManifest and validateDeclaredModuleExec stay reusable", () => {
|
|
81
|
+
const root = makeTempDir("openvcs-sdk-test");
|
|
82
|
+
const pluginDir = path.join(root, "plugin");
|
|
83
|
+
|
|
84
|
+
writeJson(path.join(pluginDir, "openvcs.plugin.json"), {
|
|
85
|
+
id: "reusable",
|
|
86
|
+
module: { exec: "plugin.js" },
|
|
87
|
+
});
|
|
88
|
+
writeText(path.join(pluginDir, "bin", "plugin.js"), "export {};\n");
|
|
89
|
+
|
|
90
|
+
const manifest = readManifest(pluginDir);
|
|
91
|
+
assert.equal(manifest.moduleExec, "plugin.js");
|
|
92
|
+
assert.doesNotThrow(() => validateDeclaredModuleExec(pluginDir, manifest.moduleExec));
|
|
93
|
+
|
|
94
|
+
cleanupTempDir(root);
|
|
95
|
+
});
|