@contractkit/plugin-bruno 0.9.1 → 0.10.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.
@@ -1,5 +1,9 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { generateOpenCollection, sanitizePath, MANIFEST_FILENAME, parseManifest } from '../src/codegen-bruno.js';
2
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { generateOpenCollection, sanitizePath, MANIFEST_FILENAME, parseManifest, mergePluginFile } from '../src/codegen-bruno.js';
6
+ import { createBrunoPlugin } from '../src/index.js';
3
7
  import {
4
8
  opRoot,
5
9
  opRoute,
@@ -483,7 +487,7 @@ describe('generateOpenCollection', () => {
483
487
  expect(yml!.content).toContain('"name": ""');
484
488
  });
485
489
 
486
- it('sets optional fields to null in expanded ref body', () => {
490
+ it('omits optional fields with no default from body', () => {
487
491
  const userModel = model('CreateUserInput', [
488
492
  field('name', scalarType('string')),
489
493
  field('nickname', scalarType('string'), { optional: true }),
@@ -495,7 +499,7 @@ describe('generateOpenCollection', () => {
495
499
  const files = generateOpenCollection([root], { collectionName: 'API', contractRoots: [contractRoot([userModel])] });
496
500
  const yml = files.find(f => f.relativePath === 'users/post-users.yml');
497
501
  expect(yml!.content).toContain('"name": ""');
498
- expect(yml!.content).toContain('"nickname": null');
502
+ expect(yml!.content).not.toContain('"nickname"');
499
503
  });
500
504
 
501
505
  it('expands inherited fields from base model in ref body', () => {
@@ -1006,3 +1010,216 @@ describe('sanitizePath', () => {
1006
1010
  expect(sanitizePath('/users//posts')).toBe('users-posts');
1007
1011
  });
1008
1012
  });
1013
+
1014
+ describe('plugin file merges', () => {
1015
+ function getRequestFile(files: ReturnType<typeof generateOpenCollection>): { relativePath: string; content: string } {
1016
+ const f = files.find(f => !['opencollection.yml', 'environments/local.yml', MANIFEST_FILENAME].includes(f.relativePath) && !f.relativePath.endsWith('folder.yml'));
1017
+ if (!f) throw new Error('no request file found');
1018
+ return f;
1019
+ }
1020
+
1021
+ it('deep-merges object override into generated request file', () => {
1022
+ const root = opRoot([
1023
+ opRoute('/users', [
1024
+ opOperation('get', {
1025
+ responses: [opResponse(200, 'User')],
1026
+ pluginFiles: { bruno: 'runtime:\n script:\n req: |\n console.log("pre");\n' },
1027
+ }),
1028
+ ]),
1029
+ ]);
1030
+ const files = generateOpenCollection([root], { collectionName: 'API' });
1031
+ const req = getRequestFile(files);
1032
+ // injected key from override
1033
+ expect(req.content).toContain('script:');
1034
+ expect(req.content).toContain('console.log("pre")');
1035
+ // generated key survives (assertions from the 200 response)
1036
+ expect(req.content).toContain('assertions:');
1037
+ });
1038
+
1039
+ it('replaces arrays in override rather than appending', () => {
1040
+ const root = opRoot([
1041
+ opRoute('/users', [
1042
+ opOperation('get', {
1043
+ responses: [opResponse(200)],
1044
+ pluginFiles: {
1045
+ bruno: [
1046
+ 'runtime:',
1047
+ ' assertions:',
1048
+ ' - expression: res.status',
1049
+ ' operator: eq',
1050
+ ' value: "200"',
1051
+ ' - expression: res.headers["x-request-id"]',
1052
+ ' operator: isDefined',
1053
+ ' value: ""',
1054
+ ].join('\n'),
1055
+ },
1056
+ }),
1057
+ ]),
1058
+ ]);
1059
+ const files = generateOpenCollection([root], { collectionName: 'API' });
1060
+ const req = getRequestFile(files);
1061
+ // Count assertion blocks — should be exactly 2 (override replaces, not appends)
1062
+ const matches = req.content.match(/operator:/g);
1063
+ expect(matches).toHaveLength(2);
1064
+ });
1065
+
1066
+ it('preserves sibling keys not touched by override', () => {
1067
+ const root = opRoot([
1068
+ opRoute('/users', [
1069
+ opOperation('get', {
1070
+ responses: [opResponse(200)],
1071
+ pluginFiles: { bruno: 'runtime:\n script:\n req: |\n // pre\n' },
1072
+ }),
1073
+ ]),
1074
+ ]);
1075
+ const files = generateOpenCollection([root], { collectionName: 'API' });
1076
+ const req = getRequestFile(files);
1077
+ // Original http block must still be present
1078
+ expect(req.content).toContain('method: GET');
1079
+ expect(req.content).toContain('url:');
1080
+ });
1081
+
1082
+ it('leaves generated content unchanged when pluginFiles is absent', () => {
1083
+ const withoutOverride = opRoot([opRoute('/users', [opOperation('get', { responses: [opResponse(200)] })])]);
1084
+ const withOverride = opRoot([
1085
+ opRoute('/users', [
1086
+ opOperation('get', {
1087
+ responses: [opResponse(200)],
1088
+ pluginFiles: {},
1089
+ }),
1090
+ ]),
1091
+ ]);
1092
+ const filesWithout = generateOpenCollection([withoutOverride], { collectionName: 'API' });
1093
+ const filesWith = generateOpenCollection([withOverride], { collectionName: 'API' });
1094
+ expect(getRequestFile(filesWithout).content).toBe(getRequestFile(filesWith).content);
1095
+ });
1096
+
1097
+ it('ignores a malformed (non-mapping) plugin file and returns generated content unchanged', () => {
1098
+ const withoutOverride = opRoot([opRoute('/users', [opOperation('get', { responses: [opResponse(200)] })])]);
1099
+ const withBadOverride = opRoot([
1100
+ opRoute('/users', [
1101
+ opOperation('get', {
1102
+ responses: [opResponse(200)],
1103
+ pluginFiles: { bruno: 'just a scalar string' },
1104
+ }),
1105
+ ]),
1106
+ ]);
1107
+ const filesWithout = generateOpenCollection([withoutOverride], { collectionName: 'API' });
1108
+ const filesWith = generateOpenCollection([withBadOverride], { collectionName: 'API' });
1109
+ expect(getRequestFile(filesWithout).content).toBe(getRequestFile(filesWith).content);
1110
+ });
1111
+
1112
+ it('scalar override value replaces generated value', () => {
1113
+ const root = opRoot([
1114
+ opRoute('/users', [
1115
+ opOperation('get', {
1116
+ pluginFiles: { bruno: 'info:\n name: Custom Name\n' },
1117
+ }),
1118
+ ]),
1119
+ ]);
1120
+ const files = generateOpenCollection([root], { collectionName: 'API' });
1121
+ const req = getRequestFile(files);
1122
+ expect(req.content).toContain('name: Custom Name');
1123
+ });
1124
+ });
1125
+
1126
+ describe('mergePluginFile', () => {
1127
+ it('merges override object keys into generated YAML', () => {
1128
+ const base = 'info:\n name: Original\n type: http\n';
1129
+ const override = 'info:\n name: Overridden\n';
1130
+ const result = mergePluginFile(base, override);
1131
+ expect(result).toContain('name: Overridden');
1132
+ expect(result).toContain('type: http');
1133
+ });
1134
+
1135
+ it('replaces arrays in the override rather than appending', () => {
1136
+ const base = 'runtime:\n assertions:\n - expression: res.status\n operator: eq\n value: "200"\n';
1137
+ const override = 'runtime:\n assertions:\n - expression: res.status\n operator: eq\n value: "201"\n - expression: res.status\n operator: eq\n value: "202"\n';
1138
+ const result = mergePluginFile(base, override);
1139
+ const matches = result.match(/operator:/g);
1140
+ expect(matches).toHaveLength(2);
1141
+ expect(result).not.toContain('"200"');
1142
+ });
1143
+
1144
+ it('returns generated YAML unchanged when override is a scalar', () => {
1145
+ const base = 'info:\n name: Original\n';
1146
+ expect(mergePluginFile(base, 'just a scalar')).toBe(base);
1147
+ });
1148
+
1149
+ it('returns generated YAML unchanged when override is an array', () => {
1150
+ const base = 'info:\n name: Original\n';
1151
+ expect(mergePluginFile(base, '- a\n- b\n')).toBe(base);
1152
+ });
1153
+
1154
+ it('adds keys from override that are absent in the generated YAML', () => {
1155
+ const base = 'http:\n method: GET\n';
1156
+ const override = 'runtime:\n script:\n req: |\n console.log("hi");\n';
1157
+ const result = mergePluginFile(base, override);
1158
+ expect(result).toContain('method: GET');
1159
+ expect(result).toContain('script:');
1160
+ });
1161
+ });
1162
+
1163
+ describe('overrideDir', () => {
1164
+ it('merges a file from overrideDir into the matching generated file', async () => {
1165
+ const dir = join(tmpdir(), `ck-bruno-test-${Date.now()}`);
1166
+ const overrideDir = join(dir, 'overrides');
1167
+ const outDir = join(dir, 'out');
1168
+ mkdirSync(join(overrideDir, 'users'), { recursive: true });
1169
+ mkdirSync(outDir, { recursive: true });
1170
+
1171
+ writeFileSync(join(overrideDir, 'users', 'get-users.yml'), 'runtime:\n script:\n req: |\n console.log("injected");\n');
1172
+
1173
+ const emitted: Record<string, string> = {};
1174
+ const ctx = {
1175
+ rootDir: dir,
1176
+ options: {},
1177
+ emitFile(path: string, content: string) {
1178
+ emitted[path] = content;
1179
+ },
1180
+ };
1181
+
1182
+ const plugin = createBrunoPlugin({ output: 'out', overrideDir: 'overrides' }, dir);
1183
+ const root = opRoot([opRoute('/users', [opOperation('get')])]);
1184
+ await plugin.generateTargets!({ opRoots: [root], contractRoots: [], modelsWithInput: new Set(), modelsWithOutput: new Set() }, ctx);
1185
+
1186
+ const requestPath = Object.keys(emitted).find(p => p.endsWith('get-users.yml'));
1187
+ expect(requestPath).toBeDefined();
1188
+ expect(emitted[requestPath!]).toContain('injected');
1189
+
1190
+ rmSync(dir, { recursive: true, force: true });
1191
+ });
1192
+
1193
+ it('leaves generated files unchanged when no matching override file exists', async () => {
1194
+ const dir = join(tmpdir(), `ck-bruno-test-${Date.now()}`);
1195
+ const overrideDir = join(dir, 'overrides');
1196
+ const outDir = join(dir, 'out');
1197
+ mkdirSync(overrideDir, { recursive: true });
1198
+ mkdirSync(outDir, { recursive: true });
1199
+
1200
+ const emitted: Record<string, string> = {};
1201
+ const ctx = {
1202
+ rootDir: dir,
1203
+ options: {},
1204
+ emitFile(path: string, content: string) {
1205
+ emitted[path] = content;
1206
+ },
1207
+ };
1208
+
1209
+ const pluginWithOverride = createBrunoPlugin({ output: 'out', overrideDir: 'overrides' }, dir);
1210
+ const pluginWithout = createBrunoPlugin({ output: 'out' }, dir);
1211
+ const root = opRoot([opRoute('/users', [opOperation('get')])]);
1212
+ const inputs = { opRoots: [root], contractRoots: [], modelsWithInput: new Set<string>(), modelsWithOutput: new Set<string>() };
1213
+
1214
+ const emittedWith: Record<string, string> = {};
1215
+ await pluginWithOverride.generateTargets!(inputs, { rootDir: dir, options: {}, emitFile: (p, c) => { emittedWith[p] = c; } });
1216
+
1217
+ const emittedWithout: Record<string, string> = {};
1218
+ await pluginWithout.generateTargets!(inputs, { rootDir: dir, options: {}, emitFile: (p, c) => { emittedWithout[p] = c; } });
1219
+
1220
+ const requestPath = Object.keys(emittedWithout).find(p => p.endsWith('get-users.yml'))!;
1221
+ expect(emittedWith[requestPath]).toBe(emittedWithout[requestPath]);
1222
+
1223
+ rmSync(dir, { recursive: true, force: true });
1224
+ });
1225
+ });
@@ -1,15 +0,0 @@
1
-
2
- > @contractkit/plugin-bruno@0.9.0 build /Users/robert/projects/contractkit/packages/plugin-bruno
3
- > tsup src/index.ts --format esm --sourcemap --dts && tsc --emitDeclarationOnly --declaration
4
-
5
- CLI Building entry: src/index.ts
6
- CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.1
8
- CLI Target: esnext
9
- ESM Build start
10
- ESM dist/index.js 19.94 KB
11
- ESM dist/index.js.map 46.49 KB
12
- ESM ⚡️ Build success in 69ms
13
- DTS Build start
14
- DTS ⚡️ Build success in 594ms
15
- DTS dist/index.d.ts 1.37 KB
@@ -1,14 +0,0 @@
1
-
2
- > @contractkit/plugin-bruno@0.9.0 test /Users/robert/projects/contractkit/packages/plugin-bruno
3
- > vitest run
4
-
5
-
6
-  RUN  v4.1.5 /Users/robert/projects/contractkit/packages/plugin-bruno
7
-
8
- ✓ tests/codegen-bruno.test.ts (85 tests) 9ms
9
-
10
-  Test Files  1 passed (1)
11
-  Tests  85 passed (85)
12
-  Start at  08:51:33
13
-  Duration  414ms (transform 136ms, setup 0ms, import 255ms, tests 9ms, environment 0ms)
14
-