@contractkit/plugin-bruno 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,6 +55,17 @@ export interface OpenCollectionOptions {
55
55
  * benefit from full coverage. Set to `false` to omit internal ops.
56
56
  */
57
57
  includeInternal?: boolean;
58
+ /**
59
+ * Map of environment name → variables. Each entry produces a
60
+ * `environments/<name>.yml` file with the variables in declaration order.
61
+ * Values are coerced to strings.
62
+ *
63
+ * When omitted, a default `environments/local.yml` is emitted with
64
+ * `baseUrl=http://localhost:3000` plus any auth env-var placeholders the
65
+ * default scheme requires. When provided, the default is replaced entirely
66
+ * — auth vars are not auto-injected, so include them explicitly if needed.
67
+ */
68
+ environments?: Record<string, Record<string, unknown>>;
58
69
  }
59
70
 
60
71
  /**
@@ -72,7 +83,9 @@ export function generateOpenCollection(roots: OpRootNode[], options: OpenCollect
72
83
  const includeInternal = options.includeInternal ?? true;
73
84
 
74
85
  files.push({ relativePath: 'opencollection.yml', content: generateCollectionRoot(options.collectionName, defaultScheme) });
75
- files.push({ relativePath: 'environments/local.yml', content: generateEnvFile(defaultScheme) });
86
+ for (const envFile of generateEnvFiles(options.environments, defaultScheme)) {
87
+ files.push(envFile);
88
+ }
76
89
  // Manifest is appended at the end so it lists every generated path including itself.
77
90
 
78
91
  for (let rootIdx = 0; rootIdx < roots.length; rootIdx++) {
@@ -183,18 +196,38 @@ function generateCollectionRoot(name: string, scheme?: BrunoSecurityScheme): str
183
196
  return lines.join('\n');
184
197
  }
185
198
 
186
- function generateEnvFile(scheme?: BrunoSecurityScheme): string {
187
- const lines = [`name: Local`, `variables:`, ` - name: baseUrl`, ` value: "http://localhost:3000"`];
199
+ function generateEnvFiles(
200
+ environments: Record<string, Record<string, unknown>> | undefined,
201
+ scheme: BrunoSecurityScheme | undefined,
202
+ ): OpenCollectionFile[] {
203
+ if (environments && Object.keys(environments).length > 0) {
204
+ return Object.entries(environments).map(([name, variables]) => ({
205
+ relativePath: `environments/${name}.yml`,
206
+ content: renderEnvFile(displayNameFor(name), variables),
207
+ }));
208
+ }
209
+ const defaultVars: Record<string, string> = { baseUrl: 'http://localhost:3000' };
188
210
  if (scheme) {
189
- for (const varName of authEnvVarNames(scheme)) {
190
- lines.push(` - name: ${varName}`);
191
- lines.push(` value: ""`);
192
- }
211
+ for (const varName of authEnvVarNames(scheme)) defaultVars[varName] = '';
212
+ }
213
+ return [{ relativePath: 'environments/local.yml', content: renderEnvFile('Local', defaultVars) }];
214
+ }
215
+
216
+ function renderEnvFile(name: string, variables: Record<string, unknown>): string {
217
+ const lines = [`name: ${name}`, `variables:`];
218
+ for (const [key, value] of Object.entries(variables)) {
219
+ const escaped = String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
220
+ lines.push(` - name: ${key}`);
221
+ lines.push(` value: "${escaped}"`);
193
222
  }
194
223
  lines.push(``);
195
224
  return lines.join('\n');
196
225
  }
197
226
 
227
+ function displayNameFor(envKey: string): string {
228
+ return envKey.charAt(0).toUpperCase() + envKey.slice(1);
229
+ }
230
+
198
231
  function generateFolderFile(name: string, seq: number): string {
199
232
  return [`info:`, ` name: ${yamlString(name)}`, ` type: folder`, ` seq: ${seq}`, ``].join('\n');
200
233
  }
package/src/index.ts CHANGED
@@ -21,6 +21,14 @@ export interface BrunoPluginConfig {
21
21
  * benefit from full coverage. Set to `false` to omit internal ops.
22
22
  */
23
23
  includeInternal?: boolean;
24
+ /**
25
+ * Map of environment name → variables. Each entry produces a
26
+ * `environments/<name>.yml` file. When omitted, a default `local.yml` is
27
+ * emitted with `baseUrl=http://localhost:3000` and any auth env-var
28
+ * placeholders. When provided, the default is replaced entirely; include
29
+ * auth variables (e.g. `token`) explicitly if you need them.
30
+ */
31
+ environments?: Record<string, Record<string, unknown>>;
24
32
  }
25
33
 
26
34
  /** Full plugin options shape read from `ctx.options` — extends {@link BrunoPluginConfig} with the `auth` block. */
@@ -47,6 +55,7 @@ const plugin: ContractKitPlugin = {
47
55
  auth,
48
56
  randomExamples: config.randomExamples ?? true,
49
57
  includeInternal: config.includeInternal,
58
+ environments: config.environments,
50
59
  });
51
60
  for (const { relativePath, content } of files) {
52
61
  ctx.emitFile(resolve(outDir, relativePath), content);
@@ -88,6 +97,8 @@ export function createBrunoPlugin(
88
97
  contractRoots,
89
98
  auth,
90
99
  randomExamples: config.randomExamples ?? true,
100
+ includeInternal: config.includeInternal,
101
+ environments: config.environments,
91
102
  });
92
103
  for (const { relativePath, content } of files) {
93
104
  ctx.emitFile(resolve(outDir, relativePath), content);
@@ -671,6 +671,61 @@ describe('generateOpenCollection', () => {
671
671
  expect(env!.content).toContain('- name: token');
672
672
  });
673
673
 
674
+ describe('environments config', () => {
675
+ const root = opRoot([opRoute('/users', [opOperation('get')])], 'users.op');
676
+
677
+ it('emits one file per entry, replacing the default local.yml', () => {
678
+ const files = generateOpenCollection([root], {
679
+ collectionName: 'API',
680
+ environments: {
681
+ local: { baseUrl: 'http://localhost:3000', token: '' },
682
+ staging: { baseUrl: 'https://staging.example.com', token: 'secret' },
683
+ },
684
+ });
685
+ const local = files.find(f => f.relativePath === 'environments/local.yml');
686
+ const staging = files.find(f => f.relativePath === 'environments/staging.yml');
687
+ expect(local).toBeDefined();
688
+ expect(staging).toBeDefined();
689
+ expect(local!.content).toContain('name: Local');
690
+ expect(local!.content).toContain('- name: baseUrl');
691
+ expect(local!.content).toContain('value: "http://localhost:3000"');
692
+ expect(local!.content).toContain('- name: token');
693
+ expect(staging!.content).toContain('name: Staging');
694
+ expect(staging!.content).toContain('value: "https://staging.example.com"');
695
+ expect(staging!.content).toContain('value: "secret"');
696
+ });
697
+
698
+ it('does not auto-inject auth env vars when environments is provided', () => {
699
+ const files = generateOpenCollection([root], {
700
+ collectionName: 'API',
701
+ auth: bearerAuth,
702
+ environments: {
703
+ local: { baseUrl: 'http://localhost:3000' },
704
+ },
705
+ });
706
+ const local = files.find(f => f.relativePath === 'environments/local.yml');
707
+ expect(local!.content).not.toContain('- name: token');
708
+ });
709
+
710
+ it('falls back to the default local.yml when environments is empty', () => {
711
+ const files = generateOpenCollection([root], { collectionName: 'API', environments: {} });
712
+ const local = files.find(f => f.relativePath === 'environments/local.yml');
713
+ expect(local).toBeDefined();
714
+ expect(local!.content).toContain('name: Local');
715
+ expect(local!.content).toContain('value: "http://localhost:3000"');
716
+ });
717
+
718
+ it('coerces non-string values to strings', () => {
719
+ const files = generateOpenCollection([root], {
720
+ collectionName: 'API',
721
+ environments: { local: { port: 3000, debug: true } as unknown as Record<string, unknown> },
722
+ });
723
+ const local = files.find(f => f.relativePath === 'environments/local.yml');
724
+ expect(local!.content).toContain('value: "3000"');
725
+ expect(local!.content).toContain('value: "true"');
726
+ });
727
+ });
728
+
674
729
  it('does not add request or auth to opencollection.yml when no security config', () => {
675
730
  const root = opRoot([opRoute('/users', [opOperation('get')])], 'users.op');
676
731
  const files = generateOpenCollection([root], { collectionName: 'API' });