@contractkit/plugin-bruno 1.5.12 → 1.6.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 +4 -4
- package/.turbo/turbo-test$colon$ci.log +7 -7
- package/CHANGELOG.md +71 -0
- package/dist/codegen-bruno.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/codegen-bruno.ts +6 -0
- package/tests/codegen-bruno.test.ts +19 -0
|
@@ -5,9 +5,9 @@ $ tsup src/index.ts --format esm --sourcemap --dts && tsc --emitDeclarationOnly
|
|
|
5
5
|
[34mCLI[39m tsup v8.5.1
|
|
6
6
|
[34mCLI[39m Target: esnext
|
|
7
7
|
[34mESM[39m Build start
|
|
8
|
-
[32mESM[39m [1mdist/index.js [22m[32m27.
|
|
9
|
-
[32mESM[39m [1mdist/index.js.map [22m[32m71.
|
|
10
|
-
[32mESM[39m ⚡️ Build success in
|
|
8
|
+
[32mESM[39m [1mdist/index.js [22m[32m27.68 KB[39m
|
|
9
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m71.65 KB[39m
|
|
10
|
+
[32mESM[39m ⚡️ Build success in 153ms
|
|
11
11
|
[34mDTS[39m Build start
|
|
12
|
-
[32mDTS[39m ⚡️ Build success in
|
|
12
|
+
[32mDTS[39m ⚡️ Build success in 2817ms
|
|
13
13
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m2.95 KB[39m
|
|
@@ -3,20 +3,20 @@ $ vitest run --coverage
|
|
|
3
3
|
[1m[30m[46m RUN [49m[39m[22m [36mv4.1.5 [39m[90m/home/runner/work/ContractKit/ContractKit/packages/plugin-bruno[39m
|
|
4
4
|
[2mCoverage enabled with [22m[33mv8[39m
|
|
5
5
|
|
|
6
|
-
[32m✓[39m tests/codegen-bruno.test.ts [2m([22m[
|
|
6
|
+
[32m✓[39m tests/codegen-bruno.test.ts [2m([22m[2m123 tests[22m[2m)[22m[33m 627[2mms[22m[39m
|
|
7
7
|
|
|
8
8
|
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
9
|
-
[2m Tests [22m [1m[
|
|
10
|
-
[2m Start at [22m
|
|
11
|
-
[2m Duration [22m
|
|
9
|
+
[2m Tests [22m [1m[32m123 passed[39m[22m[90m (123)[39m
|
|
10
|
+
[2m Start at [22m 17:30:03
|
|
11
|
+
[2m Duration [22m 4.80s[2m (transform 1.51s, setup 0ms, import 3.49s, tests 627ms, environment 0ms)[22m
|
|
12
12
|
|
|
13
13
|
[34m % [39m[2mCoverage report from [22m[33mv8[39m
|
|
14
14
|
-------------------|---------|----------|---------|---------|---------------------------------------------------------------------------------------------------------
|
|
15
15
|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
16
16
|
-------------------|---------|----------|---------|---------|---------------------------------------------------------------------------------------------------------
|
|
17
|
-
All files |
|
|
18
|
-
src |
|
|
19
|
-
codegen-bruno.ts | 89.
|
|
17
|
+
All files | 81.06 | 76.17 | 82.1 | 81.54 |
|
|
18
|
+
src | 81.4 | 75.65 | 82.85 | 81.49 |
|
|
19
|
+
codegen-bruno.ts | 89.35 | 79.67 | 93.33 | 89.94 | 337-338,549-550,634,642,695-698,705-712,732,740,744-746,762,772,796,810-812,818-820,825,829-837,846,850
|
|
20
20
|
index.ts | 26.78 | 40 | 20 | 25.49 | 76-203
|
|
21
21
|
tests | 77.77 | 81.08 | 80 | 82.05 |
|
|
22
22
|
helpers.ts | 77.77 | 81.08 | 80 | 82.05 | 43-47,59,71,113,136,148
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# @contractkit/contractkit-plugin-bruno
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e102a2c: Add a `decimal` scalar, for money and anything else that has to be exact
|
|
8
|
+
|
|
9
|
+
`number` compiles to `z.coerce.number()` — an IEEE-754 double. Anything monetary has to be exact,
|
|
10
|
+
so contracts either lied about their types or routed the value through `string` by hand. `decimal`
|
|
11
|
+
gives the language a type for it:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
contract Payslip: {
|
|
15
|
+
gross: decimal(min=0, scale=2)
|
|
16
|
+
rate: decimal
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A decimal travels as a **quoted JSON string** (`{"gross": "1250.00"}`) and becomes a decimal.js
|
|
21
|
+
`Decimal` — the same class Prisma hands you for a `Decimal` column, so a value moves between the
|
|
22
|
+
two with no conversion. Python gets `decimal.Decimal`; OpenAPI gets `type: string, format: decimal`.
|
|
23
|
+
|
|
24
|
+
A raw JSON number is **rejected**, not coerced. By the time one reaches the schema it has already
|
|
25
|
+
been through a double, which is precisely the loss the scalar exists to prevent, so accepting it
|
|
26
|
+
would defeat the point silently.
|
|
27
|
+
|
|
28
|
+
**`scale=` is a validation constraint — at most N decimal places — not a formatting directive.**
|
|
29
|
+
It cannot be one: the router assigns `ctx.body` and Koa serializes it with a `JSON.stringify` we
|
|
30
|
+
have no replacer for, so the wire form is whatever decimal.js normalizes to and `"1250.00"` reads
|
|
31
|
+
back as `"1250"`. The two are the same number; format at the display edge if you need the trailing
|
|
32
|
+
zeros. This is also what `scale` means in OpenAPI `pattern`, pydantic `condecimal(decimal_places=)`
|
|
33
|
+
and Prisma `@db.Decimal(_, n)`, so every downstream mapping stays honest.
|
|
34
|
+
|
|
35
|
+
Generated code sets `Decimal.set({ toExpNeg: -9e15, toExpPos: 9e15 })` so values never serialize in
|
|
36
|
+
exponential notation — without it `0.00000001` ships as `"1e-8"` and any peer validating
|
|
37
|
+
`^-?\d+(\.\d+)?$` rejects it. Note this is global decimal.js configuration and affects every
|
|
38
|
+
`Decimal` in the consuming process.
|
|
39
|
+
|
|
40
|
+
SDK clients rehydrate decimals through generated `reviveX` functions. The `bigint` approach does
|
|
41
|
+
not transfer — it works only because bigint invented a tagged `"123n"` wire encoding, and tagging a
|
|
42
|
+
decimal would corrupt the format for every non-ContractKit consumer. Re-parsing responses through
|
|
43
|
+
the Zod schema is not available either: `XOutput` is a type alias with no runtime value, and models
|
|
44
|
+
default to `z.strictObject`, so any field the server added would throw in every deployed client.
|
|
45
|
+
The revivers mutate in place, which preserves unknown server-added keys.
|
|
46
|
+
|
|
47
|
+
Two placements are rejected at parse time, both errors rather than warnings since no existing
|
|
48
|
+
contract can be relying on them. A decimal inside an **undiscriminated union** cannot be rehydrated
|
|
49
|
+
— the SDK has no way to tell which arm arrived, and a convert-if-string fallback would silently
|
|
50
|
+
rewrite a genuine `string` field in a sibling arm. A decimal in a **response header** has no
|
|
51
|
+
parsing step at all, so the annotation would simply be false at runtime.
|
|
52
|
+
|
|
53
|
+
`min`/`max` are kept as exact decimal strings rather than coerced through `Number()`, and OpenAPI
|
|
54
|
+
carries them in `x-contractkit-min`/`-max` extensions, since JSON Schema's numeric `minimum` and
|
|
55
|
+
`maximum` are ignored on a string type. A contract round-trips through OpenAPI and back with its
|
|
56
|
+
bounds intact.
|
|
57
|
+
|
|
58
|
+
SDKs that scaffold a `package.json` gain `decimal.js` as a dependency when a covered model uses the
|
|
59
|
+
scalar. Existing scaffolds are write-once and are not updated, so add it by hand there.
|
|
60
|
+
|
|
61
|
+
### Patch Changes
|
|
62
|
+
|
|
63
|
+
- Updated dependencies [e102a2c]
|
|
64
|
+
- @contractkit/core@0.28.0
|
|
65
|
+
|
|
66
|
+
## 1.5.13
|
|
67
|
+
|
|
68
|
+
### Patch Changes
|
|
69
|
+
|
|
70
|
+
- Updated dependencies [aea5e21]
|
|
71
|
+
- Updated dependencies [5dc2693]
|
|
72
|
+
- @contractkit/core@0.27.0
|
|
73
|
+
|
|
3
74
|
## 1.5.12
|
|
4
75
|
|
|
5
76
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codegen-bruno.d.ts","sourceRoot":"","sources":["../src/codegen-bruno.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,UAAU,EAMV,gBAAgB,EAGhB,mBAAmB,EAEnB,iBAAiB,IAAI,qBAAqB,EAC7C,MAAM,mBAAmB,CAAC;AAgB3B,iGAAiG;AACjG,MAAM,WAAW,kBAAkB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,8JAA8J;AAC9J,eAAO,MAAM,iBAAiB,qCAAqC,CAAC;AAEpE;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC,mFAAmF;AACnF,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;CACf;AAED,kHAAkH;AAClH,MAAM,WAAW,gBAAgB;IAC7B,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CACjD;AAED,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACnC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC1D;AAiBD;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,mBAAmB,CAAC;AAEhD,uKAAuK;AACvK,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,qBAAqB,EAAE,kBAAkB,CAAC;IACtF,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,qBAAqB,GAAG,kBAAkB,EAAE,CAGhH;AAED;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAC7C,KAAK,EAAE,UAAU,EAAE,EACnB,OAAO,EAAE,qBAAqB,EAC9B,YAAY,EAAE,mBAAmB,EACjC,UAAU,GAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAoB,GAC3D,iBAAiB,CA8EnB;AAkHD,uGAAuG;AACvG,wBAAgB,aAAa,IAAI,mBAAmB,CAEnD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,CAmBlE;AAsBD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAOxF;
|
|
1
|
+
{"version":3,"file":"codegen-bruno.d.ts","sourceRoot":"","sources":["../src/codegen-bruno.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,UAAU,EAMV,gBAAgB,EAGhB,mBAAmB,EAEnB,iBAAiB,IAAI,qBAAqB,EAC7C,MAAM,mBAAmB,CAAC;AAgB3B,iGAAiG;AACjG,MAAM,WAAW,kBAAkB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,8JAA8J;AAC9J,eAAO,MAAM,iBAAiB,qCAAqC,CAAC;AAEpE;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC,mFAAmF;AACnF,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;CACf;AAED,kHAAkH;AAClH,MAAM,WAAW,gBAAgB;IAC7B,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CACjD;AAED,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACnC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC1D;AAiBD;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,mBAAmB,CAAC;AAEhD,uKAAuK;AACvK,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,qBAAqB,EAAE,kBAAkB,CAAC;IACtF,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,qBAAqB,GAAG,kBAAkB,EAAE,CAGhH;AAED;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAC7C,KAAK,EAAE,UAAU,EAAE,EACnB,OAAO,EAAE,qBAAqB,EAC9B,YAAY,EAAE,mBAAmB,EACjC,UAAU,GAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAoB,GAC3D,iBAAiB,CA8EnB;AAkHD,uGAAuG;AACvG,wBAAgB,aAAa,IAAI,mBAAmB,CAEnD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,CAmBlE;AAsBD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAOxF;AAidD,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMhD;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASjD"}
|
package/dist/index.js
CHANGED
|
@@ -574,6 +574,8 @@ function paramExampleValue(type, defaultValue, randomExamples = false) {
|
|
|
574
574
|
case "int":
|
|
575
575
|
case "bigint":
|
|
576
576
|
return '"0"';
|
|
577
|
+
case "decimal":
|
|
578
|
+
return '"0.00"';
|
|
577
579
|
case "boolean":
|
|
578
580
|
return '"true"';
|
|
579
581
|
case "date":
|
|
@@ -626,6 +628,8 @@ function typeToExampleValue(type, modelMap, randomExamples = false) {
|
|
|
626
628
|
case "int":
|
|
627
629
|
case "bigint":
|
|
628
630
|
return 0;
|
|
631
|
+
case "decimal":
|
|
632
|
+
return "0.00";
|
|
629
633
|
case "boolean":
|
|
630
634
|
return true;
|
|
631
635
|
case "date":
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/codegen-bruno.ts"],"sourcesContent":["import { resolve, basename, dirname } from 'node:path';\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync, rmdirSync } from 'node:fs';\nimport { generateOpenCollectionIncremental, parseManifest, emptyManifest } from './codegen-bruno.js';\nimport type { BrunoSecurityScheme } from './codegen-bruno.js';\nimport { serializeIncrementalManifest } from '@contractkit/core';\nimport type { ContractKitPlugin, PluginContext, PluginValue, OpRootNode, ContractRootNode, IncrementalManifest } from '@contractkit/core';\n\n/** Filename for the persisted Bruno manifest under the CLI cache directory. */\nconst CACHE_MANIFEST_FILENAME = 'bruno-manifest.json';\n\n/** Configuration accepted by the Bruno plugin, both via `contractkit.config.json` and `createBrunoPlugin`. */\nexport interface BrunoPluginConfig {\n baseDir?: string;\n output?: string;\n collectionName?: string;\n /**\n * When true (default), example values use Bruno's faker templates\n * (`{{$randomUUID}}`, `{{$randomEmail}}`, etc.) so each send produces\n * fresh data. Set to false for deterministic placeholders.\n */\n randomExamples?: boolean;\n /**\n * Whether to generate request files for operations marked `internal`. Defaults to\n * `true` — Bruno collections are typically used by the team that owns the API and\n * benefit from full coverage. Set to `false` to omit internal ops.\n */\n includeInternal?: boolean;\n /**\n * Map of environment name → variables. Each entry produces a\n * `environments/<name>.yml` file. When omitted, a default `local.yml` is\n * emitted with `baseUrl=http://localhost:3000` and any auth env-var\n * placeholders. When provided, the default is replaced entirely; include\n * auth variables (e.g. `token`) explicitly if you need them.\n */\n environments?: Record<string, Record<string, unknown>>;\n}\n\n/** Full plugin options shape read from `ctx.options` — extends {@link BrunoPluginConfig} with the `auth` block. */\nexport interface BrunoPluginOptions extends BrunoPluginConfig {\n auth?: { defaultScheme: string; schemes?: Record<string, BrunoSecurityScheme> };\n}\n\n// ─── Default export: loaded via plugins array, reads config from ctx.options ─\n\n/**\n * Validates a `plugins.bruno` extension entry on an operation. The expected shape is\n * `{ template?: string }`, where `template` is a YAML fragment to deep-merge into the\n * generated request file (typically a `file://...` URL whose contents have already\n * been loaded by the CLI resolver).\n */\nexport function validateBrunoExtension(value: PluginValue): { errors?: string[] } | void {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return { errors: [`expected an object, got ${describe(value)}`] };\n }\n const errors: string[] = [];\n for (const [key, val] of Object.entries(value)) {\n if (key === 'template') {\n if (typeof val !== 'string') errors.push(`'template' must be a string, got ${describe(val)}`);\n } else {\n errors.push(`unknown field '${key}' (allowed: template)`);\n }\n }\n return errors.length ? { errors } : undefined;\n}\n\nfunction describe(value: PluginValue): string {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n return typeof value;\n}\n\nconst plugin: ContractKitPlugin = {\n name: 'bruno',\n validateExtension: validateBrunoExtension,\n async generateTargets({ opRoots, contractRoots }, ctx) {\n const { auth, ...config } = ctx.options as BrunoPluginOptions;\n await runBrunoCodegen(opRoots, contractRoots, ctx, config, auth);\n },\n};\n\nexport default plugin;\n\n// ─── Factory: for programmatic use with explicit config ────────────────────\n\n/**\n * Creates a Bruno plugin instance with explicit configuration, for programmatic use.\n *\n * Prefer the default export when loading via `contractkit.config.json`. Use this\n * factory when constructing the plugin in code (e.g. in tests or custom build scripts).\n *\n * @param config - Plugin configuration (output paths and feature flags).\n * @param rootDir - Absolute path used to resolve relative paths in `config`.\n * @param auth - Optional auth scheme configuration mirroring the `auth` key in plugin options.\n */\nexport function createBrunoPlugin(\n config: BrunoPluginConfig,\n rootDir: string,\n auth?: { defaultScheme: string; schemes?: Record<string, BrunoSecurityScheme> },\n): ContractKitPlugin {\n return {\n name: 'bruno',\n validateExtension: validateBrunoExtension,\n async generateTargets({ opRoots, contractRoots }, ctx) {\n // The factory captures rootDir at creation time; ctx.rootDir may differ when the\n // plugin is loaded via a config file, so respect the explicit one passed in here.\n await runBrunoCodegen(opRoots, contractRoots, { ...ctx, rootDir }, config, auth);\n },\n };\n}\n\n/**\n * Shared orchestration used by both the default export and {@link createBrunoPlugin}.\n *\n * Reads the prior manifest, runs the cache-aware codegen, deletes any files that\n * are no longer produced, and emits the changed files plus the new manifest. When\n * `ctx.cacheEnabled` is `false` (e.g. `--force`) the prior manifest is ignored so\n * every op regenerates.\n */\nasync function runBrunoCodegen(\n opRoots: OpRootNode[],\n contractRoots: ContractRootNode[],\n ctx: PluginContext,\n config: BrunoPluginConfig,\n auth: BrunoPluginOptions['auth'],\n): Promise<void> {\n const base = config.baseDir ? resolve(ctx.rootDir, config.baseDir) : ctx.rootDir;\n const outDir = resolve(base, config.output ?? 'bruno-collection');\n const collectionName = config.collectionName ?? basename(ctx.rootDir);\n const manifestPath = resolve(ctx.cacheDir, CACHE_MANIFEST_FILENAME);\n\n const prevManifest: IncrementalManifest = ctx.cacheEnabled ? readManifest(manifestPath) : emptyManifest();\n\n const result = generateOpenCollectionIncremental(\n opRoots,\n {\n collectionName,\n contractRoots,\n auth,\n randomExamples: config.randomExamples ?? true,\n includeInternal: config.includeInternal,\n environments: config.environments,\n },\n prevManifest,\n relPath => existsSync(resolve(outDir, relPath)),\n );\n\n deleteStalePaths(outDir, result.deletedPaths);\n\n for (const { relativePath, content } of result.filesToWrite) {\n ctx.emitFile(resolve(outDir, relativePath), content);\n }\n\n // Persist the manifest under the CLI cache dir, separate from the bruno output.\n writeManifest(manifestPath, result.manifest);\n}\n\n/** Read the previous run's manifest from `manifestPath`. Returns an empty manifest when missing or unreadable. */\nfunction readManifest(manifestPath: string): IncrementalManifest {\n if (!existsSync(manifestPath)) return emptyManifest();\n try {\n return parseManifest(readFileSync(manifestPath, 'utf-8'));\n } catch {\n return emptyManifest();\n }\n}\n\n/** Write the manifest to `manifestPath`. Creates parent dirs as needed. Errors are swallowed so a broken cache never blocks the build. */\nfunction writeManifest(manifestPath: string, manifest: IncrementalManifest): void {\n try {\n mkdirSync(dirname(manifestPath), { recursive: true });\n writeFileSync(manifestPath, serializeIncrementalManifest(manifest), 'utf-8');\n } catch {\n // best-effort\n }\n}\n\n/**\n * Delete files that the previous run tracked but the current run doesn't produce, then\n * walk back up each affected directory and remove it if it's now empty (stopping at outDir).\n * Anything user-added survives because the manifest only ever lists plugin-generated paths.\n */\nfunction deleteStalePaths(outDir: string, relPaths: string[]): void {\n if (relPaths.length === 0) return;\n const removedDirs = new Set<string>();\n for (const rel of relPaths) {\n const abs = resolve(outDir, rel);\n if (existsSync(abs)) {\n rmSync(abs, { force: true });\n removedDirs.add(dirname(abs));\n }\n }\n for (const dir of removedDirs) {\n let current = dir;\n while (current.startsWith(outDir) && current !== outDir) {\n try {\n if (readdirSync(current).length === 0) {\n rmdirSync(current);\n current = dirname(current);\n } else {\n break;\n }\n } catch {\n break;\n }\n }\n }\n}\n\n","import type {\n OpRootNode,\n OpRouteNode,\n OpOperationNode,\n OpResponseNode,\n ParamSource,\n ContractTypeNode,\n ContractRootNode,\n ModelNode,\n FieldNode,\n IncrementalManifest,\n IncrementalUnit,\n IncrementalResult as IncrementalResultBase,\n} from '@contractkit/core';\nimport {\n resolveSecurity,\n resolveModifiers,\n SECURITY_NONE,\n collectTransitiveModelRefs,\n runIncrementalCodegen,\n parseIncrementalManifest,\n emptyIncrementalManifest,\n serializeIncrementalManifest,\n hashFingerprint,\n INCREMENTAL_MANIFEST_VERSION,\n} from '@contractkit/core';\nimport { basename } from 'path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\n\n/** A single file produced by the Bruno codegen — a relative output path and its YAML content. */\nexport interface OpenCollectionFile {\n relativePath: string;\n content: string;\n}\n\n/** Manifest filename — tracks which files this plugin previously generated so subsequent runs can clean up only those, leaving any user-added files alone. */\nexport const MANIFEST_FILENAME = '.contractkit-bruno-manifest.json';\n\n/**\n * Bumped whenever the codegen output shape changes in a way that should bust\n * every per-op fingerprint. Mixed into the per-op fingerprint so a plugin\n * upgrade forces full regeneration even when source `.ck` files are unchanged.\n */\nexport const BRUNO_CODEGEN_VERSION = '1';\n\n/** Subset of a security scheme sufficient for Bruno auth generation (non-HMAC). */\nexport interface BrunoSecurityScheme {\n type: string; // \"http\" | \"apiKey\" | \"oauth2\" | \"openIdConnect\"\n scheme?: string; // \"bearer\" | \"basic\" (when type === \"http\")\n name?: string; // header/query param name (when type === \"apiKey\")\n in?: string; // \"header\" | \"query\" (when type === \"apiKey\")\n}\n\n/** Auth configuration passed to the Bruno codegen; drives collection-level auth and per-operation auth blocks. */\nexport interface BrunoAuthOptions {\n /** Name of the default scheme (from config.security.default) */\n defaultScheme?: string;\n /** Scheme definitions keyed by name (non-HMAC only) */\n schemes?: Record<string, BrunoSecurityScheme>;\n}\n\n/** Options controlling what {@link generateOpenCollection} emits. */\nexport interface OpenCollectionOptions {\n collectionName: string;\n contractRoots?: ContractRootNode[];\n auth?: BrunoAuthOptions;\n /**\n * When true, emit Bruno faker template strings (e.g. `{{$randomUUID}}`,\n * `{{$randomEmail}}`) for compatible scalar types so each send produces\n * fresh data. When false (default), use deterministic placeholders.\n */\n randomExamples?: boolean;\n /**\n * Whether to generate request files for operations marked `internal`. Defaults to\n * `true` — Bruno collections are typically used by the team that owns the API and\n * benefit from full coverage. Set to `false` to omit internal ops.\n */\n includeInternal?: boolean;\n /**\n * Map of environment name → variables. Each entry produces a\n * `environments/<name>.yml` file with the variables in declaration order.\n * Values are coerced to strings.\n *\n * When omitted, a default `environments/local.yml` is emitted with\n * `baseUrl=http://localhost:3000` plus any auth env-var placeholders the\n * default scheme requires. When provided, the default is replaced entirely\n * — auth vars are not auto-injected, so include them explicitly if needed.\n */\n environments?: Record<string, Record<string, unknown>>;\n}\n\n/** Per-operation bookkeeping computed up front: where the file lives, what to call it, and the YAML inputs needed to render or fingerprint it. */\ninterface OpEntry {\n /** Stable identifier across runs — `<file>::<METHOD> <path>`. */\n opKey: string;\n /** Output path relative to the collection root (e.g. `users/get-user.yml`). */\n relativePath: string;\n /** Display name used inside the YAML `info:` block (alphabetized within its folder). */\n requestName: string;\n /** 1-based sequence within the request's containing folder, drives Bruno's UI ordering. */\n seq: number;\n route: OpRouteNode;\n op: OpOperationNode;\n root: OpRootNode;\n}\n\n/**\n * @deprecated Use {@link IncrementalManifest} from `@contractkit/core`. Re-exported here for backwards compatibility.\n */\nexport type BrunoManifest = IncrementalManifest;\n\n/** Result of {@link generateOpenCollectionIncremental}. Renamed `skippedOpCount` for Bruno-specific clarity but otherwise the shared {@link IncrementalResultBase}. */\nexport interface IncrementalResult extends Omit<IncrementalResultBase, 'skippedUnitCount'> {\n /** Number of ops whose codegen was skipped because their fingerprint matched. */\n skippedOpCount: number;\n}\n\n/**\n * Generates an OpenCollection (https://spec.opencollection.com/) API collection\n * from a set of operation roots. Produces opencollection.yml, an environment\n * file, one .yml request file per operation, and the tracking manifest.\n *\n * This is the full-regeneration entry point — every file is rebuilt from scratch.\n * For cache-aware incremental builds, use {@link generateOpenCollectionIncremental}.\n *\n * The manifest appears in the returned list (under {@link MANIFEST_FILENAME}) for\n * convenience — single-shot callers can write the entire array as-is. The plugin's\n * incremental path persists the manifest separately (under the CLI cache dir), so\n * its `filesToWrite` does not include the manifest.\n */\nexport function generateOpenCollection(roots: OpRootNode[], options: OpenCollectionOptions): OpenCollectionFile[] {\n const result = generateOpenCollectionIncremental(roots, options, emptyManifest());\n return [...result.filesToWrite, { relativePath: MANIFEST_FILENAME, content: serializeIncrementalManifest(result.manifest) }];\n}\n\n/**\n * Cache-aware variant of {@link generateOpenCollection}. Skips re-rendering YAML\n * for any op whose fingerprint matches the entry in `prevManifest`. The caller is\n * responsible for emitting `filesToWrite`, deleting `deletedPaths`, and persisting\n * `manifest` so the next run can match against it.\n *\n * Global files (collection root, env files, folder.yml) are always regenerated —\n * they're cheap and depend on options the manifest doesn't fingerprint.\n */\nexport function generateOpenCollectionIncremental(\n roots: OpRootNode[],\n options: OpenCollectionOptions,\n prevManifest: IncrementalManifest,\n fileExists: (relativePath: string) => boolean = () => true,\n): IncrementalResult {\n const modelMap = buildModelMap(options.contractRoots ?? []);\n const authOpts = options.auth;\n const defaultScheme = authOpts?.defaultScheme ? authOpts.schemes?.[authOpts.defaultScheme] : undefined;\n const randomExamples = options.randomExamples ?? false;\n const includeInternal = options.includeInternal ?? true;\n\n // ── Global files (collection root + env files + folder.yml per area/subarea) ──\n const globalFiles: OpenCollectionFile[] = [];\n globalFiles.push({\n relativePath: 'opencollection.yml',\n content: generateCollectionRoot(options.collectionName, defaultScheme),\n });\n for (const envFile of generateEnvFiles(options.environments, defaultScheme)) {\n globalFiles.push(envFile);\n }\n\n const sortedRoots = [...roots].sort((a, b) => {\n const aArea = a.meta['area'] ?? deriveFolderName(a.file);\n const bArea = b.meta['area'] ?? deriveFolderName(b.file);\n const cmp = aArea.localeCompare(bArea);\n if (cmp !== 0) return cmp;\n return (a.meta['subarea'] ?? '').localeCompare(b.meta['subarea'] ?? '');\n });\n\n const units: IncrementalUnit[] = [];\n // Track which folder.yml paths we've already emitted so multiple roots sharing\n // an area (the area's own root + its subarea roots) don't each push the\n // area-level folder.yml — that produced N near-duplicate emits per area, only\n // the last of which survived on disk.\n const emittedFolders = new Set<string>();\n let areaSeq = 0;\n for (const root of sortedRoots) {\n const folder = root.meta['area'] ? slugifyName(root.meta['area']) : deriveFolderName(root.file);\n const folderRel = `${folder}/folder.yml`;\n if (!emittedFolders.has(folderRel)) {\n const displayName = (root.meta['area'] ?? folder).charAt(0).toUpperCase() + (root.meta['area'] ?? folder).slice(1);\n areaSeq++;\n globalFiles.push({ relativePath: folderRel, content: generateFolderFile(displayName, areaSeq) });\n emittedFolders.add(folderRel);\n }\n\n const subarea = root.meta['subarea'];\n const subareaSlug = subarea ? slugifyName(subarea) : undefined;\n const requestDir = subareaSlug ? `${folder}/${subareaSlug}` : folder;\n\n if (subareaSlug) {\n const subareaRel = `${requestDir}/folder.yml`;\n if (!emittedFolders.has(subareaRel)) {\n const subareaDisplayName = subarea!.charAt(0).toUpperCase() + subarea!.slice(1);\n globalFiles.push({ relativePath: subareaRel, content: generateFolderFile(subareaDisplayName, 1) });\n emittedFolders.add(subareaRel);\n }\n }\n\n for (const entry of buildOpEntries(root, requestDir, includeInternal)) {\n units.push({\n key: entry.opKey,\n fingerprint: computeOpFingerprint(entry, modelMap, defaultScheme, randomExamples),\n render: () => [renderOpFile(entry, modelMap, defaultScheme, randomExamples)],\n });\n }\n }\n\n const result = runIncrementalCodegen({\n codegenVersion: BRUNO_CODEGEN_VERSION,\n prevManifest,\n globalFiles,\n units,\n fileExists,\n });\n\n return {\n filesToWrite: result.filesToWrite,\n manifest: result.manifest,\n deletedPaths: result.deletedPaths,\n skippedOpCount: result.skippedUnitCount,\n };\n}\n\n/** Build the ordered, alphabetized op entries for a single root. seq numbers are assigned in alphabetical order so the Bruno UI shows requests sorted by display name. */\nfunction buildOpEntries(root: OpRootNode, requestDir: string, includeInternal: boolean): OpEntry[] {\n const requests: Array<{ route: OpRouteNode; op: OpOperationNode; requestName: string }> = [];\n for (const route of root.routes) {\n for (const op of route.operations) {\n if (!includeInternal && resolveModifiers(route, op).includes('internal')) continue;\n requests.push({ route, op, requestName: op.name ?? route.path });\n }\n }\n requests.sort((a, b) => a.requestName.localeCompare(b.requestName));\n\n const entries: OpEntry[] = [];\n let seq = 1;\n for (const { route, op, requestName } of requests) {\n const fileName = op.name ? `${slugifyName(op.name)}.yml` : `${op.method}-${sanitizePath(route.path)}.yml`;\n entries.push({\n opKey: `${root.file}::${op.method.toUpperCase()} ${route.path}`,\n relativePath: `${requestDir}/${fileName}`,\n requestName,\n seq,\n route,\n op,\n root,\n });\n seq++;\n }\n return entries;\n}\n\n/** Render a single op's request file (including any plugin-extension YAML override). */\nfunction renderOpFile(\n entry: OpEntry,\n modelMap: Map<string, ModelNode>,\n defaultScheme: BrunoSecurityScheme | undefined,\n randomExamples: boolean,\n): OpenCollectionFile {\n let content = generateRequestFile(entry.route, entry.op, entry.requestName, entry.seq, modelMap, entry.root, defaultScheme, randomExamples);\n const brunoExt = entry.op.pluginExtensions?.['bruno'];\n const pluginOverride = brunoExt && typeof brunoExt === 'object' && !Array.isArray(brunoExt) ? brunoExt['template'] : undefined;\n if (typeof pluginOverride === 'string') {\n content = mergePluginFile(content, pluginOverride);\n }\n return { relativePath: entry.relativePath, content };\n}\n\n/** Compute a fingerprint covering every input that affects this op's rendered file. Stable across runs given identical inputs. */\nfunction computeOpFingerprint(\n entry: OpEntry,\n modelMap: Map<string, ModelNode>,\n defaultScheme: BrunoSecurityScheme | undefined,\n randomExamples: boolean,\n): string {\n const referencedModels = collectTransitiveModelRefs(collectOpTypeNodes(entry.route, entry.op), modelMap);\n const modelSnapshot: Record<string, unknown> = {};\n for (const name of [...referencedModels].sort()) {\n const m = modelMap.get(name);\n if (m) modelSnapshot[name] = m;\n }\n // Only include the route fields this op actually depends on. We deliberately exclude\n // `route.operations` so a change to a sibling op doesn't invalidate this op's cache.\n const routeShape = {\n path: entry.route.path,\n params: entry.route.params ?? null,\n modifiers: entry.route.modifiers ?? null,\n security: entry.route.security ?? null,\n description: entry.route.description ?? null,\n };\n return hashFingerprint({\n v: BRUNO_CODEGEN_VERSION,\n opKey: entry.opKey,\n relativePath: entry.relativePath,\n requestName: entry.requestName,\n seq: entry.seq,\n route: routeShape,\n op: entry.op,\n rootMeta: entry.root.meta,\n rootFile: entry.root.file,\n defaultScheme: defaultScheme ?? null,\n randomExamples,\n models: modelSnapshot,\n });\n}\n\n/** Collect every ContractTypeNode that contributes to this op's rendered output. Used as the seed set for transitive model collection in {@link computeOpFingerprint}. */\nfunction collectOpTypeNodes(route: OpRouteNode, op: OpOperationNode): ContractTypeNode[] {\n const out: ContractTypeNode[] = [];\n pushFromParamSource(route.params, out);\n pushFromParamSource(op.query, out);\n pushFromParamSource(op.headers, out);\n if (op.request) {\n for (const body of op.request.bodies) out.push(body.bodyType);\n }\n for (const resp of op.responses) {\n for (const body of resp.bodies) out.push(body.bodyType);\n if (resp.headers) {\n for (const h of resp.headers) out.push(h.type);\n }\n }\n return out;\n}\n\nfunction pushFromParamSource(src: ParamSource | undefined, out: ContractTypeNode[]): void {\n if (!src) return;\n if (src.kind === 'params') {\n for (const n of src.nodes) out.push(n.type);\n } else if (src.kind === 'ref') {\n out.push({ kind: 'ref', name: src.name } as ContractTypeNode);\n } else if (src.kind === 'type') {\n out.push(src.node);\n }\n}\n\n/** Empty-state manifest, used when none has been written yet (or when the cache is being bypassed). */\nexport function emptyManifest(): IncrementalManifest {\n return emptyIncrementalManifest(BRUNO_CODEGEN_VERSION);\n}\n\n/**\n * Parse a previously-written manifest. Accepts both v1 (`{ files: string[] }`) and v2\n * (full {@link IncrementalManifest}) shapes. v1 manifests are returned with an empty\n * `units` map so the next run treats every op as a cache miss while still cleaning up\n * the tracked file list.\n *\n * Returns an empty manifest on any parse error so a stale/garbled file never blocks\n * regeneration.\n */\nexport function parseManifest(content: string): IncrementalManifest {\n const parsed = parseIncrementalManifest(content);\n // The shared parser handles v2. If v2 parsing produced an empty manifest, fall\n // through to a v1 shape ({ files: [...] }) and migrate it for cleanup purposes.\n if (parsed.files.length > 0 || Object.keys(parsed.units).length > 0) return parsed;\n try {\n const raw = JSON.parse(content);\n if (Array.isArray(raw?.files) && raw.files.every((f: unknown) => typeof f === 'string')) {\n return {\n version: INCREMENTAL_MANIFEST_VERSION,\n codegenVersion: '',\n files: raw.files as string[],\n units: {},\n };\n }\n } catch {\n // fall through\n }\n return emptyManifest();\n}\n\n// ─── Plugin file merge ─────────────────────────────────────────────────────\n\nfunction deepMerge(base: unknown, override: unknown): unknown {\n if (\n override !== null &&\n typeof override === 'object' &&\n !Array.isArray(override) &&\n base !== null &&\n typeof base === 'object' &&\n !Array.isArray(base)\n ) {\n const result: Record<string, unknown> = { ...(base as Record<string, unknown>) };\n for (const [key, val] of Object.entries(override as Record<string, unknown>)) {\n result[key] = deepMerge(result[key], val);\n }\n return result;\n }\n return override;\n}\n\n/**\n * Deep-merges a YAML override string into a generated YAML string.\n *\n * Objects are merged recursively; arrays and scalars in the override replace the\n * generated value entirely. If `pluginFileContent` is not a YAML mapping (e.g. it\n * is a scalar or a list), the generated content is returned unchanged.\n *\n * @param generatedYaml - The YAML string produced by the Bruno codegen.\n * @param pluginFileContent - The YAML override string to merge in.\n * @returns The merged YAML string, or `generatedYaml` if the override is not a mapping.\n */\nexport function mergePluginFile(generatedYaml: string, pluginFileContent: string): string {\n const overrideParsed = parseYaml(pluginFileContent);\n if (overrideParsed === null || typeof overrideParsed !== 'object' || Array.isArray(overrideParsed)) {\n return generatedYaml;\n }\n const merged = deepMerge(parseYaml(generatedYaml), overrideParsed);\n return stringifyYaml(merged, { lineWidth: 0 });\n}\n\n// ─── File generators ───────────────────────────────────────────────────────\n\nfunction generateCollectionRoot(name: string, scheme?: BrunoSecurityScheme): string {\n const lines = [`opencollection: \"1.0.0\"`, `info:`, ` name: ${yamlString(name)}`];\n if (scheme) {\n lines.push(``);\n lines.push(`request:`);\n lines.push(...renderAuthBlock(scheme, ' '));\n }\n lines.push(``);\n return lines.join('\\n');\n}\n\nfunction generateEnvFiles(\n environments: Record<string, Record<string, unknown>> | undefined,\n scheme: BrunoSecurityScheme | undefined,\n): OpenCollectionFile[] {\n if (environments && Object.keys(environments).length > 0) {\n return Object.entries(environments).map(([name, variables]) => ({\n relativePath: `environments/${name}.yml`,\n content: renderEnvFile(displayNameFor(name), variables),\n }));\n }\n const defaultVars: Record<string, string> = { baseUrl: 'http://localhost:3000' };\n if (scheme) {\n for (const varName of authEnvVarNames(scheme)) defaultVars[varName] = '';\n }\n return [{ relativePath: 'environments/local.yml', content: renderEnvFile('Local', defaultVars) }];\n}\n\nfunction renderEnvFile(name: string, variables: Record<string, unknown>): string {\n const lines = [`name: ${name}`, `variables:`];\n for (const [key, value] of Object.entries(variables)) {\n const escaped = String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n lines.push(` - name: ${key}`);\n lines.push(` value: \"${escaped}\"`);\n }\n lines.push(``);\n return lines.join('\\n');\n}\n\nfunction displayNameFor(envKey: string): string {\n return envKey.charAt(0).toUpperCase() + envKey.slice(1);\n}\n\nfunction generateFolderFile(name: string, seq: number): string {\n return [`info:`, ` name: ${yamlString(name)}`, ` type: folder`, ` seq: ${seq}`, ``].join('\\n');\n}\n\nfunction generateRequestFile(\n route: OpRouteNode,\n op: OpOperationNode,\n name: string,\n seq: number,\n modelMap: Map<string, ModelNode>,\n root?: OpRootNode,\n defaultScheme?: BrunoSecurityScheme,\n randomExamples = false,\n): string {\n const lines: string[] = [];\n\n lines.push(`info:`);\n lines.push(` name: ${yamlString(name)}`);\n lines.push(` type: http`);\n lines.push(` seq: ${seq}`);\n lines.push(``);\n lines.push(`http:`);\n lines.push(` method: ${op.method.toUpperCase()}`);\n lines.push(` url: ${yamlString(`{{baseUrl}}${openCollectionPath(route.path)}`)}`);\n\n // Params — flat array with type: \"path\" | \"query\". Optional query params are\n // emitted with disabled: true so users opt in before sending.\n const pathParams: Array<ParamEntry & { kind: 'path' | 'query' }> = extractPathParamNames(route.path).map(n => ({\n name: n,\n type: findParamType(route.params, n, modelMap),\n optional: false,\n kind: 'path' as const,\n }));\n const queryParams: Array<ParamEntry & { kind: 'path' | 'query' }> = op.query\n ? expandParamSource(op.query, modelMap).map(e => ({ ...e, kind: 'query' as const }))\n : [];\n const allParams = [...pathParams, ...queryParams];\n\n if (allParams.length > 0) {\n lines.push(` params:`);\n for (const p of allParams) {\n lines.push(` - name: ${p.name}`);\n lines.push(` value: ${paramExampleValue(p.type, p.default, randomExamples)}`);\n lines.push(` type: ${p.kind}`);\n if (p.optional && p.kind === 'query') lines.push(` disabled: true`);\n }\n }\n\n // Headers\n if (op.headers) {\n const headerEntries = expandParamSource(op.headers, modelMap);\n if (headerEntries.length > 0) {\n lines.push(` headers:`);\n for (const h of headerEntries) {\n lines.push(` - name: ${h.name}`);\n lines.push(` value: ${paramExampleValue(h.type, h.default, randomExamples)}`);\n if (h.optional) lines.push(` disabled: true`);\n }\n }\n }\n\n // Auth — inside http block; inherit collection default unless this op is explicitly public\n if (defaultScheme) {\n const security = root ? resolveSecurity(route, op, root) : (op.security ?? route.security);\n if (security === SECURITY_NONE) {\n lines.push(` auth:`);\n lines.push(` type: none`);\n } else {\n lines.push(` auth: inherit`);\n }\n }\n\n // Body — Bruno supports a single body per request, so prefer JSON, then form-urlencoded, then multipart.\n if (op.request && op.request.bodies.length > 0) {\n const preferredOrder: Array<(typeof op.request.bodies)[number]['contentType']> = [\n 'application/json',\n 'application/x-www-form-urlencoded',\n 'multipart/form-data',\n ];\n const primary =\n preferredOrder.map(ct => op.request!.bodies.find(b => b.contentType === ct)).find(b => b !== undefined) ?? op.request.bodies[0]!;\n\n lines.push(` body:`);\n if (primary.contentType === 'multipart/form-data') {\n lines.push(` type: multipart-form`);\n lines.push(` data: []`);\n } else if (primary.contentType === 'application/x-www-form-urlencoded') {\n lines.push(` type: form-urlencoded`);\n lines.push(` data: []`);\n } else {\n const json = JSON.stringify(typeToExampleValue(primary.bodyType, modelMap, randomExamples), null, 2);\n lines.push(` type: json`);\n lines.push(` data: |`);\n for (const jsonLine of json.split('\\n')) {\n lines.push(` ${jsonLine}`);\n }\n }\n }\n\n // runtime.assertions — auto-generate a status-code check and presence checks for required response headers.\n const expectedStatus = pickAssertionStatus(op.responses);\n const assertedResponse = op.responses.find(r => r.statusCode === expectedStatus);\n const requiredHeaders = (assertedResponse?.headers ?? []).filter(h => !h.optional);\n if (expectedStatus !== undefined) {\n lines.push(``);\n lines.push(`runtime:`);\n lines.push(` assertions:`);\n lines.push(` - expression: res.status`);\n lines.push(` operator: eq`);\n // Always quote — the OpenCollection schema types `value` as a string,\n // so we must keep \"200\" from being parsed as YAML number 200.\n lines.push(` value: \"${expectedStatus}\"`);\n for (const h of requiredHeaders) {\n lines.push(` - expression: res.headers[\"${h.name.toLowerCase()}\"]`);\n lines.push(` operator: isDefined`);\n lines.push(` value: \"\"`);\n }\n }\n\n // docs — combine route- and operation-level descriptions plus the declared response-header summary.\n const docs = buildRequestDocs(route, op, assertedResponse);\n if (docs) {\n lines.push(``);\n lines.push(`docs: |-`);\n for (const docLine of docs.split('\\n')) {\n lines.push(` ${docLine}`);\n }\n }\n\n lines.push(``);\n return lines.join('\\n');\n}\n\n/** Pick the response whose status code we'll assert against. Prefers the first declared 2xx; otherwise falls back to the first declared response. Returns undefined if no responses are declared. */\nfunction pickAssertionStatus(responses: OpResponseNode[]): number | undefined {\n const success = responses.find(r => r.statusCode >= 200 && r.statusCode < 300);\n return success?.statusCode ?? responses[0]?.statusCode;\n}\n\n/** Build a markdown docs block from route- and operation-level descriptions, plus declared response-header summary. */\nfunction buildRequestDocs(route: OpRouteNode, op: OpOperationNode, assertedResponse?: OpResponseNode): string | undefined {\n const parts: string[] = [];\n if (route.description) parts.push(route.description.trim());\n if (op.description) parts.push(op.description.trim());\n const headers = assertedResponse?.headers ?? [];\n if (headers.length > 0) {\n const lines = ['**Response headers**', ''];\n for (const h of headers) {\n const tag = h.optional ? 'optional' : 'required';\n const desc = h.description ? ` — ${h.description}` : '';\n lines.push(`- \\`${h.name}\\` (${tag})${desc}`);\n }\n parts.push(lines.join('\\n'));\n }\n return parts.length > 0 ? parts.join('\\n\\n') : undefined;\n}\n\n// ─── Auth helpers ──────────────────────────────────────────────────────────\n\n/** Generate the YAML lines for an auth block (flat, per spec), indented by `indent`. */\nfunction renderAuthBlock(scheme: BrunoSecurityScheme, indent: string): string[] {\n const i = indent;\n if (scheme.type === 'http' && scheme.scheme === 'bearer') {\n return [`${i}auth:`, `${i} type: bearer`, `${i} token: \"{{token}}\"`];\n }\n if (scheme.type === 'http' && scheme.scheme === 'basic') {\n return [`${i}auth:`, `${i} type: basic`, `${i} username: \"{{username}}\"`, `${i} password: \"{{password}}\"`];\n }\n if (scheme.type === 'apiKey' && scheme.in === 'header') {\n const headerName = scheme.name ?? 'X-Api-Key';\n return [`${i}auth:`, `${i} type: apikey`, `${i} key: ${headerName}`, `${i} value: \"{{apiKey}}\"`, `${i} placement: header`];\n }\n return [];\n}\n\n/** Return the environment variable names needed for a given auth scheme. */\nfunction authEnvVarNames(scheme: BrunoSecurityScheme): string[] {\n if (scheme.type === 'http' && scheme.scheme === 'bearer') return ['token'];\n if (scheme.type === 'http' && scheme.scheme === 'basic') return ['username', 'password'];\n if (scheme.type === 'apiKey') return ['apiKey'];\n return [];\n}\n\n// ─── Model registry ────────────────────────────────────────────────────────\n\nfunction buildModelMap(contractRoots: ContractRootNode[]): Map<string, ModelNode> {\n const map = new Map<string, ModelNode>();\n for (const root of contractRoots) {\n for (const model of root.models) {\n map.set(model.name, model);\n }\n }\n return map;\n}\n\n/** Resolve all fields for a model, including inherited base fields (bases first, in declaration order). */\nfunction resolveModelFields(model: ModelNode, modelMap: Map<string, ModelNode>): FieldNode[] {\n const collected: FieldNode[] = [];\n if (model.bases) {\n for (const base of model.bases) {\n const baseModel = modelMap.get(base);\n if (baseModel) collected.push(...resolveModelFields(baseModel, modelMap));\n }\n }\n return [...collected, ...model.fields];\n}\n\n// ─── Param helpers ─────────────────────────────────────────────────────────\n\ninterface ParamEntry {\n name: string;\n type: ContractTypeNode | undefined;\n default?: string | number | boolean;\n optional: boolean;\n}\n\n/** Expand a ParamSource into a flat list of named entries with their types. */\nfunction expandParamSource(source: ParamSource, modelMap: Map<string, ModelNode>): ParamEntry[] {\n if (source.kind === 'params') {\n return source.nodes.map(n => ({ name: n.name, type: n.type, default: n.default, optional: n.optional }));\n }\n if (source.kind === 'ref') {\n const model = modelMap.get(source.name);\n if (model) {\n return resolveModelFields(model, modelMap)\n .filter(f => f.visibility !== 'readonly')\n .map(f => ({ name: f.name, type: f.type, default: f.default, optional: f.optional }));\n }\n // Fallback: single placeholder entry\n const name = source.name.charAt(0).toLowerCase() + source.name.slice(1);\n return [{ name, type: undefined, optional: false }];\n }\n // kind === 'type': if it's an inline object, expand its fields\n if (source.node.kind === 'inlineObject') {\n return source.node.fields.map(f => ({ name: f.name, type: f.type, default: f.default, optional: f.optional }));\n }\n return [];\n}\n\n/** Look up a named path param's type from route.params. */\nfunction findParamType(source: ParamSource | undefined, name: string, modelMap: Map<string, ModelNode>): ContractTypeNode | undefined {\n if (!source) return undefined;\n if (source.kind === 'params') return source.nodes.find(n => n.name === name)?.type;\n if (source.kind === 'ref') {\n const model = modelMap.get(source.name);\n if (model) return resolveModelFields(model, modelMap).find(f => f.name === name)?.type;\n }\n if (source.kind === 'type' && source.node.kind === 'inlineObject') {\n return source.node.fields.find(f => f.name === name)?.type;\n }\n return undefined;\n}\n\n/** Return a YAML-quoted example value string for a param, preferring a default value when provided. */\nfunction paramExampleValue(type: ContractTypeNode | undefined, defaultValue?: string | number | boolean, randomExamples = false): string {\n if (defaultValue !== undefined) return yamlDoubleQuoted(defaultValue);\n if (!type) return '\"\"';\n if (type.kind === 'enum') return type.values.length > 0 ? yamlDoubleQuoted(type.values[0]!) : '\"\"';\n if (type.kind === 'literal') return yamlDoubleQuoted(type.value);\n if (type.kind !== 'scalar') return '\"\"';\n if (randomExamples) {\n const random = randomScalarTemplate(type.name);\n if (random !== undefined) return `\"${random}\"`;\n }\n switch (type.name) {\n case 'uuid':\n return '\"00000000-0000-0000-0000-000000000000\"';\n case 'email':\n return '\"user@example.com\"';\n case 'url':\n return '\"https://example.com\"';\n case 'number':\n case 'int':\n case 'bigint':\n return '\"0\"';\n case 'boolean':\n return '\"true\"';\n case 'date':\n return '\"2024-01-01\"';\n case 'time':\n return '\"00:00:00\"';\n case 'datetime':\n return '\"2024-01-01T00:00:00Z\"';\n case 'duration':\n return '\"PT1H\"';\n default:\n return '\"\"';\n }\n}\n\n/** Bruno faker template for a scalar type, or undefined when no clean equivalent exists (date, time, duration, raw string). */\nfunction randomScalarTemplate(name: string): string | undefined {\n switch (name) {\n case 'uuid':\n return '{{$randomUUID}}';\n case 'email':\n return '{{$randomEmail}}';\n case 'url':\n return '{{$randomUrl}}';\n case 'number':\n case 'int':\n case 'bigint':\n return '{{$randomInt}}';\n case 'boolean':\n return '{{$randomBoolean}}';\n case 'datetime':\n return '{{$isoTimestamp}}';\n default:\n return undefined;\n }\n}\n\n// ─── Body helpers ──────────────────────────────────────────────────────────\n\n/**\n * Recursively build an example JSON value from a ContractTypeNode.\n *\n * When `randomExamples` is true we substitute Bruno faker templates only for\n * scalars whose JSON representation is a string (uuid/email/url/datetime).\n * Numbers and booleans stay deterministic — embedding `{{$randomInt}}` as a\n * bare JSON number would require sentinel-stripping the surrounding quotes,\n * and the body skeleton is meant as a starting point users edit anyway.\n */\nfunction typeToExampleValue(type: ContractTypeNode, modelMap: Map<string, ModelNode>, randomExamples = false): unknown {\n switch (type.kind) {\n case 'scalar':\n switch (type.name) {\n case 'string':\n return '';\n case 'email':\n return randomExamples ? '{{$randomEmail}}' : 'user@example.com';\n case 'url':\n return randomExamples ? '{{$randomUrl}}' : 'https://example.com';\n case 'uuid':\n return randomExamples ? '{{$randomUUID}}' : '00000000-0000-0000-0000-000000000000';\n case 'number':\n case 'int':\n case 'bigint':\n return 0;\n case 'boolean':\n return true;\n case 'date':\n return '2024-01-01';\n case 'time':\n return '00:00:00';\n case 'datetime':\n return randomExamples ? '{{$isoTimestamp}}' : '2024-01-01T00:00:00Z';\n case 'duration':\n return 'PT1H';\n case 'null':\n return null;\n default:\n return null;\n }\n case 'enum':\n return type.values[0] ?? '';\n case 'literal':\n return type.value;\n case 'array':\n return [typeToExampleValue(type.item, modelMap, randomExamples)];\n case 'tuple':\n return type.items.map(t => typeToExampleValue(t, modelMap, randomExamples));\n case 'record':\n return {};\n case 'union':\n return type.members.length > 0 ? typeToExampleValue(type.members[0]!, modelMap, randomExamples) : null;\n case 'discriminatedUnion':\n return type.members.length > 0 ? typeToExampleValue(type.members[0]!, modelMap, randomExamples) : null;\n case 'intersection':\n return {};\n case 'ref': {\n const model = modelMap.get(type.name);\n if (!model) return {};\n // Type alias — recurse into the aliased type\n if (model.type) return typeToExampleValue(model.type, modelMap, randomExamples);\n return modelToExampleObject(model, modelMap, randomExamples);\n }\n case 'lazy':\n return typeToExampleValue(type.inner, modelMap, randomExamples);\n case 'inlineObject':\n return fieldsToExampleObject(type.fields, modelMap, randomExamples);\n default:\n return null;\n }\n}\n\n/** Build an example object from a ModelNode's fields (including inherited base fields). */\nfunction modelToExampleObject(model: ModelNode, modelMap: Map<string, ModelNode>, randomExamples = false): Record<string, unknown> {\n return fieldsToExampleObject(resolveModelFields(model, modelMap), modelMap, randomExamples);\n}\n\n/** Build an example object from a list of FieldNodes. Excludes readonly fields; uses defaults when available; omits optional fields that have no default so they don't appear in the JSON output. */\nfunction fieldsToExampleObject(fields: FieldNode[], modelMap: Map<string, ModelNode>, randomExamples = false): Record<string, unknown> {\n const obj: Record<string, unknown> = {};\n for (const field of fields) {\n if (field.visibility === 'readonly') continue;\n if (field.default !== undefined) {\n obj[field.name] = field.default;\n } else if (!field.optional) {\n obj[field.name] = typeToExampleValue(field.type, modelMap, randomExamples);\n }\n }\n return obj;\n}\n\n// ─── Path helpers ──────────────────────────────────────────────────────────\n\n/** Convert /users/{id}/posts → /users/:id/posts (Bruno path parameter syntax) */\nfunction openCollectionPath(path: string): string {\n return path.replace(/\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}/g, ':$1');\n}\n\n/** Convert \"Create an Offer\" → create-an-offer (for .yml file names) */\nexport function slugifyName(name: string): string {\n const result = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n return result || 'request';\n}\n\n/** Convert /users/{id}/posts → users-id-posts (for .yml file names) */\nexport function sanitizePath(path: string): string {\n const result = path\n .replace(/^\\//, '')\n .replace(/\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}/g, '$1')\n .replace(/\\//g, '-')\n .replace(/[^a-zA-Z0-9-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n return result || 'root';\n}\n\n/** Extract param names from a URL template, e.g. /users/{id} → ['id'] */\nfunction extractPathParamNames(path: string): string[] {\n return [...path.matchAll(/\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}/g)].map(m => m[1]!);\n}\n\n/** Derive folder name from op file path, e.g. src/users.op → users */\nfunction deriveFolderName(file: string): string {\n return basename(file).replace(/\\.(op|ck)$/, '');\n}\n\n/**\n * Escape a value into an always-quoted, valid double-quoted YAML scalar.\n *\n * Unlike {@link yamlString} (which only quotes when required), this always wraps\n * the value in double quotes — used for example/param values that must stay\n * strings (e.g. `\"0\"`, `\"true\"`). `.ck` string literals permit unescaped `\"`\n * inside either quote style, so backslashes and quotes are escaped here to keep\n * the emitted YAML scalar valid.\n */\nfunction yamlDoubleQuoted(value: string | number | boolean): string {\n return `\"${String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\n/**\n * Wrap a string in YAML double quotes if it contains characters that require quoting\n * (flow indicators, colons, braces, etc.).\n */\nfunction yamlString(value: string): string {\n if (/[:{}[\\],&*#?|<>=!%@`\"']/.test(value) || /^\\s|\\s$/.test(value)) {\n return `\"${value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n }\n return value;\n}\n"],"mappings":"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","names":["resolve","basename","dirname","existsSync","readFileSync","writeFileSync","mkdirSync","rmSync","readdirSync","rmdirSync","resolveSecurity","resolveModifiers","SECURITY_NONE","collectTransitiveModelRefs","runIncrementalCodegen","parseIncrementalManifest","emptyIncrementalManifest","serializeIncrementalManifest","hashFingerprint","INCREMENTAL_MANIFEST_VERSION","basename","parse","parseYaml","stringify","stringifyYaml","BRUNO_CODEGEN_VERSION","generateOpenCollectionIncremental","roots","options","prevManifest","fileExists","modelMap","buildModelMap","contractRoots","authOpts","auth","defaultScheme","schemes","undefined","randomExamples","includeInternal","globalFiles","push","relativePath","content","generateCollectionRoot","collectionName","envFile","generateEnvFiles","environments","sortedRoots","sort","a","b","aArea","meta","deriveFolderName","file","bArea","cmp","localeCompare","units","emittedFolders","Set","areaSeq","root","folder","slugifyName","folderRel","has","displayName","charAt","toUpperCase","slice","generateFolderFile","add","subarea","subareaSlug","requestDir","subareaRel","subareaDisplayName","entry","buildOpEntries","key","opKey","fingerprint","computeOpFingerprint","render","renderOpFile","result","runIncrementalCodegen","codegenVersion","BRUNO_CODEGEN_VERSION","filesToWrite","manifest","deletedPaths","skippedOpCount","skippedUnitCount","requests","route","routes","op","operations","resolveModifiers","includes","requestName","name","path","entries","seq","fileName","method","sanitizePath","generateRequestFile","brunoExt","pluginExtensions","pluginOverride","Array","isArray","mergePluginFile","referencedModels","collectTransitiveModelRefs","collectOpTypeNodes","modelSnapshot","m","get","routeShape","params","modifiers","security","description","hashFingerprint","v","rootMeta","rootFile","models","out","pushFromParamSource","query","headers","request","body","bodies","bodyType","resp","responses","h","type","src","kind","n","nodes","node","emptyManifest","emptyIncrementalManifest","parseManifest","parsed","parseIncrementalManifest","files","length","Object","keys","raw","JSON","parse","every","f","version","INCREMENTAL_MANIFEST_VERSION","deepMerge","base","override","val","generatedYaml","pluginFileContent","overrideParsed","parseYaml","merged","stringifyYaml","lineWidth","scheme","lines","yamlString","renderAuthBlock","join","map","variables","renderEnvFile","displayNameFor","defaultVars","baseUrl","varName","authEnvVarNames","value","escaped","String","replace","envKey","openCollectionPath","pathParams","extractPathParamNames","findParamType","optional","queryParams","expandParamSource","e","allParams","p","paramExampleValue","default","headerEntries","resolveSecurity","SECURITY_NONE","preferredOrder","primary","ct","find","contentType","json","stringify","typeToExampleValue","jsonLine","split","expectedStatus","pickAssertionStatus","assertedResponse","r","statusCode","requiredHeaders","filter","toLowerCase","docs","buildRequestDocs","docLine","success","parts","trim","tag","desc","indent","i","in","headerName","Map","model","set","resolveModelFields","collected","bases","baseModel","fields","source","visibility","defaultValue","yamlDoubleQuoted","values","random","randomScalarTemplate","item","items","t","members","modelToExampleObject","inner","fieldsToExampleObject","obj","field","matchAll","basename","test","serializeIncrementalManifest","CACHE_MANIFEST_FILENAME","validateBrunoExtension","value","Array","isArray","errors","describe","key","val","Object","entries","push","length","undefined","plugin","name","validateExtension","generateTargets","opRoots","contractRoots","ctx","auth","config","options","runBrunoCodegen","createBrunoPlugin","rootDir","base","baseDir","resolve","outDir","output","collectionName","basename","manifestPath","cacheDir","prevManifest","cacheEnabled","readManifest","emptyManifest","result","generateOpenCollectionIncremental","randomExamples","includeInternal","environments","relPath","existsSync","deleteStalePaths","deletedPaths","relativePath","content","filesToWrite","emitFile","writeManifest","manifest","parseManifest","readFileSync","mkdirSync","dirname","recursive","writeFileSync","serializeIncrementalManifest","relPaths","removedDirs","Set","rel","abs","rmSync","force","add","dir","current","startsWith","readdirSync","rmdirSync"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/codegen-bruno.ts"],"sourcesContent":["import { resolve, basename, dirname } from 'node:path';\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync, rmdirSync } from 'node:fs';\nimport { generateOpenCollectionIncremental, parseManifest, emptyManifest } from './codegen-bruno.js';\nimport type { BrunoSecurityScheme } from './codegen-bruno.js';\nimport { serializeIncrementalManifest } from '@contractkit/core';\nimport type { ContractKitPlugin, PluginContext, PluginValue, OpRootNode, ContractRootNode, IncrementalManifest } from '@contractkit/core';\n\n/** Filename for the persisted Bruno manifest under the CLI cache directory. */\nconst CACHE_MANIFEST_FILENAME = 'bruno-manifest.json';\n\n/** Configuration accepted by the Bruno plugin, both via `contractkit.config.json` and `createBrunoPlugin`. */\nexport interface BrunoPluginConfig {\n baseDir?: string;\n output?: string;\n collectionName?: string;\n /**\n * When true (default), example values use Bruno's faker templates\n * (`{{$randomUUID}}`, `{{$randomEmail}}`, etc.) so each send produces\n * fresh data. Set to false for deterministic placeholders.\n */\n randomExamples?: boolean;\n /**\n * Whether to generate request files for operations marked `internal`. Defaults to\n * `true` — Bruno collections are typically used by the team that owns the API and\n * benefit from full coverage. Set to `false` to omit internal ops.\n */\n includeInternal?: boolean;\n /**\n * Map of environment name → variables. Each entry produces a\n * `environments/<name>.yml` file. When omitted, a default `local.yml` is\n * emitted with `baseUrl=http://localhost:3000` and any auth env-var\n * placeholders. When provided, the default is replaced entirely; include\n * auth variables (e.g. `token`) explicitly if you need them.\n */\n environments?: Record<string, Record<string, unknown>>;\n}\n\n/** Full plugin options shape read from `ctx.options` — extends {@link BrunoPluginConfig} with the `auth` block. */\nexport interface BrunoPluginOptions extends BrunoPluginConfig {\n auth?: { defaultScheme: string; schemes?: Record<string, BrunoSecurityScheme> };\n}\n\n// ─── Default export: loaded via plugins array, reads config from ctx.options ─\n\n/**\n * Validates a `plugins.bruno` extension entry on an operation. The expected shape is\n * `{ template?: string }`, where `template` is a YAML fragment to deep-merge into the\n * generated request file (typically a `file://...` URL whose contents have already\n * been loaded by the CLI resolver).\n */\nexport function validateBrunoExtension(value: PluginValue): { errors?: string[] } | void {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return { errors: [`expected an object, got ${describe(value)}`] };\n }\n const errors: string[] = [];\n for (const [key, val] of Object.entries(value)) {\n if (key === 'template') {\n if (typeof val !== 'string') errors.push(`'template' must be a string, got ${describe(val)}`);\n } else {\n errors.push(`unknown field '${key}' (allowed: template)`);\n }\n }\n return errors.length ? { errors } : undefined;\n}\n\nfunction describe(value: PluginValue): string {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n return typeof value;\n}\n\nconst plugin: ContractKitPlugin = {\n name: 'bruno',\n validateExtension: validateBrunoExtension,\n async generateTargets({ opRoots, contractRoots }, ctx) {\n const { auth, ...config } = ctx.options as BrunoPluginOptions;\n await runBrunoCodegen(opRoots, contractRoots, ctx, config, auth);\n },\n};\n\nexport default plugin;\n\n// ─── Factory: for programmatic use with explicit config ────────────────────\n\n/**\n * Creates a Bruno plugin instance with explicit configuration, for programmatic use.\n *\n * Prefer the default export when loading via `contractkit.config.json`. Use this\n * factory when constructing the plugin in code (e.g. in tests or custom build scripts).\n *\n * @param config - Plugin configuration (output paths and feature flags).\n * @param rootDir - Absolute path used to resolve relative paths in `config`.\n * @param auth - Optional auth scheme configuration mirroring the `auth` key in plugin options.\n */\nexport function createBrunoPlugin(\n config: BrunoPluginConfig,\n rootDir: string,\n auth?: { defaultScheme: string; schemes?: Record<string, BrunoSecurityScheme> },\n): ContractKitPlugin {\n return {\n name: 'bruno',\n validateExtension: validateBrunoExtension,\n async generateTargets({ opRoots, contractRoots }, ctx) {\n // The factory captures rootDir at creation time; ctx.rootDir may differ when the\n // plugin is loaded via a config file, so respect the explicit one passed in here.\n await runBrunoCodegen(opRoots, contractRoots, { ...ctx, rootDir }, config, auth);\n },\n };\n}\n\n/**\n * Shared orchestration used by both the default export and {@link createBrunoPlugin}.\n *\n * Reads the prior manifest, runs the cache-aware codegen, deletes any files that\n * are no longer produced, and emits the changed files plus the new manifest. When\n * `ctx.cacheEnabled` is `false` (e.g. `--force`) the prior manifest is ignored so\n * every op regenerates.\n */\nasync function runBrunoCodegen(\n opRoots: OpRootNode[],\n contractRoots: ContractRootNode[],\n ctx: PluginContext,\n config: BrunoPluginConfig,\n auth: BrunoPluginOptions['auth'],\n): Promise<void> {\n const base = config.baseDir ? resolve(ctx.rootDir, config.baseDir) : ctx.rootDir;\n const outDir = resolve(base, config.output ?? 'bruno-collection');\n const collectionName = config.collectionName ?? basename(ctx.rootDir);\n const manifestPath = resolve(ctx.cacheDir, CACHE_MANIFEST_FILENAME);\n\n const prevManifest: IncrementalManifest = ctx.cacheEnabled ? readManifest(manifestPath) : emptyManifest();\n\n const result = generateOpenCollectionIncremental(\n opRoots,\n {\n collectionName,\n contractRoots,\n auth,\n randomExamples: config.randomExamples ?? true,\n includeInternal: config.includeInternal,\n environments: config.environments,\n },\n prevManifest,\n relPath => existsSync(resolve(outDir, relPath)),\n );\n\n deleteStalePaths(outDir, result.deletedPaths);\n\n for (const { relativePath, content } of result.filesToWrite) {\n ctx.emitFile(resolve(outDir, relativePath), content);\n }\n\n // Persist the manifest under the CLI cache dir, separate from the bruno output.\n writeManifest(manifestPath, result.manifest);\n}\n\n/** Read the previous run's manifest from `manifestPath`. Returns an empty manifest when missing or unreadable. */\nfunction readManifest(manifestPath: string): IncrementalManifest {\n if (!existsSync(manifestPath)) return emptyManifest();\n try {\n return parseManifest(readFileSync(manifestPath, 'utf-8'));\n } catch {\n return emptyManifest();\n }\n}\n\n/** Write the manifest to `manifestPath`. Creates parent dirs as needed. Errors are swallowed so a broken cache never blocks the build. */\nfunction writeManifest(manifestPath: string, manifest: IncrementalManifest): void {\n try {\n mkdirSync(dirname(manifestPath), { recursive: true });\n writeFileSync(manifestPath, serializeIncrementalManifest(manifest), 'utf-8');\n } catch {\n // best-effort\n }\n}\n\n/**\n * Delete files that the previous run tracked but the current run doesn't produce, then\n * walk back up each affected directory and remove it if it's now empty (stopping at outDir).\n * Anything user-added survives because the manifest only ever lists plugin-generated paths.\n */\nfunction deleteStalePaths(outDir: string, relPaths: string[]): void {\n if (relPaths.length === 0) return;\n const removedDirs = new Set<string>();\n for (const rel of relPaths) {\n const abs = resolve(outDir, rel);\n if (existsSync(abs)) {\n rmSync(abs, { force: true });\n removedDirs.add(dirname(abs));\n }\n }\n for (const dir of removedDirs) {\n let current = dir;\n while (current.startsWith(outDir) && current !== outDir) {\n try {\n if (readdirSync(current).length === 0) {\n rmdirSync(current);\n current = dirname(current);\n } else {\n break;\n }\n } catch {\n break;\n }\n }\n }\n}\n\n","import type {\n OpRootNode,\n OpRouteNode,\n OpOperationNode,\n OpResponseNode,\n ParamSource,\n ContractTypeNode,\n ContractRootNode,\n ModelNode,\n FieldNode,\n IncrementalManifest,\n IncrementalUnit,\n IncrementalResult as IncrementalResultBase,\n} from '@contractkit/core';\nimport {\n resolveSecurity,\n resolveModifiers,\n SECURITY_NONE,\n collectTransitiveModelRefs,\n runIncrementalCodegen,\n parseIncrementalManifest,\n emptyIncrementalManifest,\n serializeIncrementalManifest,\n hashFingerprint,\n INCREMENTAL_MANIFEST_VERSION,\n} from '@contractkit/core';\nimport { basename } from 'path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\n\n/** A single file produced by the Bruno codegen — a relative output path and its YAML content. */\nexport interface OpenCollectionFile {\n relativePath: string;\n content: string;\n}\n\n/** Manifest filename — tracks which files this plugin previously generated so subsequent runs can clean up only those, leaving any user-added files alone. */\nexport const MANIFEST_FILENAME = '.contractkit-bruno-manifest.json';\n\n/**\n * Bumped whenever the codegen output shape changes in a way that should bust\n * every per-op fingerprint. Mixed into the per-op fingerprint so a plugin\n * upgrade forces full regeneration even when source `.ck` files are unchanged.\n */\nexport const BRUNO_CODEGEN_VERSION = '1';\n\n/** Subset of a security scheme sufficient for Bruno auth generation (non-HMAC). */\nexport interface BrunoSecurityScheme {\n type: string; // \"http\" | \"apiKey\" | \"oauth2\" | \"openIdConnect\"\n scheme?: string; // \"bearer\" | \"basic\" (when type === \"http\")\n name?: string; // header/query param name (when type === \"apiKey\")\n in?: string; // \"header\" | \"query\" (when type === \"apiKey\")\n}\n\n/** Auth configuration passed to the Bruno codegen; drives collection-level auth and per-operation auth blocks. */\nexport interface BrunoAuthOptions {\n /** Name of the default scheme (from config.security.default) */\n defaultScheme?: string;\n /** Scheme definitions keyed by name (non-HMAC only) */\n schemes?: Record<string, BrunoSecurityScheme>;\n}\n\n/** Options controlling what {@link generateOpenCollection} emits. */\nexport interface OpenCollectionOptions {\n collectionName: string;\n contractRoots?: ContractRootNode[];\n auth?: BrunoAuthOptions;\n /**\n * When true, emit Bruno faker template strings (e.g. `{{$randomUUID}}`,\n * `{{$randomEmail}}`) for compatible scalar types so each send produces\n * fresh data. When false (default), use deterministic placeholders.\n */\n randomExamples?: boolean;\n /**\n * Whether to generate request files for operations marked `internal`. Defaults to\n * `true` — Bruno collections are typically used by the team that owns the API and\n * benefit from full coverage. Set to `false` to omit internal ops.\n */\n includeInternal?: boolean;\n /**\n * Map of environment name → variables. Each entry produces a\n * `environments/<name>.yml` file with the variables in declaration order.\n * Values are coerced to strings.\n *\n * When omitted, a default `environments/local.yml` is emitted with\n * `baseUrl=http://localhost:3000` plus any auth env-var placeholders the\n * default scheme requires. When provided, the default is replaced entirely\n * — auth vars are not auto-injected, so include them explicitly if needed.\n */\n environments?: Record<string, Record<string, unknown>>;\n}\n\n/** Per-operation bookkeeping computed up front: where the file lives, what to call it, and the YAML inputs needed to render or fingerprint it. */\ninterface OpEntry {\n /** Stable identifier across runs — `<file>::<METHOD> <path>`. */\n opKey: string;\n /** Output path relative to the collection root (e.g. `users/get-user.yml`). */\n relativePath: string;\n /** Display name used inside the YAML `info:` block (alphabetized within its folder). */\n requestName: string;\n /** 1-based sequence within the request's containing folder, drives Bruno's UI ordering. */\n seq: number;\n route: OpRouteNode;\n op: OpOperationNode;\n root: OpRootNode;\n}\n\n/**\n * @deprecated Use {@link IncrementalManifest} from `@contractkit/core`. Re-exported here for backwards compatibility.\n */\nexport type BrunoManifest = IncrementalManifest;\n\n/** Result of {@link generateOpenCollectionIncremental}. Renamed `skippedOpCount` for Bruno-specific clarity but otherwise the shared {@link IncrementalResultBase}. */\nexport interface IncrementalResult extends Omit<IncrementalResultBase, 'skippedUnitCount'> {\n /** Number of ops whose codegen was skipped because their fingerprint matched. */\n skippedOpCount: number;\n}\n\n/**\n * Generates an OpenCollection (https://spec.opencollection.com/) API collection\n * from a set of operation roots. Produces opencollection.yml, an environment\n * file, one .yml request file per operation, and the tracking manifest.\n *\n * This is the full-regeneration entry point — every file is rebuilt from scratch.\n * For cache-aware incremental builds, use {@link generateOpenCollectionIncremental}.\n *\n * The manifest appears in the returned list (under {@link MANIFEST_FILENAME}) for\n * convenience — single-shot callers can write the entire array as-is. The plugin's\n * incremental path persists the manifest separately (under the CLI cache dir), so\n * its `filesToWrite` does not include the manifest.\n */\nexport function generateOpenCollection(roots: OpRootNode[], options: OpenCollectionOptions): OpenCollectionFile[] {\n const result = generateOpenCollectionIncremental(roots, options, emptyManifest());\n return [...result.filesToWrite, { relativePath: MANIFEST_FILENAME, content: serializeIncrementalManifest(result.manifest) }];\n}\n\n/**\n * Cache-aware variant of {@link generateOpenCollection}. Skips re-rendering YAML\n * for any op whose fingerprint matches the entry in `prevManifest`. The caller is\n * responsible for emitting `filesToWrite`, deleting `deletedPaths`, and persisting\n * `manifest` so the next run can match against it.\n *\n * Global files (collection root, env files, folder.yml) are always regenerated —\n * they're cheap and depend on options the manifest doesn't fingerprint.\n */\nexport function generateOpenCollectionIncremental(\n roots: OpRootNode[],\n options: OpenCollectionOptions,\n prevManifest: IncrementalManifest,\n fileExists: (relativePath: string) => boolean = () => true,\n): IncrementalResult {\n const modelMap = buildModelMap(options.contractRoots ?? []);\n const authOpts = options.auth;\n const defaultScheme = authOpts?.defaultScheme ? authOpts.schemes?.[authOpts.defaultScheme] : undefined;\n const randomExamples = options.randomExamples ?? false;\n const includeInternal = options.includeInternal ?? true;\n\n // ── Global files (collection root + env files + folder.yml per area/subarea) ──\n const globalFiles: OpenCollectionFile[] = [];\n globalFiles.push({\n relativePath: 'opencollection.yml',\n content: generateCollectionRoot(options.collectionName, defaultScheme),\n });\n for (const envFile of generateEnvFiles(options.environments, defaultScheme)) {\n globalFiles.push(envFile);\n }\n\n const sortedRoots = [...roots].sort((a, b) => {\n const aArea = a.meta['area'] ?? deriveFolderName(a.file);\n const bArea = b.meta['area'] ?? deriveFolderName(b.file);\n const cmp = aArea.localeCompare(bArea);\n if (cmp !== 0) return cmp;\n return (a.meta['subarea'] ?? '').localeCompare(b.meta['subarea'] ?? '');\n });\n\n const units: IncrementalUnit[] = [];\n // Track which folder.yml paths we've already emitted so multiple roots sharing\n // an area (the area's own root + its subarea roots) don't each push the\n // area-level folder.yml — that produced N near-duplicate emits per area, only\n // the last of which survived on disk.\n const emittedFolders = new Set<string>();\n let areaSeq = 0;\n for (const root of sortedRoots) {\n const folder = root.meta['area'] ? slugifyName(root.meta['area']) : deriveFolderName(root.file);\n const folderRel = `${folder}/folder.yml`;\n if (!emittedFolders.has(folderRel)) {\n const displayName = (root.meta['area'] ?? folder).charAt(0).toUpperCase() + (root.meta['area'] ?? folder).slice(1);\n areaSeq++;\n globalFiles.push({ relativePath: folderRel, content: generateFolderFile(displayName, areaSeq) });\n emittedFolders.add(folderRel);\n }\n\n const subarea = root.meta['subarea'];\n const subareaSlug = subarea ? slugifyName(subarea) : undefined;\n const requestDir = subareaSlug ? `${folder}/${subareaSlug}` : folder;\n\n if (subareaSlug) {\n const subareaRel = `${requestDir}/folder.yml`;\n if (!emittedFolders.has(subareaRel)) {\n const subareaDisplayName = subarea!.charAt(0).toUpperCase() + subarea!.slice(1);\n globalFiles.push({ relativePath: subareaRel, content: generateFolderFile(subareaDisplayName, 1) });\n emittedFolders.add(subareaRel);\n }\n }\n\n for (const entry of buildOpEntries(root, requestDir, includeInternal)) {\n units.push({\n key: entry.opKey,\n fingerprint: computeOpFingerprint(entry, modelMap, defaultScheme, randomExamples),\n render: () => [renderOpFile(entry, modelMap, defaultScheme, randomExamples)],\n });\n }\n }\n\n const result = runIncrementalCodegen({\n codegenVersion: BRUNO_CODEGEN_VERSION,\n prevManifest,\n globalFiles,\n units,\n fileExists,\n });\n\n return {\n filesToWrite: result.filesToWrite,\n manifest: result.manifest,\n deletedPaths: result.deletedPaths,\n skippedOpCount: result.skippedUnitCount,\n };\n}\n\n/** Build the ordered, alphabetized op entries for a single root. seq numbers are assigned in alphabetical order so the Bruno UI shows requests sorted by display name. */\nfunction buildOpEntries(root: OpRootNode, requestDir: string, includeInternal: boolean): OpEntry[] {\n const requests: Array<{ route: OpRouteNode; op: OpOperationNode; requestName: string }> = [];\n for (const route of root.routes) {\n for (const op of route.operations) {\n if (!includeInternal && resolveModifiers(route, op).includes('internal')) continue;\n requests.push({ route, op, requestName: op.name ?? route.path });\n }\n }\n requests.sort((a, b) => a.requestName.localeCompare(b.requestName));\n\n const entries: OpEntry[] = [];\n let seq = 1;\n for (const { route, op, requestName } of requests) {\n const fileName = op.name ? `${slugifyName(op.name)}.yml` : `${op.method}-${sanitizePath(route.path)}.yml`;\n entries.push({\n opKey: `${root.file}::${op.method.toUpperCase()} ${route.path}`,\n relativePath: `${requestDir}/${fileName}`,\n requestName,\n seq,\n route,\n op,\n root,\n });\n seq++;\n }\n return entries;\n}\n\n/** Render a single op's request file (including any plugin-extension YAML override). */\nfunction renderOpFile(\n entry: OpEntry,\n modelMap: Map<string, ModelNode>,\n defaultScheme: BrunoSecurityScheme | undefined,\n randomExamples: boolean,\n): OpenCollectionFile {\n let content = generateRequestFile(entry.route, entry.op, entry.requestName, entry.seq, modelMap, entry.root, defaultScheme, randomExamples);\n const brunoExt = entry.op.pluginExtensions?.['bruno'];\n const pluginOverride = brunoExt && typeof brunoExt === 'object' && !Array.isArray(brunoExt) ? brunoExt['template'] : undefined;\n if (typeof pluginOverride === 'string') {\n content = mergePluginFile(content, pluginOverride);\n }\n return { relativePath: entry.relativePath, content };\n}\n\n/** Compute a fingerprint covering every input that affects this op's rendered file. Stable across runs given identical inputs. */\nfunction computeOpFingerprint(\n entry: OpEntry,\n modelMap: Map<string, ModelNode>,\n defaultScheme: BrunoSecurityScheme | undefined,\n randomExamples: boolean,\n): string {\n const referencedModels = collectTransitiveModelRefs(collectOpTypeNodes(entry.route, entry.op), modelMap);\n const modelSnapshot: Record<string, unknown> = {};\n for (const name of [...referencedModels].sort()) {\n const m = modelMap.get(name);\n if (m) modelSnapshot[name] = m;\n }\n // Only include the route fields this op actually depends on. We deliberately exclude\n // `route.operations` so a change to a sibling op doesn't invalidate this op's cache.\n const routeShape = {\n path: entry.route.path,\n params: entry.route.params ?? null,\n modifiers: entry.route.modifiers ?? null,\n security: entry.route.security ?? null,\n description: entry.route.description ?? null,\n };\n return hashFingerprint({\n v: BRUNO_CODEGEN_VERSION,\n opKey: entry.opKey,\n relativePath: entry.relativePath,\n requestName: entry.requestName,\n seq: entry.seq,\n route: routeShape,\n op: entry.op,\n rootMeta: entry.root.meta,\n rootFile: entry.root.file,\n defaultScheme: defaultScheme ?? null,\n randomExamples,\n models: modelSnapshot,\n });\n}\n\n/** Collect every ContractTypeNode that contributes to this op's rendered output. Used as the seed set for transitive model collection in {@link computeOpFingerprint}. */\nfunction collectOpTypeNodes(route: OpRouteNode, op: OpOperationNode): ContractTypeNode[] {\n const out: ContractTypeNode[] = [];\n pushFromParamSource(route.params, out);\n pushFromParamSource(op.query, out);\n pushFromParamSource(op.headers, out);\n if (op.request) {\n for (const body of op.request.bodies) out.push(body.bodyType);\n }\n for (const resp of op.responses) {\n for (const body of resp.bodies) out.push(body.bodyType);\n if (resp.headers) {\n for (const h of resp.headers) out.push(h.type);\n }\n }\n return out;\n}\n\nfunction pushFromParamSource(src: ParamSource | undefined, out: ContractTypeNode[]): void {\n if (!src) return;\n if (src.kind === 'params') {\n for (const n of src.nodes) out.push(n.type);\n } else if (src.kind === 'ref') {\n out.push({ kind: 'ref', name: src.name } as ContractTypeNode);\n } else if (src.kind === 'type') {\n out.push(src.node);\n }\n}\n\n/** Empty-state manifest, used when none has been written yet (or when the cache is being bypassed). */\nexport function emptyManifest(): IncrementalManifest {\n return emptyIncrementalManifest(BRUNO_CODEGEN_VERSION);\n}\n\n/**\n * Parse a previously-written manifest. Accepts both v1 (`{ files: string[] }`) and v2\n * (full {@link IncrementalManifest}) shapes. v1 manifests are returned with an empty\n * `units` map so the next run treats every op as a cache miss while still cleaning up\n * the tracked file list.\n *\n * Returns an empty manifest on any parse error so a stale/garbled file never blocks\n * regeneration.\n */\nexport function parseManifest(content: string): IncrementalManifest {\n const parsed = parseIncrementalManifest(content);\n // The shared parser handles v2. If v2 parsing produced an empty manifest, fall\n // through to a v1 shape ({ files: [...] }) and migrate it for cleanup purposes.\n if (parsed.files.length > 0 || Object.keys(parsed.units).length > 0) return parsed;\n try {\n const raw = JSON.parse(content);\n if (Array.isArray(raw?.files) && raw.files.every((f: unknown) => typeof f === 'string')) {\n return {\n version: INCREMENTAL_MANIFEST_VERSION,\n codegenVersion: '',\n files: raw.files as string[],\n units: {},\n };\n }\n } catch {\n // fall through\n }\n return emptyManifest();\n}\n\n// ─── Plugin file merge ─────────────────────────────────────────────────────\n\nfunction deepMerge(base: unknown, override: unknown): unknown {\n if (\n override !== null &&\n typeof override === 'object' &&\n !Array.isArray(override) &&\n base !== null &&\n typeof base === 'object' &&\n !Array.isArray(base)\n ) {\n const result: Record<string, unknown> = { ...(base as Record<string, unknown>) };\n for (const [key, val] of Object.entries(override as Record<string, unknown>)) {\n result[key] = deepMerge(result[key], val);\n }\n return result;\n }\n return override;\n}\n\n/**\n * Deep-merges a YAML override string into a generated YAML string.\n *\n * Objects are merged recursively; arrays and scalars in the override replace the\n * generated value entirely. If `pluginFileContent` is not a YAML mapping (e.g. it\n * is a scalar or a list), the generated content is returned unchanged.\n *\n * @param generatedYaml - The YAML string produced by the Bruno codegen.\n * @param pluginFileContent - The YAML override string to merge in.\n * @returns The merged YAML string, or `generatedYaml` if the override is not a mapping.\n */\nexport function mergePluginFile(generatedYaml: string, pluginFileContent: string): string {\n const overrideParsed = parseYaml(pluginFileContent);\n if (overrideParsed === null || typeof overrideParsed !== 'object' || Array.isArray(overrideParsed)) {\n return generatedYaml;\n }\n const merged = deepMerge(parseYaml(generatedYaml), overrideParsed);\n return stringifyYaml(merged, { lineWidth: 0 });\n}\n\n// ─── File generators ───────────────────────────────────────────────────────\n\nfunction generateCollectionRoot(name: string, scheme?: BrunoSecurityScheme): string {\n const lines = [`opencollection: \"1.0.0\"`, `info:`, ` name: ${yamlString(name)}`];\n if (scheme) {\n lines.push(``);\n lines.push(`request:`);\n lines.push(...renderAuthBlock(scheme, ' '));\n }\n lines.push(``);\n return lines.join('\\n');\n}\n\nfunction generateEnvFiles(\n environments: Record<string, Record<string, unknown>> | undefined,\n scheme: BrunoSecurityScheme | undefined,\n): OpenCollectionFile[] {\n if (environments && Object.keys(environments).length > 0) {\n return Object.entries(environments).map(([name, variables]) => ({\n relativePath: `environments/${name}.yml`,\n content: renderEnvFile(displayNameFor(name), variables),\n }));\n }\n const defaultVars: Record<string, string> = { baseUrl: 'http://localhost:3000' };\n if (scheme) {\n for (const varName of authEnvVarNames(scheme)) defaultVars[varName] = '';\n }\n return [{ relativePath: 'environments/local.yml', content: renderEnvFile('Local', defaultVars) }];\n}\n\nfunction renderEnvFile(name: string, variables: Record<string, unknown>): string {\n const lines = [`name: ${name}`, `variables:`];\n for (const [key, value] of Object.entries(variables)) {\n const escaped = String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n lines.push(` - name: ${key}`);\n lines.push(` value: \"${escaped}\"`);\n }\n lines.push(``);\n return lines.join('\\n');\n}\n\nfunction displayNameFor(envKey: string): string {\n return envKey.charAt(0).toUpperCase() + envKey.slice(1);\n}\n\nfunction generateFolderFile(name: string, seq: number): string {\n return [`info:`, ` name: ${yamlString(name)}`, ` type: folder`, ` seq: ${seq}`, ``].join('\\n');\n}\n\nfunction generateRequestFile(\n route: OpRouteNode,\n op: OpOperationNode,\n name: string,\n seq: number,\n modelMap: Map<string, ModelNode>,\n root?: OpRootNode,\n defaultScheme?: BrunoSecurityScheme,\n randomExamples = false,\n): string {\n const lines: string[] = [];\n\n lines.push(`info:`);\n lines.push(` name: ${yamlString(name)}`);\n lines.push(` type: http`);\n lines.push(` seq: ${seq}`);\n lines.push(``);\n lines.push(`http:`);\n lines.push(` method: ${op.method.toUpperCase()}`);\n lines.push(` url: ${yamlString(`{{baseUrl}}${openCollectionPath(route.path)}`)}`);\n\n // Params — flat array with type: \"path\" | \"query\". Optional query params are\n // emitted with disabled: true so users opt in before sending.\n const pathParams: Array<ParamEntry & { kind: 'path' | 'query' }> = extractPathParamNames(route.path).map(n => ({\n name: n,\n type: findParamType(route.params, n, modelMap),\n optional: false,\n kind: 'path' as const,\n }));\n const queryParams: Array<ParamEntry & { kind: 'path' | 'query' }> = op.query\n ? expandParamSource(op.query, modelMap).map(e => ({ ...e, kind: 'query' as const }))\n : [];\n const allParams = [...pathParams, ...queryParams];\n\n if (allParams.length > 0) {\n lines.push(` params:`);\n for (const p of allParams) {\n lines.push(` - name: ${p.name}`);\n lines.push(` value: ${paramExampleValue(p.type, p.default, randomExamples)}`);\n lines.push(` type: ${p.kind}`);\n if (p.optional && p.kind === 'query') lines.push(` disabled: true`);\n }\n }\n\n // Headers\n if (op.headers) {\n const headerEntries = expandParamSource(op.headers, modelMap);\n if (headerEntries.length > 0) {\n lines.push(` headers:`);\n for (const h of headerEntries) {\n lines.push(` - name: ${h.name}`);\n lines.push(` value: ${paramExampleValue(h.type, h.default, randomExamples)}`);\n if (h.optional) lines.push(` disabled: true`);\n }\n }\n }\n\n // Auth — inside http block; inherit collection default unless this op is explicitly public\n if (defaultScheme) {\n const security = root ? resolveSecurity(route, op, root) : (op.security ?? route.security);\n if (security === SECURITY_NONE) {\n lines.push(` auth:`);\n lines.push(` type: none`);\n } else {\n lines.push(` auth: inherit`);\n }\n }\n\n // Body — Bruno supports a single body per request, so prefer JSON, then form-urlencoded, then multipart.\n if (op.request && op.request.bodies.length > 0) {\n const preferredOrder: Array<(typeof op.request.bodies)[number]['contentType']> = [\n 'application/json',\n 'application/x-www-form-urlencoded',\n 'multipart/form-data',\n ];\n const primary =\n preferredOrder.map(ct => op.request!.bodies.find(b => b.contentType === ct)).find(b => b !== undefined) ?? op.request.bodies[0]!;\n\n lines.push(` body:`);\n if (primary.contentType === 'multipart/form-data') {\n lines.push(` type: multipart-form`);\n lines.push(` data: []`);\n } else if (primary.contentType === 'application/x-www-form-urlencoded') {\n lines.push(` type: form-urlencoded`);\n lines.push(` data: []`);\n } else {\n const json = JSON.stringify(typeToExampleValue(primary.bodyType, modelMap, randomExamples), null, 2);\n lines.push(` type: json`);\n lines.push(` data: |`);\n for (const jsonLine of json.split('\\n')) {\n lines.push(` ${jsonLine}`);\n }\n }\n }\n\n // runtime.assertions — auto-generate a status-code check and presence checks for required response headers.\n const expectedStatus = pickAssertionStatus(op.responses);\n const assertedResponse = op.responses.find(r => r.statusCode === expectedStatus);\n const requiredHeaders = (assertedResponse?.headers ?? []).filter(h => !h.optional);\n if (expectedStatus !== undefined) {\n lines.push(``);\n lines.push(`runtime:`);\n lines.push(` assertions:`);\n lines.push(` - expression: res.status`);\n lines.push(` operator: eq`);\n // Always quote — the OpenCollection schema types `value` as a string,\n // so we must keep \"200\" from being parsed as YAML number 200.\n lines.push(` value: \"${expectedStatus}\"`);\n for (const h of requiredHeaders) {\n lines.push(` - expression: res.headers[\"${h.name.toLowerCase()}\"]`);\n lines.push(` operator: isDefined`);\n lines.push(` value: \"\"`);\n }\n }\n\n // docs — combine route- and operation-level descriptions plus the declared response-header summary.\n const docs = buildRequestDocs(route, op, assertedResponse);\n if (docs) {\n lines.push(``);\n lines.push(`docs: |-`);\n for (const docLine of docs.split('\\n')) {\n lines.push(` ${docLine}`);\n }\n }\n\n lines.push(``);\n return lines.join('\\n');\n}\n\n/** Pick the response whose status code we'll assert against. Prefers the first declared 2xx; otherwise falls back to the first declared response. Returns undefined if no responses are declared. */\nfunction pickAssertionStatus(responses: OpResponseNode[]): number | undefined {\n const success = responses.find(r => r.statusCode >= 200 && r.statusCode < 300);\n return success?.statusCode ?? responses[0]?.statusCode;\n}\n\n/** Build a markdown docs block from route- and operation-level descriptions, plus declared response-header summary. */\nfunction buildRequestDocs(route: OpRouteNode, op: OpOperationNode, assertedResponse?: OpResponseNode): string | undefined {\n const parts: string[] = [];\n if (route.description) parts.push(route.description.trim());\n if (op.description) parts.push(op.description.trim());\n const headers = assertedResponse?.headers ?? [];\n if (headers.length > 0) {\n const lines = ['**Response headers**', ''];\n for (const h of headers) {\n const tag = h.optional ? 'optional' : 'required';\n const desc = h.description ? ` — ${h.description}` : '';\n lines.push(`- \\`${h.name}\\` (${tag})${desc}`);\n }\n parts.push(lines.join('\\n'));\n }\n return parts.length > 0 ? parts.join('\\n\\n') : undefined;\n}\n\n// ─── Auth helpers ──────────────────────────────────────────────────────────\n\n/** Generate the YAML lines for an auth block (flat, per spec), indented by `indent`. */\nfunction renderAuthBlock(scheme: BrunoSecurityScheme, indent: string): string[] {\n const i = indent;\n if (scheme.type === 'http' && scheme.scheme === 'bearer') {\n return [`${i}auth:`, `${i} type: bearer`, `${i} token: \"{{token}}\"`];\n }\n if (scheme.type === 'http' && scheme.scheme === 'basic') {\n return [`${i}auth:`, `${i} type: basic`, `${i} username: \"{{username}}\"`, `${i} password: \"{{password}}\"`];\n }\n if (scheme.type === 'apiKey' && scheme.in === 'header') {\n const headerName = scheme.name ?? 'X-Api-Key';\n return [`${i}auth:`, `${i} type: apikey`, `${i} key: ${headerName}`, `${i} value: \"{{apiKey}}\"`, `${i} placement: header`];\n }\n return [];\n}\n\n/** Return the environment variable names needed for a given auth scheme. */\nfunction authEnvVarNames(scheme: BrunoSecurityScheme): string[] {\n if (scheme.type === 'http' && scheme.scheme === 'bearer') return ['token'];\n if (scheme.type === 'http' && scheme.scheme === 'basic') return ['username', 'password'];\n if (scheme.type === 'apiKey') return ['apiKey'];\n return [];\n}\n\n// ─── Model registry ────────────────────────────────────────────────────────\n\nfunction buildModelMap(contractRoots: ContractRootNode[]): Map<string, ModelNode> {\n const map = new Map<string, ModelNode>();\n for (const root of contractRoots) {\n for (const model of root.models) {\n map.set(model.name, model);\n }\n }\n return map;\n}\n\n/** Resolve all fields for a model, including inherited base fields (bases first, in declaration order). */\nfunction resolveModelFields(model: ModelNode, modelMap: Map<string, ModelNode>): FieldNode[] {\n const collected: FieldNode[] = [];\n if (model.bases) {\n for (const base of model.bases) {\n const baseModel = modelMap.get(base);\n if (baseModel) collected.push(...resolveModelFields(baseModel, modelMap));\n }\n }\n return [...collected, ...model.fields];\n}\n\n// ─── Param helpers ─────────────────────────────────────────────────────────\n\ninterface ParamEntry {\n name: string;\n type: ContractTypeNode | undefined;\n default?: string | number | boolean;\n optional: boolean;\n}\n\n/** Expand a ParamSource into a flat list of named entries with their types. */\nfunction expandParamSource(source: ParamSource, modelMap: Map<string, ModelNode>): ParamEntry[] {\n if (source.kind === 'params') {\n return source.nodes.map(n => ({ name: n.name, type: n.type, default: n.default, optional: n.optional }));\n }\n if (source.kind === 'ref') {\n const model = modelMap.get(source.name);\n if (model) {\n return resolveModelFields(model, modelMap)\n .filter(f => f.visibility !== 'readonly')\n .map(f => ({ name: f.name, type: f.type, default: f.default, optional: f.optional }));\n }\n // Fallback: single placeholder entry\n const name = source.name.charAt(0).toLowerCase() + source.name.slice(1);\n return [{ name, type: undefined, optional: false }];\n }\n // kind === 'type': if it's an inline object, expand its fields\n if (source.node.kind === 'inlineObject') {\n return source.node.fields.map(f => ({ name: f.name, type: f.type, default: f.default, optional: f.optional }));\n }\n return [];\n}\n\n/** Look up a named path param's type from route.params. */\nfunction findParamType(source: ParamSource | undefined, name: string, modelMap: Map<string, ModelNode>): ContractTypeNode | undefined {\n if (!source) return undefined;\n if (source.kind === 'params') return source.nodes.find(n => n.name === name)?.type;\n if (source.kind === 'ref') {\n const model = modelMap.get(source.name);\n if (model) return resolveModelFields(model, modelMap).find(f => f.name === name)?.type;\n }\n if (source.kind === 'type' && source.node.kind === 'inlineObject') {\n return source.node.fields.find(f => f.name === name)?.type;\n }\n return undefined;\n}\n\n/** Return a YAML-quoted example value string for a param, preferring a default value when provided. */\nfunction paramExampleValue(type: ContractTypeNode | undefined, defaultValue?: string | number | boolean, randomExamples = false): string {\n if (defaultValue !== undefined) return yamlDoubleQuoted(defaultValue);\n if (!type) return '\"\"';\n if (type.kind === 'enum') return type.values.length > 0 ? yamlDoubleQuoted(type.values[0]!) : '\"\"';\n if (type.kind === 'literal') return yamlDoubleQuoted(type.value);\n if (type.kind !== 'scalar') return '\"\"';\n if (randomExamples) {\n const random = randomScalarTemplate(type.name);\n if (random !== undefined) return `\"${random}\"`;\n }\n switch (type.name) {\n case 'uuid':\n return '\"00000000-0000-0000-0000-000000000000\"';\n case 'email':\n return '\"user@example.com\"';\n case 'url':\n return '\"https://example.com\"';\n case 'number':\n case 'int':\n case 'bigint':\n return '\"0\"';\n case 'decimal':\n return '\"0.00\"';\n case 'boolean':\n return '\"true\"';\n case 'date':\n return '\"2024-01-01\"';\n case 'time':\n return '\"00:00:00\"';\n case 'datetime':\n return '\"2024-01-01T00:00:00Z\"';\n case 'duration':\n return '\"PT1H\"';\n default:\n return '\"\"';\n }\n}\n\n/** Bruno faker template for a scalar type, or undefined when no clean equivalent exists (date, time, duration, raw string). */\nfunction randomScalarTemplate(name: string): string | undefined {\n switch (name) {\n case 'uuid':\n return '{{$randomUUID}}';\n case 'email':\n return '{{$randomEmail}}';\n case 'url':\n return '{{$randomUrl}}';\n case 'number':\n case 'int':\n case 'bigint':\n return '{{$randomInt}}';\n case 'boolean':\n return '{{$randomBoolean}}';\n case 'datetime':\n return '{{$isoTimestamp}}';\n default:\n return undefined;\n }\n}\n\n// ─── Body helpers ──────────────────────────────────────────────────────────\n\n/**\n * Recursively build an example JSON value from a ContractTypeNode.\n *\n * When `randomExamples` is true we substitute Bruno faker templates only for\n * scalars whose JSON representation is a string (uuid/email/url/datetime).\n * Numbers and booleans stay deterministic — embedding `{{$randomInt}}` as a\n * bare JSON number would require sentinel-stripping the surrounding quotes,\n * and the body skeleton is meant as a starting point users edit anyway.\n */\nfunction typeToExampleValue(type: ContractTypeNode, modelMap: Map<string, ModelNode>, randomExamples = false): unknown {\n switch (type.kind) {\n case 'scalar':\n switch (type.name) {\n case 'string':\n return '';\n case 'email':\n return randomExamples ? '{{$randomEmail}}' : 'user@example.com';\n case 'url':\n return randomExamples ? '{{$randomUrl}}' : 'https://example.com';\n case 'uuid':\n return randomExamples ? '{{$randomUUID}}' : '00000000-0000-0000-0000-000000000000';\n case 'number':\n case 'int':\n case 'bigint':\n return 0;\n case 'decimal':\n // A string, not 0 — `_ZodDecimal` rejects raw JSON numbers, so a numeric\n // skeleton value would generate a request the API is guaranteed to refuse.\n return '0.00';\n case 'boolean':\n return true;\n case 'date':\n return '2024-01-01';\n case 'time':\n return '00:00:00';\n case 'datetime':\n return randomExamples ? '{{$isoTimestamp}}' : '2024-01-01T00:00:00Z';\n case 'duration':\n return 'PT1H';\n case 'null':\n return null;\n default:\n return null;\n }\n case 'enum':\n return type.values[0] ?? '';\n case 'literal':\n return type.value;\n case 'array':\n return [typeToExampleValue(type.item, modelMap, randomExamples)];\n case 'tuple':\n return type.items.map(t => typeToExampleValue(t, modelMap, randomExamples));\n case 'record':\n return {};\n case 'union':\n return type.members.length > 0 ? typeToExampleValue(type.members[0]!, modelMap, randomExamples) : null;\n case 'discriminatedUnion':\n return type.members.length > 0 ? typeToExampleValue(type.members[0]!, modelMap, randomExamples) : null;\n case 'intersection':\n return {};\n case 'ref': {\n const model = modelMap.get(type.name);\n if (!model) return {};\n // Type alias — recurse into the aliased type\n if (model.type) return typeToExampleValue(model.type, modelMap, randomExamples);\n return modelToExampleObject(model, modelMap, randomExamples);\n }\n case 'lazy':\n return typeToExampleValue(type.inner, modelMap, randomExamples);\n case 'inlineObject':\n return fieldsToExampleObject(type.fields, modelMap, randomExamples);\n default:\n return null;\n }\n}\n\n/** Build an example object from a ModelNode's fields (including inherited base fields). */\nfunction modelToExampleObject(model: ModelNode, modelMap: Map<string, ModelNode>, randomExamples = false): Record<string, unknown> {\n return fieldsToExampleObject(resolveModelFields(model, modelMap), modelMap, randomExamples);\n}\n\n/** Build an example object from a list of FieldNodes. Excludes readonly fields; uses defaults when available; omits optional fields that have no default so they don't appear in the JSON output. */\nfunction fieldsToExampleObject(fields: FieldNode[], modelMap: Map<string, ModelNode>, randomExamples = false): Record<string, unknown> {\n const obj: Record<string, unknown> = {};\n for (const field of fields) {\n if (field.visibility === 'readonly') continue;\n if (field.default !== undefined) {\n obj[field.name] = field.default;\n } else if (!field.optional) {\n obj[field.name] = typeToExampleValue(field.type, modelMap, randomExamples);\n }\n }\n return obj;\n}\n\n// ─── Path helpers ──────────────────────────────────────────────────────────\n\n/** Convert /users/{id}/posts → /users/:id/posts (Bruno path parameter syntax) */\nfunction openCollectionPath(path: string): string {\n return path.replace(/\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}/g, ':$1');\n}\n\n/** Convert \"Create an Offer\" → create-an-offer (for .yml file names) */\nexport function slugifyName(name: string): string {\n const result = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n return result || 'request';\n}\n\n/** Convert /users/{id}/posts → users-id-posts (for .yml file names) */\nexport function sanitizePath(path: string): string {\n const result = path\n .replace(/^\\//, '')\n .replace(/\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}/g, '$1')\n .replace(/\\//g, '-')\n .replace(/[^a-zA-Z0-9-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n return result || 'root';\n}\n\n/** Extract param names from a URL template, e.g. /users/{id} → ['id'] */\nfunction extractPathParamNames(path: string): string[] {\n return [...path.matchAll(/\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}/g)].map(m => m[1]!);\n}\n\n/** Derive folder name from op file path, e.g. src/users.op → users */\nfunction deriveFolderName(file: string): string {\n return basename(file).replace(/\\.(op|ck)$/, '');\n}\n\n/**\n * Escape a value into an always-quoted, valid double-quoted YAML scalar.\n *\n * Unlike {@link yamlString} (which only quotes when required), this always wraps\n * the value in double quotes — used for example/param values that must stay\n * strings (e.g. `\"0\"`, `\"true\"`). `.ck` string literals permit unescaped `\"`\n * inside either quote style, so backslashes and quotes are escaped here to keep\n * the emitted YAML scalar valid.\n */\nfunction yamlDoubleQuoted(value: string | number | boolean): string {\n return `\"${String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\n/**\n * Wrap a string in YAML double quotes if it contains characters that require quoting\n * (flow indicators, colons, braces, etc.).\n */\nfunction yamlString(value: string): string {\n if (/[:{}[\\],&*#?|<>=!%@`\"']/.test(value) || /^\\s|\\s$/.test(value)) {\n return `\"${value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n }\n return value;\n}\n"],"mappings":"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","names":["resolve","basename","dirname","existsSync","readFileSync","writeFileSync","mkdirSync","rmSync","readdirSync","rmdirSync","resolveSecurity","resolveModifiers","SECURITY_NONE","collectTransitiveModelRefs","runIncrementalCodegen","parseIncrementalManifest","emptyIncrementalManifest","serializeIncrementalManifest","hashFingerprint","INCREMENTAL_MANIFEST_VERSION","basename","parse","parseYaml","stringify","stringifyYaml","BRUNO_CODEGEN_VERSION","generateOpenCollectionIncremental","roots","options","prevManifest","fileExists","modelMap","buildModelMap","contractRoots","authOpts","auth","defaultScheme","schemes","undefined","randomExamples","includeInternal","globalFiles","push","relativePath","content","generateCollectionRoot","collectionName","envFile","generateEnvFiles","environments","sortedRoots","sort","a","b","aArea","meta","deriveFolderName","file","bArea","cmp","localeCompare","units","emittedFolders","Set","areaSeq","root","folder","slugifyName","folderRel","has","displayName","charAt","toUpperCase","slice","generateFolderFile","add","subarea","subareaSlug","requestDir","subareaRel","subareaDisplayName","entry","buildOpEntries","key","opKey","fingerprint","computeOpFingerprint","render","renderOpFile","result","runIncrementalCodegen","codegenVersion","BRUNO_CODEGEN_VERSION","filesToWrite","manifest","deletedPaths","skippedOpCount","skippedUnitCount","requests","route","routes","op","operations","resolveModifiers","includes","requestName","name","path","entries","seq","fileName","method","sanitizePath","generateRequestFile","brunoExt","pluginExtensions","pluginOverride","Array","isArray","mergePluginFile","referencedModels","collectTransitiveModelRefs","collectOpTypeNodes","modelSnapshot","m","get","routeShape","params","modifiers","security","description","hashFingerprint","v","rootMeta","rootFile","models","out","pushFromParamSource","query","headers","request","body","bodies","bodyType","resp","responses","h","type","src","kind","n","nodes","node","emptyManifest","emptyIncrementalManifest","parseManifest","parsed","parseIncrementalManifest","files","length","Object","keys","raw","JSON","parse","every","f","version","INCREMENTAL_MANIFEST_VERSION","deepMerge","base","override","val","generatedYaml","pluginFileContent","overrideParsed","parseYaml","merged","stringifyYaml","lineWidth","scheme","lines","yamlString","renderAuthBlock","join","map","variables","renderEnvFile","displayNameFor","defaultVars","baseUrl","varName","authEnvVarNames","value","escaped","String","replace","envKey","openCollectionPath","pathParams","extractPathParamNames","findParamType","optional","queryParams","expandParamSource","e","allParams","p","paramExampleValue","default","headerEntries","resolveSecurity","SECURITY_NONE","preferredOrder","primary","ct","find","contentType","json","stringify","typeToExampleValue","jsonLine","split","expectedStatus","pickAssertionStatus","assertedResponse","r","statusCode","requiredHeaders","filter","toLowerCase","docs","buildRequestDocs","docLine","success","parts","trim","tag","desc","indent","i","in","headerName","Map","model","set","resolveModelFields","collected","bases","baseModel","fields","source","visibility","defaultValue","yamlDoubleQuoted","values","random","randomScalarTemplate","item","items","t","members","modelToExampleObject","inner","fieldsToExampleObject","obj","field","matchAll","basename","test","serializeIncrementalManifest","CACHE_MANIFEST_FILENAME","validateBrunoExtension","value","Array","isArray","errors","describe","key","val","Object","entries","push","length","undefined","plugin","name","validateExtension","generateTargets","opRoots","contractRoots","ctx","auth","config","options","runBrunoCodegen","createBrunoPlugin","rootDir","base","baseDir","resolve","outDir","output","collectionName","basename","manifestPath","cacheDir","prevManifest","cacheEnabled","readManifest","emptyManifest","result","generateOpenCollectionIncremental","randomExamples","includeInternal","environments","relPath","existsSync","deleteStalePaths","deletedPaths","relativePath","content","filesToWrite","emitFile","writeManifest","manifest","parseManifest","readFileSync","mkdirSync","dirname","recursive","writeFileSync","serializeIncrementalManifest","relPaths","removedDirs","Set","rel","abs","rmSync","force","add","dir","current","startsWith","readdirSync","rmdirSync"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractkit/plugin-bruno",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "ContractKit built-in plugin: Bruno REST collection generation",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"yaml": "^2.8.3",
|
|
31
|
-
"@contractkit/core": "0.
|
|
31
|
+
"@contractkit/core": "0.28.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@repo/config-eslint": "0.3.1",
|
package/src/codegen-bruno.ts
CHANGED
|
@@ -734,6 +734,8 @@ function paramExampleValue(type: ContractTypeNode | undefined, defaultValue?: st
|
|
|
734
734
|
case 'int':
|
|
735
735
|
case 'bigint':
|
|
736
736
|
return '"0"';
|
|
737
|
+
case 'decimal':
|
|
738
|
+
return '"0.00"';
|
|
737
739
|
case 'boolean':
|
|
738
740
|
return '"true"';
|
|
739
741
|
case 'date':
|
|
@@ -798,6 +800,10 @@ function typeToExampleValue(type: ContractTypeNode, modelMap: Map<string, ModelN
|
|
|
798
800
|
case 'int':
|
|
799
801
|
case 'bigint':
|
|
800
802
|
return 0;
|
|
803
|
+
case 'decimal':
|
|
804
|
+
// A string, not 0 — `_ZodDecimal` rejects raw JSON numbers, so a numeric
|
|
805
|
+
// skeleton value would generate a request the API is guaranteed to refuse.
|
|
806
|
+
return '0.00';
|
|
801
807
|
case 'boolean':
|
|
802
808
|
return true;
|
|
803
809
|
case 'date':
|
|
@@ -243,6 +243,13 @@ describe('generateOpenCollection', () => {
|
|
|
243
243
|
expect(yml!.content).toContain('value: "2024-01-01"');
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
+
it('uses a quoted decimal example for decimal path params', () => {
|
|
247
|
+
const root = opRoot([opRoute('/pay/{amount}', [opOperation('get')], paramNodes([opParam('amount', scalarType('decimal'))]))], 'pay.op');
|
|
248
|
+
const files = generateOpenCollection([root], { collectionName: 'API' });
|
|
249
|
+
const yml = files.find(f => f.relativePath === 'pay/get-pay-amount.yml');
|
|
250
|
+
expect(yml!.content).toContain('value: "0.00"');
|
|
251
|
+
});
|
|
252
|
+
|
|
246
253
|
it('uses ISO 8601 duration example value for duration path params', () => {
|
|
247
254
|
const root = opRoot(
|
|
248
255
|
[opRoute('/jobs/{timeout}', [opOperation('get')], paramNodes([opParam('timeout', scalarType('duration'))]))],
|
|
@@ -517,6 +524,18 @@ describe('generateOpenCollection', () => {
|
|
|
517
524
|
expect(yml!.content).toContain('"timeout": "PT1H"');
|
|
518
525
|
});
|
|
519
526
|
|
|
527
|
+
it('uses a quoted decimal string in the body skeleton, never a JSON number', () => {
|
|
528
|
+
// The switch behind this has a bare `default:` returning null, so a missing case would
|
|
529
|
+
// silently emit `"gross": null`. A numeric `0` would be just as wrong — `_ZodDecimal`
|
|
530
|
+
// rejects raw JSON numbers, so the skeleton would generate a request the API refuses.
|
|
531
|
+
const bodyType = inlineObjectType([field('gross', scalarType('decimal', { scale: 2 }))]);
|
|
532
|
+
const root = opRoot([opRoute('/payslips', [opOperation('post', { request: opRequest(bodyType) })])], 'payslips.op');
|
|
533
|
+
const files = generateOpenCollection([root], { collectionName: 'API' });
|
|
534
|
+
const yml = files.find(f => f.relativePath === 'payslips/post-payslips.yml');
|
|
535
|
+
expect(yml!.content).toContain('"gross": "0.00"');
|
|
536
|
+
expect(yml!.content).not.toContain('"gross": null');
|
|
537
|
+
});
|
|
538
|
+
|
|
520
539
|
it('excludes readonly fields from inline object body skeleton', () => {
|
|
521
540
|
const bodyType = inlineObjectType([
|
|
522
541
|
field('id', scalarType('uuid'), { visibility: 'readonly' }),
|