@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.
- package/.turbo/turbo-build$colon$ci.log +19 -35
- package/.turbo/turbo-test$colon$ci.log +26 -128
- package/CHANGELOG.md +21 -0
- package/README.md +49 -0
- package/coverage/clover.xml +335 -275
- package/coverage/coverage-final.json +3 -2
- package/coverage/index.html +29 -29
- package/coverage/src/codegen-bruno.ts.html +242 -110
- package/coverage/src/index.html +33 -18
- package/coverage/src/index.ts.html +562 -0
- package/coverage/tests/helpers.ts.html +28 -28
- package/coverage/tests/index.html +19 -19
- package/dist/codegen-bruno.d.ts +15 -0
- package/dist/codegen-bruno.d.ts.map +1 -1
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -6
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/codegen-bruno.ts +52 -8
- package/src/index.ts +32 -3
- package/tests/codegen-bruno.test.ts +220 -3
- package/.turbo/turbo-build.log +0 -15
- package/.turbo/turbo-test.log +0 -14
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
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('
|
|
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"
|
|
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
|
+
});
|
package/.turbo/turbo-build.log
DELETED
|
@@ -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
|
package/.turbo/turbo-test.log
DELETED
|
@@ -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
|
-
[1m[30m[46m RUN [49m[39m[22m [36mv4.1.5 [39m[90m/Users/robert/projects/contractkit/packages/plugin-bruno[39m
|
|
7
|
-
|
|
8
|
-
[32m✓[39m tests/codegen-bruno.test.ts [2m([22m[2m85 tests[22m[2m)[22m[32m 9[2mms[22m[39m
|
|
9
|
-
|
|
10
|
-
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
11
|
-
[2m Tests [22m [1m[32m85 passed[39m[22m[90m (85)[39m
|
|
12
|
-
[2m Start at [22m 08:51:33
|
|
13
|
-
[2m Duration [22m 414ms[2m (transform 136ms, setup 0ms, import 255ms, tests 9ms, environment 0ms)[22m
|
|
14
|
-
|