@contractkit/plugin-bruno 0.9.1

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/src/index.ts ADDED
@@ -0,0 +1,130 @@
1
+ import { resolve, basename, dirname } from 'node:path';
2
+ import { existsSync, readFileSync, rmSync, readdirSync, rmdirSync } from 'node:fs';
3
+ import { generateOpenCollection, MANIFEST_FILENAME, parseManifest } from './codegen-bruno.js';
4
+ import type { BrunoSecurityScheme } from './codegen-bruno.js';
5
+ import type { ContractKitPlugin } from '@contractkit/core';
6
+
7
+ export interface BrunoPluginConfig {
8
+ baseDir?: string;
9
+ output?: string;
10
+ collectionName?: string;
11
+ /**
12
+ * When true (default), example values use Bruno's faker templates
13
+ * (`{{$randomUUID}}`, `{{$randomEmail}}`, etc.) so each send produces
14
+ * fresh data. Set to false for deterministic placeholders.
15
+ */
16
+ randomExamples?: boolean;
17
+ /**
18
+ * Whether to generate request files for operations marked `internal`. Defaults to
19
+ * `true` — Bruno collections are typically used by the team that owns the API and
20
+ * benefit from full coverage. Set to `false` to omit internal ops.
21
+ */
22
+ includeInternal?: boolean;
23
+ }
24
+
25
+ export interface BrunoPluginOptions extends BrunoPluginConfig {
26
+ auth?: { defaultScheme: string; schemes?: Record<string, BrunoSecurityScheme> };
27
+ }
28
+
29
+ // ─── Default export: loaded via plugins array, reads config from ctx.options ─
30
+
31
+ const plugin: ContractKitPlugin = {
32
+ name: 'bruno',
33
+ cacheKey: 'bruno',
34
+ async generateTargets({ opRoots, contractRoots }, ctx) {
35
+ const { auth, ...config } = ctx.options as BrunoPluginOptions;
36
+ const base = config.baseDir ? resolve(ctx.rootDir, config.baseDir) : ctx.rootDir;
37
+ const outDir = resolve(base, config.output ?? 'bruno-collection');
38
+ const collectionName = config.collectionName ?? basename(ctx.rootDir);
39
+
40
+ cleanupTrackedFiles(outDir);
41
+
42
+ const files = generateOpenCollection(opRoots, {
43
+ collectionName,
44
+ contractRoots,
45
+ auth,
46
+ randomExamples: config.randomExamples ?? true,
47
+ includeInternal: config.includeInternal,
48
+ });
49
+ for (const { relativePath, content } of files) {
50
+ ctx.emitFile(resolve(outDir, relativePath), content);
51
+ }
52
+ },
53
+ };
54
+
55
+ export default plugin;
56
+
57
+ // ─── Factory: for programmatic use with explicit config ────────────────────
58
+
59
+ export function createBrunoPlugin(
60
+ config: BrunoPluginConfig,
61
+ rootDir: string,
62
+ auth?: { defaultScheme: string; schemes?: Record<string, BrunoSecurityScheme> },
63
+ ): ContractKitPlugin {
64
+ return {
65
+ name: 'bruno',
66
+ cacheKey: `bruno:${JSON.stringify(config)}`,
67
+ async generateTargets({ opRoots, contractRoots }, ctx) {
68
+ const base = config.baseDir ? resolve(rootDir, config.baseDir) : rootDir;
69
+ const outDir = resolve(base, config.output ?? 'bruno-collection');
70
+ const collectionName = config.collectionName ?? basename(rootDir);
71
+
72
+ cleanupTrackedFiles(outDir);
73
+
74
+ const files = generateOpenCollection(opRoots, {
75
+ collectionName,
76
+ contractRoots,
77
+ auth,
78
+ randomExamples: config.randomExamples ?? true,
79
+ });
80
+ for (const { relativePath, content } of files) {
81
+ ctx.emitFile(resolve(outDir, relativePath), content);
82
+ }
83
+ },
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Delete files this plugin generated on the previous run, leaving anything
89
+ * the user added (custom .bru files, scripts, secrets, etc.) untouched.
90
+ *
91
+ * On first run — or after manual deletion of the manifest — nothing is
92
+ * removed; stale files from prior versions linger until manually cleaned.
93
+ */
94
+ function cleanupTrackedFiles(outDir: string): void {
95
+ const manifestPath = resolve(outDir, MANIFEST_FILENAME);
96
+ if (!existsSync(manifestPath)) return;
97
+
98
+ let tracked: string[];
99
+ try {
100
+ tracked = parseManifest(readFileSync(manifestPath, 'utf-8'));
101
+ } catch {
102
+ return;
103
+ }
104
+
105
+ const removedDirs = new Set<string>();
106
+ for (const rel of tracked) {
107
+ const abs = resolve(outDir, rel);
108
+ if (existsSync(abs)) {
109
+ rmSync(abs, { force: true });
110
+ removedDirs.add(dirname(abs));
111
+ }
112
+ }
113
+
114
+ // Walk up from each affected directory and remove it if empty, stopping at outDir.
115
+ for (const dir of removedDirs) {
116
+ let current = dir;
117
+ while (current.startsWith(outDir) && current !== outDir) {
118
+ try {
119
+ if (readdirSync(current).length === 0) {
120
+ rmdirSync(current);
121
+ current = dirname(current);
122
+ } else {
123
+ break;
124
+ }
125
+ } catch {
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ }