@voxgig/sdkgen 4.6.0 → 4.7.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.
Files changed (115) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/Feature.js +5 -0
  3. package/dist/cmp/Feature.js.map +1 -1
  4. package/dist/cmp/TestControl.d.ts +4 -0
  5. package/dist/cmp/TestControl.js +51 -0
  6. package/dist/cmp/TestControl.js.map +1 -0
  7. package/dist/helpers/featureSource.d.ts +3 -1
  8. package/dist/helpers/featureSource.js +105 -0
  9. package/dist/helpers/featureSource.js.map +1 -1
  10. package/dist/sdkgen.d.ts +3 -2
  11. package/dist/sdkgen.js +9 -4
  12. package/dist/sdkgen.js.map +1 -1
  13. package/dist/tsconfig.tsbuildinfo +1 -1
  14. package/dist/utility.d.ts +2 -1
  15. package/dist/utility.js +52 -0
  16. package/dist/utility.js.map +1 -1
  17. package/model/sdkgen.aon +58 -0
  18. package/package.json +1 -1
  19. package/project/.sdk/model/feature/secrets.aon +82 -0
  20. package/project/.sdk/src/cmp/cpp/Main_cpp.ts +2 -1
  21. package/project/.sdk/src/cmp/cpp/Test_cpp.ts +5 -1
  22. package/project/.sdk/src/cmp/csharp/Main_csharp.ts +2 -1
  23. package/project/.sdk/src/cmp/csharp/TestDirect_csharp.ts +30 -6
  24. package/project/.sdk/src/cmp/csharp/TestEntity_csharp.ts +29 -5
  25. package/project/.sdk/src/cmp/csharp/Test_csharp.ts +5 -1
  26. package/project/.sdk/src/cmp/dart/Main_dart.ts +2 -1
  27. package/project/.sdk/src/cmp/dart/TestDirect_dart.ts +23 -6
  28. package/project/.sdk/src/cmp/dart/TestEntity_dart.ts +28 -7
  29. package/project/.sdk/src/cmp/dart/Test_dart.ts +5 -1
  30. package/project/.sdk/src/cmp/go/Main_go.ts +2 -1
  31. package/project/.sdk/src/cmp/go/Test_go.ts +5 -1
  32. package/project/.sdk/src/cmp/java/Main_java.ts +2 -1
  33. package/project/.sdk/src/cmp/java/Test_java.ts +5 -1
  34. package/project/.sdk/src/cmp/js/Main_js.ts +2 -1
  35. package/project/.sdk/src/cmp/js/TestDirect_js.ts +30 -5
  36. package/project/.sdk/src/cmp/js/TestEntity_js.ts +35 -5
  37. package/project/.sdk/src/cmp/js/Test_js.ts +5 -1
  38. package/project/.sdk/src/cmp/js/fragment/Direct.test.fragment.js +7 -1
  39. package/project/.sdk/src/cmp/js/fragment/Entity.test.fragment.js +7 -1
  40. package/project/.sdk/src/cmp/kotlin/Main_kotlin.ts +2 -1
  41. package/project/.sdk/src/cmp/kotlin/Test_kotlin.ts +5 -1
  42. package/project/.sdk/src/cmp/lua/Main_lua.ts +2 -1
  43. package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +28 -4
  44. package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +24 -4
  45. package/project/.sdk/src/cmp/lua/Test_lua.ts +5 -1
  46. package/project/.sdk/src/cmp/ocaml/Gitignore_ocaml.ts +1 -0
  47. package/project/.sdk/src/cmp/perl/Main_perl.ts +2 -1
  48. package/project/.sdk/src/cmp/perl/TestDirect_perl.ts +25 -4
  49. package/project/.sdk/src/cmp/perl/TestEntity_perl.ts +24 -4
  50. package/project/.sdk/src/cmp/perl/Test_perl.ts +5 -1
  51. package/project/.sdk/src/cmp/perl/utility_perl.ts +10 -0
  52. package/project/.sdk/src/cmp/php/Main_php.ts +2 -1
  53. package/project/.sdk/src/cmp/php/TestDirect_php.ts +24 -5
  54. package/project/.sdk/src/cmp/php/TestEntity_php.ts +28 -4
  55. package/project/.sdk/src/cmp/php/Test_php.ts +5 -1
  56. package/project/.sdk/src/cmp/py/Main_py.ts +2 -1
  57. package/project/.sdk/src/cmp/py/Test_py.ts +5 -1
  58. package/project/.sdk/src/cmp/rb/Main_rb.ts +2 -1
  59. package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +24 -5
  60. package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +24 -4
  61. package/project/.sdk/src/cmp/rb/Test_rb.ts +5 -1
  62. package/project/.sdk/src/cmp/rb/utility_rb.ts +3 -1
  63. package/project/.sdk/src/cmp/rust/Main_rust.ts +2 -1
  64. package/project/.sdk/src/cmp/rust/TestDirect_rust.ts +31 -4
  65. package/project/.sdk/src/cmp/rust/TestEntity_rust.ts +37 -4
  66. package/project/.sdk/src/cmp/rust/Test_rust.ts +5 -1
  67. package/project/.sdk/src/cmp/scala/Main_scala.ts +2 -1
  68. package/project/.sdk/src/cmp/scala/Test_scala.ts +5 -1
  69. package/project/.sdk/src/cmp/ts/Config_ts.ts +50 -8
  70. package/project/.sdk/src/cmp/ts/Main_ts.ts +10 -2
  71. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +10 -3
  72. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +11 -3
  73. package/project/.sdk/src/cmp/ts/Test_ts.ts +5 -1
  74. package/project/.sdk/tm/csharp/test/Runner.cs +45 -0
  75. package/project/.sdk/tm/dart/test/utility.dart +39 -0
  76. package/project/.sdk/tm/js/test/sdk-test-control.json +19 -0
  77. package/project/.sdk/tm/js/test/utility.js +91 -1
  78. package/project/.sdk/tm/lua/test/runner.lua +40 -0
  79. package/project/.sdk/tm/ocaml/test/harness.ml +14 -4
  80. package/project/.sdk/tm/perl/t/runner.pm +30 -0
  81. package/project/.sdk/tm/php/test/Runner.php +36 -0
  82. package/project/.sdk/tm/rb/test/runner.rb +25 -0
  83. package/project/.sdk/tm/rust/tests/common/mod.rs +32 -0
  84. package/project/.sdk/tm/rust/utility/make_options.rs +1 -1
  85. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sekreto.ts +8 -2
  86. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Sigv4.ts +29 -5
  87. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/index.ts +30 -21
  88. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/Registry.ts +90 -0
  89. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/addr.ts +48 -0
  90. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/aws.ts +219 -0
  91. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/azuresecrets.ts +144 -0
  92. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/boru.ts +113 -0
  93. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/doppler.ts +77 -0
  94. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/dotenv.ts +51 -0
  95. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/env.ts +31 -0
  96. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/file.ts +54 -0
  97. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/gcpsecrets.ts +118 -0
  98. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/hashicorp.ts +159 -0
  99. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/http.ts +66 -0
  100. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/infisical.ts +107 -0
  101. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/memory.ts +33 -0
  102. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/onepassword.ts +120 -0
  103. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/provider/support.ts +176 -0
  104. package/project/.sdk/tm/ts/test/vendor/omni/Runner.ts +15 -13
  105. package/project/.sdk/tm/ts/test/vendor/omni/Util.ts +25 -19
  106. package/project/.sdk/tm/ts/test/vendor/omni/compat.ts +1 -1
  107. package/project/.sdk/tm/ts/test/vendor/omni/index.ts +1 -1
  108. package/project/.sdk/tm/zig/core/utility.zig +1 -1
  109. package/project/sdkgen-package.json +1 -1
  110. package/src/cmp/Feature.ts +5 -0
  111. package/src/cmp/TestControl.ts +64 -0
  112. package/src/helpers/featureSource.ts +130 -0
  113. package/src/sdkgen.ts +7 -0
  114. package/src/utility.ts +61 -0
  115. package/project/.sdk/tm/ts/src/feature/secrets/sekreto/Providers.ts +0 -1221
@@ -1,5 +1,5 @@
1
1
  // VENDORED: @voxgig/omni 0.1.2 (typescript/src/Runner.ts)
2
- // Source: https://github.com/voxgig/omni @ bc9535d655564c0833f6eff003b0b13dad8b350f
2
+ // Source: https://github.com/voxgig/omni @ 5956cc4e5ecdaeebd11eab8bb4b9462dfc76e018
3
3
  // License: MIT (c) voxgig - see repository LICENSE. Do not edit: resync from upstream.
4
4
  // Omni: the shared multi-language test runner.
5
5
  //
@@ -352,20 +352,22 @@ function fixjsonval(val: Json, donull: boolean): Json {
352
352
  }
353
353
 
354
354
  // The JSON form of an error: always at least {name,message}.
355
- // PATCH (solardemo prototype, pending upstream fix), here and in
356
- // errmessage below: handle error-shaped MAPS. The shared corpus throws
357
- // plain objects in some ports (this SDK's makeError rethrows the
358
- // fixture's ctrl.err verbatim), and the struct repos' original runner
359
- // matched err.message regardless of the thrown value's class. Upstream
360
- // collapses any non-Error to String(err) '[object Object]' — which
361
- // fails both the err check and every match.err.* leaf.
355
+ //
356
+ // A thrown value need not be an Error. Ports commonly rethrow an
357
+ // error-SHAPED map ({name, message, ...}) - voxgig/sdkgen's generated
358
+ // makeError rethrows the fixture's own error object verbatim - and
359
+ // collapsing that to String(err) yields '[object Object]', which fails both
360
+ // the `err` check and every `match.err.*` leaf. The struct repository's
361
+ // original runner read `.message` regardless of the thrown value's class.
362
362
  function errify(err: any): Json {
363
363
  if (err instanceof Error) {
364
364
  return { ...err, name: err.name, message: err.message }
365
365
  }
366
+
366
367
  if (null != err && 'object' === typeof err) {
367
368
  return { name: 'Error', ...err }
368
369
  }
370
+
369
371
  return { name: 'Error', message: String(err) }
370
372
  }
371
373
 
@@ -481,11 +483,11 @@ function handleerror(flags: Flags, index: number, entry: Json, err: any) {
481
483
 
482
484
  // Check that every leaf of `check` is present, and matches, in `base`.
483
485
  function match(flags: Flags, index: number, entry: Json, check: Json, base: Json) {
484
- // PATCH (solardemo prototype, pending upstream fix): read the base
485
- // directly instead of cloning it. The clone gained nothing (the walk
486
- // below only READS via getpath) and blew the stack on cyclic bases —
487
- // which every corpus ctx entry produces, because a live Context reaches
488
- // its client and the client's root context reaches the client again.
486
+ // Read the base DIRECTLY. The clone bought nothing - the walk below only
487
+ // reads, via getpath - and it blows the stack on a cyclic base. A port
488
+ // that drives entries with live objects rather than pure JSON produces
489
+ // those routinely: voxgig/sdkgen's corpus matches against a live client
490
+ // context whose root context reaches the client again.
489
491
  const cbase = base
490
492
 
491
493
  const at = (path: Json[]) => (0 === path.length ? '<root>' : pathify(path))
@@ -1,5 +1,5 @@
1
1
  // VENDORED: @voxgig/omni 0.1.2 (typescript/src/Util.ts)
2
- // Source: https://github.com/voxgig/omni @ bc9535d655564c0833f6eff003b0b13dad8b350f
2
+ // Source: https://github.com/voxgig/omni @ 5956cc4e5ecdaeebd11eab8bb4b9462dfc76e018
3
3
  // License: MIT (c) voxgig - see repository LICENSE. Do not edit: resync from upstream.
4
4
  // Omni internal JSON utilities.
5
5
  //
@@ -158,12 +158,17 @@ function deepequal(a: Json, b: Json): boolean {
158
158
 
159
159
  // Compact JSON text with map keys sorted, so that messages are identical
160
160
  // in every port regardless of local map ordering.
161
- // PATCH (solardemo prototype, pending upstream fix): cycle guard. The
162
- // corpus drives LIVE Context objects through entries (entry.ctx, and the
163
- // entry bookkeeping fail() prints with jsonstr), and a live ctx is cyclic
164
- // (sdk._rootctx.client === sdk). Upstream jsonstr recursed forever; the
165
- // struct repos' original runner emitted '[Circular]', restored here.
166
- function jsonstr(val: Json, seen?: WeakSet<object>): string {
161
+ //
162
+ // CYCLE SAFE. Building a FAILURE MESSAGE must never be the thing that
163
+ // crashes: a port driving entries with live objects rather than pure JSON
164
+ // can carry a cyclic value in the entry bookkeeping fail() prints, and this
165
+ // recursed until the stack gave out. A cycle renders as "[Circular]", as
166
+ // the struct repository's original runner did.
167
+ //
168
+ // `seen` tracks the ANCESTORS of the current value, not every value
169
+ // visited: it is removed again on the way out, so the same object appearing
170
+ // twice as siblings - a DAG, not a cycle - still renders in full.
171
+ function jsonstr(val: Json, seen?: Set<any>): string {
167
172
  if (undefined === val) {
168
173
  return 'undefined'
169
174
  }
@@ -184,23 +189,24 @@ function jsonstr(val: Json, seen?: WeakSet<object>): string {
184
189
  return val ? 'true' : 'false'
185
190
  }
186
191
 
187
- if (islist(val)) {
188
- seen = seen || new WeakSet()
189
- if (seen.has(val)) {
190
- return '"[Circular]"'
191
- }
192
- seen.add(val)
193
- return '[' + val.map((entry: Json) => jsonstr(entry, seen)).join(',') + ']'
194
- }
192
+ if (islist(val) || ismap(val)) {
193
+ seen = seen || new Set()
195
194
 
196
- if (ismap(val)) {
197
- seen = seen || new WeakSet()
198
195
  if (seen.has(val)) {
199
196
  return '"[Circular]"'
200
197
  }
198
+
201
199
  seen.add(val)
202
- const keys = Object.keys(val).sort()
203
- return '{' + keys.map((key) => JSON.stringify(key) + ':' + jsonstr(val[key], seen)).join(',') + '}'
200
+
201
+ const out = islist(val)
202
+ ? '[' + val.map((entry: Json) => jsonstr(entry, seen)).join(',') + ']'
203
+ : '{' + Object.keys(val).sort()
204
+ .map((key) => JSON.stringify(key) + ':' + jsonstr((val as any)[key], seen))
205
+ .join(',') + '}'
206
+
207
+ seen.delete(val)
208
+
209
+ return out
204
210
  }
205
211
 
206
212
  if ('function' === typeof val) {
@@ -1,5 +1,5 @@
1
1
  // VENDORED: @voxgig/omni 0.1.2 (typescript/compat/struct.ts), import path adapted ('../src' -> './index').
2
- // Source: https://github.com/voxgig/omni @ bc9535d655564c0833f6eff003b0b13dad8b350f
2
+ // Source: https://github.com/voxgig/omni @ 5956cc4e5ecdaeebd11eab8bb4b9462dfc76e018
3
3
  // License: MIT (c) voxgig - see repository LICENSE. Do not edit: resync from upstream.
4
4
  // Drop-in replacement for the in-situ test runner in the voxgig/struct
5
5
  // repository (`typescript/test/runner.ts`).
@@ -1,5 +1,5 @@
1
1
  // VENDORED: @voxgig/omni 0.1.2 (typescript/src/index.ts)
2
- // Source: https://github.com/voxgig/omni @ bc9535d655564c0833f6eff003b0b13dad8b350f
2
+ // Source: https://github.com/voxgig/omni @ 5956cc4e5ecdaeebd11eab8bb4b9462dfc76e018
3
3
  // License: MIT (c) voxgig - see repository LICENSE. Do not edit: resync from upstream.
4
4
  // @voxgig/omni - shared multi-language test runner.
5
5
 
@@ -372,7 +372,7 @@ pub fn make_options_util(ctx: *Context) Value {
372
372
  .{ "base", h.vstr("http://localhost:8000") },
373
373
  .{ "prefix", h.vstr("") },
374
374
  .{ "suffix", h.vstr("") },
375
- .{ "auth", h.jo(&.{.{ "prefix", h.vstr("") }}) },
375
+ .{ "auth", h.jo(&.{ .{ "prefix", h.vstr("") }, .{ "basic", h.vbool(false) } }) },
376
376
  .{ "headers", h.jo(&.{.{ "`$CHILD`", h.vstr("`$STRING`") }}) },
377
377
  .{ "allow", h.jo(&.{
378
378
  .{ "method", h.vstr("GET,PUT,POST,PATCH,DELETE,OPTIONS") },
@@ -3,7 +3,7 @@
3
3
  "package": 1
4
4
  },
5
5
  "name": "@voxgig/sdkgen",
6
- "version": "4.6.0",
6
+ "version": "4.7.0",
7
7
  "provides": {
8
8
  "target": [
9
9
  "c",
@@ -3,6 +3,7 @@ import { cmp, Copy, Folder } from 'jostraca'
3
3
 
4
4
  import { ensureStdrep } from '../helpers/stdrep'
5
5
  import { featureApplies, featureTags } from '../helpers/applicability'
6
+ import { pluginExcludesFor } from '../helpers/featureSource'
6
7
 
7
8
  const Feature = cmp(function Feature(props: any) {
8
9
  const { target, feature, ctx$ } = props
@@ -31,6 +32,10 @@ const Feature = cmp(function Feature(props: any) {
31
32
  // TODO: Copy should just warn if from not found
32
33
  Copy({
33
34
  from: 'tm/' + target.name + '/src/feature/' + feature.name,
35
+ // An ACTIVE feature's INACTIVE plugins do not come with it. This
36
+ // is the copy that decides it: Main's exclude never sees these
37
+ // files, because this Copy has already written them.
38
+ exclude: pluginExcludesFor(ctx$.model, feature.name),
34
39
  replace: {
35
40
  // Feature templates reference the SDK class by placeholder — e.g.
36
41
  // tm/ts/.../TestFeature.ts imports `ProjectNameSDK`. Without the
@@ -0,0 +1,64 @@
1
+ import Path from 'node:path'
2
+
3
+ import {
4
+ cmp,
5
+ File,
6
+ Fragment,
7
+ } from 'jostraca'
8
+
9
+
10
+ const TEST_CONTROL_FILE = 'sdk-test-control.json'
11
+
12
+
13
+ // Keep the blanket per-target `Copy({ from: 'tm/<lang>' })` from restoring the
14
+ // template default over a project's edited control file. One regex for every
15
+ // target: the file sits under `test/`, `t/`, `tests/` or `sdktest/` depending
16
+ // on the language, and the name is what identifies it either way.
17
+ const TEST_CONTROL_EXCLUDE = new RegExp('(^|/)' + TEST_CONTROL_FILE + '$')
18
+
19
+
20
+ // `sdk-test-control.json` is the ONE generated file a project is meant to edit
21
+ // by hand: it names the tests to skip, the live pacing, and the extra client
22
+ // options a live run needs (`test.client.options`).
23
+ //
24
+ // It shipped as part of each target's blanket `Copy({ from: 'tm/<lang>' })`,
25
+ // and a Copy has no per-file "leave it alone if it already exists" — so every
26
+ // `npm run generate` overwrote whatever the project had put there. The
27
+ // documented workaround, editing the template master under `.sdk/tm/`, is
28
+ // worse: `doctor` reports it as drift and the next `target add <lang>` reverts
29
+ // it.
30
+ //
31
+ // So it is emitted HERE instead, write-once: `File({ exclude: true })` returns
32
+ // early when the file already exists (FileOp), which is exactly the semantics
33
+ // the file is documented to have. A project without one yet gets the
34
+ // template's default; a project that has edited one keeps its edits across
35
+ // both `generate` and `target add`.
36
+ //
37
+ // The content still comes from the target's own template, so there remains
38
+ // exactly one copy of the default on disk.
39
+ //
40
+ // The template is addressed the SAME way the Copy this replaces addressed it:
41
+ // `tm/<lang>/...` relative to the working directory, which is the project's
42
+ // `.sdk`. Resolved to absolute here because Fragment resolves a RELATIVE
43
+ // `from` against the OUTPUT folder instead — a different base, and the one
44
+ // place these two could silently disagree.
45
+ const TestControl = cmp(function TestControl(props: {
46
+ target: { name: string }
47
+ dir: string
48
+ ctx$?: any
49
+ }) {
50
+ const { target, dir } = props
51
+
52
+ File({ name: TEST_CONTROL_FILE, exclude: true }, () => {
53
+ Fragment({
54
+ from: Path.resolve('tm', target.name, dir, TEST_CONTROL_FILE)
55
+ })
56
+ })
57
+ })
58
+
59
+
60
+ export {
61
+ TestControl,
62
+ TEST_CONTROL_FILE,
63
+ TEST_CONTROL_EXCLUDE,
64
+ }
@@ -260,6 +260,134 @@ function srcFeatureExcludes(model: any): RegExp[] {
260
260
  }
261
261
 
262
262
 
263
+ // The same rule as srcFeatureExcludes, ONE LEVEL DEEPER: a feature's
264
+ // declared-but-inactive PLUGINS.
265
+ //
266
+ // A plugin is a feature's optional part — the `secrets` feature over
267
+ // sekreto has one per provider kind, and each carries its own platform
268
+ // cost (node:crypto for AWS request signing, fetch for seven HTTP vault
269
+ // clients). A project whose chain is `[dotenv, env]` should carry neither,
270
+ // and before the plugin trim it carried both, because every kind was
271
+ // reachable from a single import.
272
+ //
273
+ // Layout is `src/feature/<feature>/plugin/<plugin>/`, so a plugin's tree
274
+ // is excluded exactly as a feature's is. Only declared-but-inactive
275
+ // plugins are excluded: a directory the model never mentions is left
276
+ // alone, matching srcFeatureExcludes' treatment of `base`.
277
+ //
278
+ // Note this trims a plugin belonging to an ACTIVE feature. An inactive
279
+ // feature's whole tree — plugins included — is already gone via
280
+ // srcFeatureExcludes, so walking its plugins here would be redundant, and
281
+ // asking the model for them would fail on the `only_active` filter anyway.
282
+ // One feature's inactive-plugin patterns.
283
+ //
284
+ // Separate from pluginExcludes(model) because the two COPIES that need it
285
+ // see different things: Main copies the whole target tree and has the
286
+ // model, while the Feature component copies one feature's tree and has
287
+ // only that feature. Feature's copy is the one that actually matters —
288
+ // it runs for every ACTIVE feature, so without this an active feature's
289
+ // plugins were all copied regardless, and Main's exclude never saw them.
290
+ function pluginExcludesFor(model: any, fname: string): RegExp[] {
291
+ if (null == model || null == fname) {
292
+ return []
293
+ }
294
+
295
+ // `only_active: false`, and this is the whole subtlety. The feature
296
+ // object a component is handed has ALREADY been filtered, so its
297
+ // `plugin` map contains only the ACTIVE plugins — and a function looking
298
+ // there for something inactive finds nothing and excludes nothing. That
299
+ // is exactly the trap srcFeatureExcludes documents, hit again one level
300
+ // down: the first cut of this read the filtered feature and silently
301
+ // trimmed nothing while every test passed.
302
+ const plugins = getModelPath(model,
303
+ `main.${KIT}.feature.${fname}.plugin`,
304
+ { required: false, only_active: false }) || {}
305
+
306
+ const out: RegExp[] = []
307
+ const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
308
+
309
+ for (const pname of Object.keys(plugins).sort()) {
310
+ const plugin = plugins[pname]
311
+ if (false !== plugin?.active) continue
312
+
313
+ const declared = plugin.path || []
314
+
315
+ if (0 < declared.length) {
316
+ for (const one of declared) {
317
+ // FEATURE-RELATIVE, because jostraca tests a Copy's candidates
318
+ // against the path RELATIVE TO THAT COPY'S ROOT — `sekreto/
319
+ // provider/aws.ts`, not `src/feature/secrets/sekreto/provider/
320
+ // aws.ts`. The Feature component's Copy is rooted at
321
+ // `tm/<target>/src/feature/<feature>`, so a full declared path
322
+ // never matches and the trim silently does nothing.
323
+ //
324
+ // Worth stating because it is invisible: an exclude that matches
325
+ // nothing is indistinguishable from no exclude at all, and both
326
+ // the patterns and the model were correct while nothing was
327
+ // trimmed. Main's copy IS rooted at the target root, which is why
328
+ // pluginExcludes below keeps the full path.
329
+ const rel = String(one).replace(
330
+ new RegExp('^src/feature/' + esc(fname) + '/'), '')
331
+ const pat = esc(rel)
332
+ out.push(new RegExp('(^|/)' + pat.replace(/\\\/$/, '') +
333
+ (/\/$/.test(rel) ? '/' : '$')))
334
+ }
335
+ continue
336
+ }
337
+
338
+ out.push(new RegExp('(^|/)plugin/' + esc(pname) + '/'))
339
+ }
340
+
341
+ return out
342
+ }
343
+
344
+
345
+ function pluginExcludes(model: any): RegExp[] {
346
+ const active = getModelPath(model, `main.${KIT}.feature`,
347
+ { required: false }) || {}
348
+
349
+ const out: RegExp[] = []
350
+ const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
351
+
352
+ for (const fname of Object.keys(active)) {
353
+ const all = getModelPath(model,
354
+ `main.${KIT}.feature.${fname}.plugin`,
355
+ { required: false, only_active: false }) || {}
356
+ const on = getModelPath(model,
357
+ `main.${KIT}.feature.${fname}.plugin`,
358
+ { required: false }) || {}
359
+
360
+ for (const pname of Object.keys(all)) {
361
+ if (null != on[pname]) continue
362
+
363
+ // DECLARED paths first. A plugin's files are often not free to
364
+ // move — sekreto's provider modules are vendored, and both the
365
+ // vendoring guard and their own relative imports pin them at
366
+ // upstream's directory depth — so a plugin says which paths it
367
+ // owns rather than being assumed to own a directory.
368
+ const declared = all[pname].path || []
369
+
370
+ if (0 < declared.length) {
371
+ for (const one of declared) {
372
+ const pat = esc(String(one))
373
+ // A trailing slash means a folder and everything under it;
374
+ // anything else matches that path exactly, as featureExcludes
375
+ // does for a file source.
376
+ out.push(new RegExp('(^|/)' + pat.replace(/\\\/$/, '') +
377
+ (/\/$/.test(String(one)) ? '/' : '$')))
378
+ }
379
+ continue
380
+ }
381
+
382
+ out.push(new RegExp(
383
+ '(^|/)src/feature/' + esc(fname) + '/plugin/' + esc(pname) + '/'))
384
+ }
385
+ }
386
+
387
+ return out
388
+ }
389
+
390
+
263
391
  export type {
264
392
  FeatureSource,
265
393
  FeatureEntry,
@@ -275,4 +403,6 @@ export {
275
403
  featureExcludes,
276
404
  fullsetExcludes,
277
405
  srcFeatureExcludes,
406
+ pluginExcludes,
407
+ pluginExcludesFor,
278
408
  }
package/src/sdkgen.ts CHANGED
@@ -40,6 +40,7 @@ import { License } from './cmp/License'
40
40
  import { Security } from './cmp/Security'
41
41
  import { Changelog } from './cmp/Changelog'
42
42
  import { Test } from './cmp/Test'
43
+ import { TestControl, TEST_CONTROL_EXCLUDE } from './cmp/TestControl'
43
44
  import { ReadmeInstall } from './cmp/ReadmeInstall'
44
45
  import { ReadmeQuick } from './cmp/ReadmeQuick'
45
46
  import { ReadmeErrors } from './cmp/ReadmeErrors'
@@ -73,6 +74,8 @@ import {
73
74
  featureExcludes,
74
75
  fullsetExcludes,
75
76
  srcFeatureExcludes,
77
+ pluginExcludes,
78
+ pluginExcludesFor,
76
79
  } from './helpers/featureSource'
77
80
  import type { FeatureSource } from './helpers/featureSource'
78
81
  import { stationLibrary } from './helpers/station'
@@ -1072,6 +1075,8 @@ export {
1072
1075
  Entity,
1073
1076
  Feature,
1074
1077
  Test,
1078
+ TestControl,
1079
+ TEST_CONTROL_EXCLUDE,
1075
1080
  Readme,
1076
1081
  ReadmeTop,
1077
1082
  AgentGuideTop,
@@ -1167,6 +1172,8 @@ export {
1167
1172
  featureExcludes,
1168
1173
  fullsetExcludes,
1169
1174
  srcFeatureExcludes,
1175
+ pluginExcludes,
1176
+ pluginExcludesFor,
1170
1177
  stationLibrary,
1171
1178
 
1172
1179
  featureApplies,
package/src/utility.ts CHANGED
@@ -82,6 +82,34 @@ function isHttpBasicAuth(model: any): boolean {
82
82
  }
83
83
 
84
84
 
85
+ // The API's ACCESS-TOKEN EXCHANGE, as apidef recorded it from the spec
86
+ // (main.kit.info.security.exchange): where the token endpoint lives, and the
87
+ // field names it sends and answers with. Null when the spec describes none,
88
+ // which is the common case.
89
+ //
90
+ // These are FACTS ABOUT THE API, so they belong in the generated config
91
+ // rather than in a project's hand-written model: an SDK whose spec says the
92
+ // exchange is at `auth/token` should not need to be told so again. A feature
93
+ // opts in to receiving them with `spec: { authexchange: '<options-key>' }`;
94
+ // configDefinition does the overlay.
95
+ //
96
+ // Deliberately NOT included: `active`. Whether a client performs the
97
+ // exchange is the project's decision (some resolve a static credential
98
+ // through the same feature on an API that also has a token endpoint), so
99
+ // apidef records only what the exchange IS, never that it should run.
100
+ function resolveAuthExchange(model: any): Record<string, any> | null {
101
+ const security = getModelPath(model, `main.${KIT}.info.security`,
102
+ { only_active: false, required: false })
103
+
104
+ const exchange = security?.exchange
105
+ if (null == exchange || 'object' !== typeof exchange) {
106
+ return null
107
+ }
108
+
109
+ return exchange
110
+ }
111
+
112
+
85
113
  function requirePath(ctx$: any, path: string, flags?: { ignore?: boolean }): any {
86
114
  const fullpath = resolvePath(ctx$, path)
87
115
  const ignore = null == flags?.ignore ? false : flags.ignore
@@ -118,6 +146,7 @@ export {
118
146
  requirePath,
119
147
  isAuthActive,
120
148
  resolveAuthPrefix,
149
+ resolveAuthExchange,
121
150
  isHttpBasicAuth,
122
151
  SdkGenError,
123
152
  CONFIG_DATA_THRESHOLD,
@@ -301,6 +330,15 @@ function rawStringLiteral(s: string): string {
301
330
  }
302
331
 
303
332
 
333
+ // The closed vocabulary of spec-derived facts a feature may ask for by
334
+ // declaring `spec: { <fact>: <options-key> }`. Closed, and resolved through
335
+ // one function each, so a feature cannot reach arbitrarily into the model
336
+ // and a fact's shape is defined in exactly one place.
337
+ const SPEC_FACTS: Record<string, (model: any) => any> = {
338
+ authexchange: resolveAuthExchange,
339
+ }
340
+
341
+
304
342
  // THE CANONICAL CONFIG OBJECT, and the JSON the threshold is measured on.
305
343
  //
306
344
  // Every target builds its config from this one function, so the literal a
@@ -361,6 +399,29 @@ function configDefinition(model: any, targetname?: string): { def: any, json: st
361
399
  if (null != f.transport && '' !== f.transport) {
362
400
  fdef.transport = String(f.transport)
363
401
  }
402
+
403
+ // SPEC-DERIVED OPTIONS. A feature declares `spec: { <fact>: <options
404
+ // key> }` to receive facts apidef recorded from the OpenAPI spec — the
405
+ // same declare-never-infer rule `needs` and `transport` follow, and for
406
+ // the same reason: the alternative is this function knowing feature
407
+ // names, which is exactly the coupling the generic feature loop exists
408
+ // to avoid.
409
+ //
410
+ // The fact overlays the feature's DECLARED DEFAULTS, and rightly: a
411
+ // default like `path: 'auth/token'` is a generic guess, while the spec
412
+ // states where this API's endpoint actually is. A project that needs
413
+ // something else still overrides at runtime through `options.feature`,
414
+ // which beats the embedded config either way.
415
+ for (const factname of Object.keys(f.spec || {}).sort()) {
416
+ const optkey = f.spec[factname]
417
+ const fact = SPEC_FACTS[factname]?.(model)
418
+ if (null == fact || null == optkey || '' === optkey) {
419
+ continue
420
+ }
421
+ fdef.options = { ...(fdef.options || {}) }
422
+ fdef.options[optkey] = { ...(fdef.options[optkey] || {}), ...fact }
423
+ }
424
+
364
425
  featureDefs[f.name] = fdef
365
426
  })
366
427