@contractkit/plugin-bruno 1.1.0 → 1.3.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 +7 -7
- package/.turbo/turbo-test$colon$ci.log +15 -14
- package/CHANGELOG.md +23 -0
- package/README.md +7 -5
- package/coverage/clover.xml +339 -278
- package/coverage/coverage-final.json +3 -2
- package/coverage/index.html +21 -21
- package/coverage/src/codegen-bruno.ts.html +170 -113
- package/coverage/src/index.html +34 -19
- package/coverage/src/index.ts.html +631 -0
- package/coverage/tests/helpers.ts.html +9 -9
- package/coverage/tests/index.html +1 -1
- package/dist/codegen-bruno.d.ts.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +63 -15
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/codegen-bruno.ts +31 -12
- package/src/index.ts +30 -1
- package/tests/codegen-bruno.test.ts +92 -17
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { resolve, basename, dirname } from 'node:path';
|
|
|
2
2
|
import { existsSync, readFileSync, rmSync, readdirSync, rmdirSync } from 'node:fs';
|
|
3
3
|
import { generateOpenCollection, MANIFEST_FILENAME, parseManifest } from './codegen-bruno.js';
|
|
4
4
|
import type { BrunoSecurityScheme } from './codegen-bruno.js';
|
|
5
|
-
import type { ContractKitPlugin } from '@contractkit/core';
|
|
5
|
+
import type { ContractKitPlugin, PluginValue } from '@contractkit/core';
|
|
6
6
|
|
|
7
7
|
/** Configuration accepted by the Bruno plugin, both via `contractkit.config.json` and `createBrunoPlugin`. */
|
|
8
8
|
export interface BrunoPluginConfig {
|
|
@@ -38,9 +38,37 @@ export interface BrunoPluginOptions extends BrunoPluginConfig {
|
|
|
38
38
|
|
|
39
39
|
// ─── Default export: loaded via plugins array, reads config from ctx.options ─
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Validates a `plugins.bruno` extension entry on an operation. The expected shape is
|
|
43
|
+
* `{ template?: string }`, where `template` is a YAML fragment to deep-merge into the
|
|
44
|
+
* generated request file (typically a `file://...` URL whose contents have already
|
|
45
|
+
* been loaded by the CLI resolver).
|
|
46
|
+
*/
|
|
47
|
+
export function validateBrunoExtension(value: PluginValue): { errors?: string[] } | void {
|
|
48
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
49
|
+
return { errors: [`expected an object, got ${describe(value)}`] };
|
|
50
|
+
}
|
|
51
|
+
const errors: string[] = [];
|
|
52
|
+
for (const [key, val] of Object.entries(value)) {
|
|
53
|
+
if (key === 'template') {
|
|
54
|
+
if (typeof val !== 'string') errors.push(`'template' must be a string, got ${describe(val)}`);
|
|
55
|
+
} else {
|
|
56
|
+
errors.push(`unknown field '${key}' (allowed: template)`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return errors.length ? { errors } : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function describe(value: PluginValue): string {
|
|
63
|
+
if (value === null) return 'null';
|
|
64
|
+
if (Array.isArray(value)) return 'array';
|
|
65
|
+
return typeof value;
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
const plugin: ContractKitPlugin = {
|
|
42
69
|
name: 'bruno',
|
|
43
70
|
cacheKey: 'bruno',
|
|
71
|
+
validateExtension: validateBrunoExtension,
|
|
44
72
|
async generateTargets({ opRoots, contractRoots }, ctx) {
|
|
45
73
|
const { auth, ...config } = ctx.options as BrunoPluginOptions;
|
|
46
74
|
const base = config.baseDir ? resolve(ctx.rootDir, config.baseDir) : ctx.rootDir;
|
|
@@ -85,6 +113,7 @@ export function createBrunoPlugin(
|
|
|
85
113
|
return {
|
|
86
114
|
name: 'bruno',
|
|
87
115
|
cacheKey: `bruno:${JSON.stringify(config)}`,
|
|
116
|
+
validateExtension: validateBrunoExtension,
|
|
88
117
|
async generateTargets({ opRoots, contractRoots }, ctx) {
|
|
89
118
|
const base = config.baseDir ? resolve(rootDir, config.baseDir) : rootDir;
|
|
90
119
|
const outDir = resolve(base, config.output ?? 'bruno-collection');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { generateOpenCollection, sanitizePath, MANIFEST_FILENAME, parseManifest, mergePluginFile } from '../src/codegen-bruno.js';
|
|
3
|
+
import { validateBrunoExtension } from '../src/index.js';
|
|
3
4
|
import {
|
|
4
5
|
opRoot,
|
|
5
6
|
opRoute,
|
|
@@ -671,6 +672,48 @@ describe('generateOpenCollection', () => {
|
|
|
671
672
|
expect(env!.content).toContain('- name: token');
|
|
672
673
|
});
|
|
673
674
|
|
|
675
|
+
describe('alphabetical ordering', () => {
|
|
676
|
+
function seqOf(content: string): number | undefined {
|
|
677
|
+
const match = content.match(/seq:\s*(\d+)/);
|
|
678
|
+
return match ? parseInt(match[1]!, 10) : undefined;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
it('orders top-level folders alphabetically by area', () => {
|
|
682
|
+
const usersRoot = opRoot([opRoute('/users', [opOperation('get')])], 'users.op', { area: 'users' });
|
|
683
|
+
const authRoot = opRoot([opRoute('/auth', [opOperation('post')])], 'auth.op', { area: 'auth' });
|
|
684
|
+
const paymentsRoot = opRoot([opRoute('/payments', [opOperation('get')])], 'payments.op', { area: 'payments' });
|
|
685
|
+
// Pass in a non-alphabetical input order to prove the sort is doing the work.
|
|
686
|
+
const files = generateOpenCollection([usersRoot, authRoot, paymentsRoot], { collectionName: 'API' });
|
|
687
|
+
|
|
688
|
+
const auth = files.find(f => f.relativePath === 'auth/folder.yml');
|
|
689
|
+
const payments = files.find(f => f.relativePath === 'payments/folder.yml');
|
|
690
|
+
const users = files.find(f => f.relativePath === 'users/folder.yml');
|
|
691
|
+
expect(seqOf(auth!.content)).toBe(1);
|
|
692
|
+
expect(seqOf(payments!.content)).toBe(2);
|
|
693
|
+
expect(seqOf(users!.content)).toBe(3);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('orders requests within a folder alphabetically by request name', () => {
|
|
697
|
+
const root = opRoot(
|
|
698
|
+
[
|
|
699
|
+
opRoute('/zebras', [opOperation('get', { name: 'Zebra list' })]),
|
|
700
|
+
opRoute('/aardvarks', [opOperation('get', { name: 'Aardvark list' })]),
|
|
701
|
+
opRoute('/mammals', [opOperation('get', { name: 'Mammal list' })]),
|
|
702
|
+
],
|
|
703
|
+
'zoo.op',
|
|
704
|
+
{ area: 'zoo' },
|
|
705
|
+
);
|
|
706
|
+
const files = generateOpenCollection([root], { collectionName: 'API' });
|
|
707
|
+
|
|
708
|
+
const aardvark = files.find(f => f.relativePath === 'zoo/aardvark-list.yml');
|
|
709
|
+
const mammal = files.find(f => f.relativePath === 'zoo/mammal-list.yml');
|
|
710
|
+
const zebra = files.find(f => f.relativePath === 'zoo/zebra-list.yml');
|
|
711
|
+
expect(seqOf(aardvark!.content)).toBe(1);
|
|
712
|
+
expect(seqOf(mammal!.content)).toBe(2);
|
|
713
|
+
expect(seqOf(zebra!.content)).toBe(3);
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
674
717
|
describe('environments config', () => {
|
|
675
718
|
const root = opRoot([opRoute('/users', [opOperation('get')])], 'users.op');
|
|
676
719
|
|
|
@@ -1074,7 +1117,7 @@ describe('plugin file merges', () => {
|
|
|
1074
1117
|
opRoute('/users', [
|
|
1075
1118
|
opOperation('get', {
|
|
1076
1119
|
responses: [opResponse(200, 'User')],
|
|
1077
|
-
|
|
1120
|
+
pluginExtensions: { bruno: { template: 'runtime:\n script:\n req: |\n console.log("pre");\n' } },
|
|
1078
1121
|
}),
|
|
1079
1122
|
]),
|
|
1080
1123
|
]);
|
|
@@ -1092,17 +1135,19 @@ describe('plugin file merges', () => {
|
|
|
1092
1135
|
opRoute('/users', [
|
|
1093
1136
|
opOperation('get', {
|
|
1094
1137
|
responses: [opResponse(200)],
|
|
1095
|
-
|
|
1096
|
-
bruno:
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1138
|
+
pluginExtensions: {
|
|
1139
|
+
bruno: {
|
|
1140
|
+
template: [
|
|
1141
|
+
'runtime:',
|
|
1142
|
+
' assertions:',
|
|
1143
|
+
' - expression: res.status',
|
|
1144
|
+
' operator: eq',
|
|
1145
|
+
' value: "200"',
|
|
1146
|
+
' - expression: res.headers["x-request-id"]',
|
|
1147
|
+
' operator: isDefined',
|
|
1148
|
+
' value: ""',
|
|
1149
|
+
].join('\n'),
|
|
1150
|
+
},
|
|
1106
1151
|
},
|
|
1107
1152
|
}),
|
|
1108
1153
|
]),
|
|
@@ -1119,7 +1164,7 @@ describe('plugin file merges', () => {
|
|
|
1119
1164
|
opRoute('/users', [
|
|
1120
1165
|
opOperation('get', {
|
|
1121
1166
|
responses: [opResponse(200)],
|
|
1122
|
-
|
|
1167
|
+
pluginExtensions: { bruno: { template: 'runtime:\n script:\n req: |\n // pre\n' } },
|
|
1123
1168
|
}),
|
|
1124
1169
|
]),
|
|
1125
1170
|
]);
|
|
@@ -1130,13 +1175,13 @@ describe('plugin file merges', () => {
|
|
|
1130
1175
|
expect(req.content).toContain('url:');
|
|
1131
1176
|
});
|
|
1132
1177
|
|
|
1133
|
-
it('leaves generated content unchanged when
|
|
1178
|
+
it('leaves generated content unchanged when pluginExtensions is absent', () => {
|
|
1134
1179
|
const withoutOverride = opRoot([opRoute('/users', [opOperation('get', { responses: [opResponse(200)] })])]);
|
|
1135
1180
|
const withOverride = opRoot([
|
|
1136
1181
|
opRoute('/users', [
|
|
1137
1182
|
opOperation('get', {
|
|
1138
1183
|
responses: [opResponse(200)],
|
|
1139
|
-
|
|
1184
|
+
pluginExtensions: {},
|
|
1140
1185
|
}),
|
|
1141
1186
|
]),
|
|
1142
1187
|
]);
|
|
@@ -1151,7 +1196,7 @@ describe('plugin file merges', () => {
|
|
|
1151
1196
|
opRoute('/users', [
|
|
1152
1197
|
opOperation('get', {
|
|
1153
1198
|
responses: [opResponse(200)],
|
|
1154
|
-
|
|
1199
|
+
pluginExtensions: { bruno: { template: 'just a scalar string' } },
|
|
1155
1200
|
}),
|
|
1156
1201
|
]),
|
|
1157
1202
|
]);
|
|
@@ -1164,7 +1209,7 @@ describe('plugin file merges', () => {
|
|
|
1164
1209
|
const root = opRoot([
|
|
1165
1210
|
opRoute('/users', [
|
|
1166
1211
|
opOperation('get', {
|
|
1167
|
-
|
|
1212
|
+
pluginExtensions: { bruno: { template: 'info:\n name: Custom Name\n' } },
|
|
1168
1213
|
}),
|
|
1169
1214
|
]),
|
|
1170
1215
|
]);
|
|
@@ -1211,3 +1256,33 @@ describe('mergePluginFile', () => {
|
|
|
1211
1256
|
});
|
|
1212
1257
|
});
|
|
1213
1258
|
|
|
1259
|
+
describe('validateBrunoExtension', () => {
|
|
1260
|
+
it('accepts an empty object', () => {
|
|
1261
|
+
expect(validateBrunoExtension({})).toBeUndefined();
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it('accepts { template: <string> }', () => {
|
|
1265
|
+
expect(validateBrunoExtension({ template: 'runtime: {}' })).toBeUndefined();
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
it('rejects a non-object value', () => {
|
|
1269
|
+
const result = validateBrunoExtension('foo') as { errors: string[] };
|
|
1270
|
+
expect(result.errors[0]).toContain('expected an object');
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
it('rejects an array value', () => {
|
|
1274
|
+
const result = validateBrunoExtension(['x']) as { errors: string[] };
|
|
1275
|
+
expect(result.errors[0]).toContain('array');
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
it('rejects template that is not a string', () => {
|
|
1279
|
+
const result = validateBrunoExtension({ template: 7 }) as { errors: string[] };
|
|
1280
|
+
expect(result.errors[0]).toContain("'template' must be a string");
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
it('rejects unknown fields', () => {
|
|
1284
|
+
const result = validateBrunoExtension({ template: 'x', other: 'y' }) as { errors: string[] };
|
|
1285
|
+
expect(result.errors[0]).toContain("unknown field 'other'");
|
|
1286
|
+
});
|
|
1287
|
+
});
|
|
1288
|
+
|