@voxgig/sdkgen 3.4.6 → 3.5.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 (198) hide show
  1. package/bin/voxgig-sdkgen +78 -10
  2. package/dist/action/action.d.ts +5 -2
  3. package/dist/action/action.js +113 -6
  4. package/dist/action/action.js.map +1 -1
  5. package/dist/action/check.d.ts +15 -0
  6. package/dist/action/check.js +347 -0
  7. package/dist/action/check.js.map +1 -0
  8. package/dist/action/dispatch.d.ts +8 -0
  9. package/dist/action/dispatch.js +74 -0
  10. package/dist/action/dispatch.js.map +1 -0
  11. package/dist/action/docs.d.ts +4 -0
  12. package/dist/action/docs.js +236 -0
  13. package/dist/action/docs.js.map +1 -0
  14. package/dist/action/doctor.d.ts +5 -2
  15. package/dist/action/doctor.js +526 -74
  16. package/dist/action/doctor.js.map +1 -1
  17. package/dist/action/feature.js +96 -17
  18. package/dist/action/feature.js.map +1 -1
  19. package/dist/action/kind.d.ts +30 -0
  20. package/dist/action/kind.js +258 -0
  21. package/dist/action/kind.js.map +1 -0
  22. package/dist/action/package.d.ts +25 -0
  23. package/dist/action/package.js +852 -0
  24. package/dist/action/package.js.map +1 -0
  25. package/dist/action/resolve.d.ts +18 -0
  26. package/dist/action/resolve.js +280 -0
  27. package/dist/action/resolve.js.map +1 -0
  28. package/dist/action/target.d.ts +7 -1
  29. package/dist/action/target.js +240 -97
  30. package/dist/action/target.js.map +1 -1
  31. package/dist/cmp/Docs.d.ts +4 -0
  32. package/dist/cmp/Docs.js +98 -0
  33. package/dist/cmp/Docs.js.map +1 -0
  34. package/dist/cmp/ExternalDocs.d.ts +2 -0
  35. package/dist/cmp/ExternalDocs.js +43 -0
  36. package/dist/cmp/ExternalDocs.js.map +1 -0
  37. package/dist/cmp/ReadmeTop.js +5 -1
  38. package/dist/cmp/ReadmeTop.js.map +1 -1
  39. package/dist/helpers/definition.d.ts +5 -0
  40. package/dist/helpers/definition.js +72 -0
  41. package/dist/helpers/definition.js.map +1 -0
  42. package/dist/helpers/featureSource.d.ts +8 -2
  43. package/dist/helpers/featureSource.js +61 -12
  44. package/dist/helpers/featureSource.js.map +1 -1
  45. package/dist/helpers/junk.d.ts +11 -0
  46. package/dist/helpers/junk.js +149 -0
  47. package/dist/helpers/junk.js.map +1 -0
  48. package/dist/helpers/manifest.d.ts +50 -0
  49. package/dist/helpers/manifest.js +328 -0
  50. package/dist/helpers/manifest.js.map +1 -0
  51. package/dist/helpers/modelcheck.d.ts +22 -0
  52. package/dist/helpers/modelcheck.js +203 -0
  53. package/dist/helpers/modelcheck.js.map +1 -0
  54. package/dist/helpers/semver.d.ts +4 -0
  55. package/dist/helpers/semver.js +174 -0
  56. package/dist/helpers/semver.js.map +1 -0
  57. package/dist/helpers/shipped.d.ts +4 -0
  58. package/dist/helpers/shipped.js +36 -0
  59. package/dist/helpers/shipped.js.map +1 -0
  60. package/dist/helpers/stdrep.d.ts +9 -1
  61. package/dist/helpers/stdrep.js +22 -0
  62. package/dist/helpers/stdrep.js.map +1 -1
  63. package/dist/sdkgen.d.ts +15 -3
  64. package/dist/sdkgen.js +233 -67
  65. package/dist/sdkgen.js.map +1 -1
  66. package/dist/tsconfig.tsbuildinfo +1 -1
  67. package/dist/types.d.ts +10 -2
  68. package/dist/utility.js +17 -0
  69. package/dist/utility.js.map +1 -1
  70. package/model/sdkgen.aontu +69 -0
  71. package/package.json +3 -2
  72. package/project/.sdk/model/feature/audit.aontu +1 -0
  73. package/project/.sdk/model/feature/cache.aontu +1 -0
  74. package/project/.sdk/model/feature/clienttrack.aontu +1 -0
  75. package/project/.sdk/model/feature/debug.aontu +1 -0
  76. package/project/.sdk/model/feature/idempotency.aontu +1 -0
  77. package/project/.sdk/model/feature/log.aontu +1 -0
  78. package/project/.sdk/model/feature/metrics.aontu +1 -0
  79. package/project/.sdk/model/feature/netsim.aontu +1 -0
  80. package/project/.sdk/model/feature/paging.aontu +1 -0
  81. package/project/.sdk/model/feature/proxy.aontu +1 -0
  82. package/project/.sdk/model/feature/ratelimit.aontu +1 -0
  83. package/project/.sdk/model/feature/rbac.aontu +1 -0
  84. package/project/.sdk/model/feature/retry.aontu +1 -0
  85. package/project/.sdk/model/feature/streaming.aontu +1 -0
  86. package/project/.sdk/model/feature/telemetry.aontu +1 -0
  87. package/project/.sdk/model/feature/test.aontu +1 -0
  88. package/project/.sdk/model/feature/timeout.aontu +1 -0
  89. package/project/.sdk/model/target/c.aontu +1 -0
  90. package/project/.sdk/model/target/clojure.aontu +1 -0
  91. package/project/.sdk/model/target/cpp.aontu +1 -0
  92. package/project/.sdk/model/target/dart.aontu +1 -0
  93. package/project/.sdk/model/target/elixir.aontu +1 -0
  94. package/project/.sdk/model/target/go-cli.aontu +1 -0
  95. package/project/.sdk/model/target/go-mcp.aontu +1 -0
  96. package/project/.sdk/model/target/go.aontu +1 -0
  97. package/project/.sdk/model/target/haskell.aontu +1 -0
  98. package/project/.sdk/model/target/java.aontu +1 -0
  99. package/project/.sdk/model/target/js.aontu +1 -0
  100. package/project/.sdk/model/target/kotlin.aontu +1 -0
  101. package/project/.sdk/model/target/lean.aontu +1 -0
  102. package/project/.sdk/model/target/lua.aontu +1 -0
  103. package/project/.sdk/model/target/ocaml.aontu +1 -0
  104. package/project/.sdk/model/target/perl.aontu +1 -0
  105. package/project/.sdk/model/target/php.aontu +1 -0
  106. package/project/.sdk/model/target/py-data.aontu +1 -0
  107. package/project/.sdk/model/target/py.aontu +1 -0
  108. package/project/.sdk/model/target/rb.aontu +1 -0
  109. package/project/.sdk/model/target/rust.aontu +1 -0
  110. package/project/.sdk/model/target/scala.aontu +1 -0
  111. package/project/.sdk/model/target/seneca-provider.aontu +1 -0
  112. package/project/.sdk/model/target/zig.aontu +1 -0
  113. package/project/.sdk/src/cmp/c/Config_c.ts +44 -0
  114. package/project/.sdk/src/cmp/c/fragment/Main.fragment.c +3 -1
  115. package/project/.sdk/src/cmp/clojure/Config_clojure.ts +37 -42
  116. package/project/.sdk/src/cmp/clojure/utility_clojure.ts +42 -0
  117. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +18 -0
  118. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +125 -52
  119. package/project/.sdk/src/cmp/csharp/fragment/Main.fragment.cs +3 -1
  120. package/project/.sdk/src/cmp/csharp/utility_csharp.ts +94 -6
  121. package/project/.sdk/src/cmp/dart/Config_dart.ts +56 -3
  122. package/project/.sdk/src/cmp/dart/fragment/Config.data.fragment.dart +74 -0
  123. package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +6 -9
  124. package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +16 -5
  125. package/project/.sdk/src/cmp/dart/utility_dart.ts +18 -0
  126. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +128 -13
  127. package/project/.sdk/src/cmp/elixir/fragment/Main.fragment.ex +3 -1
  128. package/project/.sdk/src/cmp/elixir/utility_elixir.ts +6 -0
  129. package/project/.sdk/src/cmp/haskell/Config_haskell.ts +35 -52
  130. package/project/.sdk/src/cmp/haskell/Package_haskell.ts +1 -0
  131. package/project/.sdk/src/cmp/haskell/Test_haskell.ts +1 -1
  132. package/project/.sdk/src/cmp/haskell/utility_haskell.ts +15 -5
  133. package/project/.sdk/src/cmp/java/Config_java.ts +21 -0
  134. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +15 -0
  135. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +42 -39
  136. package/project/.sdk/src/cmp/perl/Config_perl.ts +15 -0
  137. package/project/.sdk/src/cmp/perl/fragment/Main.fragment.pm +3 -1
  138. package/project/.sdk/src/cmp/rust/Config_rust.ts +44 -0
  139. package/project/.sdk/src/cmp/rust/fragment/Main.fragment.rs +4 -2
  140. package/project/.sdk/src/cmp/scala/Config_scala.ts +15 -0
  141. package/project/.sdk/src/cmp/swift/Config_swift.ts +17 -0
  142. package/project/.sdk/src/cmp/swift/fragment/Main.fragment.swift +3 -1
  143. package/project/.sdk/src/cmp/zig/Config_zig.ts +44 -0
  144. package/project/.sdk/src/cmp/zig/TestEntity_zig.ts +9 -2
  145. package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +31 -18
  146. package/project/.sdk/tm/c/core/sdk.h +1 -0
  147. package/project/.sdk/tm/c/utility/make_options.c +5 -1
  148. package/project/.sdk/tm/clojure/src/sdk/core.clj +51 -3
  149. package/project/.sdk/tm/cpp/core/types.hpp +4 -1
  150. package/project/.sdk/tm/cpp/utility/pipeline.hpp +5 -1
  151. package/project/.sdk/tm/csharp/test/PipelineTest.cs +15 -0
  152. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +67 -1
  153. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +43 -0
  154. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +58 -2
  155. package/project/.sdk/tm/haskell/{test/TestJson.hs → src/SdkJson.hs} +8 -4
  156. package/project/.sdk/tm/haskell/src/SdkRuntime.hs +55 -2
  157. package/project/.sdk/tm/haskell/test/Runner.hs +1 -1
  158. package/project/.sdk/tm/haskell/test/StructCorpus.hs +1 -1
  159. package/project/.sdk/tm/java/core/SdkClient.java +3 -1
  160. package/project/.sdk/tm/java/utility/MakeOptions.java +5 -1
  161. package/project/.sdk/tm/kotlin/core/SdkClient.kt +3 -1
  162. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +5 -1
  163. package/project/.sdk/tm/ocaml/Makefile +3 -2
  164. package/project/.sdk/tm/ocaml/sdk_json.ml +106 -0
  165. package/project/.sdk/tm/ocaml/sdk_runtime.ml +56 -0
  166. package/project/.sdk/tm/ocaml/test/struct_corpus.ml +4 -86
  167. package/project/.sdk/tm/perl/utility/make_options.pm +5 -1
  168. package/project/.sdk/tm/rust/utility/make_options.rs +79 -1
  169. package/project/.sdk/tm/scala/core/SdkClient.scala +3 -1
  170. package/project/.sdk/tm/scala/utility/Make.scala +5 -1
  171. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +5 -1
  172. package/project/.sdk/tm/zig/core/utility.zig +85 -1
  173. package/project/.sdk/tm/zig/test/gotcha_test.zig +8 -8
  174. package/project/sdkgen-package.json +57 -0
  175. package/src/action/action.ts +136 -6
  176. package/src/action/check.ts +495 -0
  177. package/src/action/dispatch.ts +105 -0
  178. package/src/action/docs.ts +310 -0
  179. package/src/action/doctor.ts +692 -81
  180. package/src/action/feature.ts +109 -17
  181. package/src/action/kind.ts +375 -0
  182. package/src/action/package.ts +1173 -0
  183. package/src/action/resolve.ts +362 -0
  184. package/src/action/target.ts +290 -108
  185. package/src/cmp/Docs.ts +125 -0
  186. package/src/cmp/ExternalDocs.ts +54 -0
  187. package/src/cmp/ReadmeTop.ts +6 -1
  188. package/src/helpers/definition.ts +82 -0
  189. package/src/helpers/featureSource.ts +83 -13
  190. package/src/helpers/junk.ts +168 -0
  191. package/src/helpers/manifest.ts +474 -0
  192. package/src/helpers/modelcheck.ts +262 -0
  193. package/src/helpers/semver.ts +211 -0
  194. package/src/helpers/shipped.ts +41 -0
  195. package/src/helpers/stdrep.ts +70 -0
  196. package/src/sdkgen.ts +294 -66
  197. package/src/types.ts +33 -3
  198. package/src/utility.ts +17 -0
@@ -27,6 +27,12 @@ import { SdkGenError } from '../utility'
27
27
 
28
28
  import { findFeatureSources } from '../helpers/featureSource'
29
29
 
30
+ import { templateReplacements } from '../helpers/stdrep'
31
+
32
+ import { copyOpts } from '../helpers/junk'
33
+
34
+ import { resolveKind, kindModel } from './kind'
35
+
30
36
 
31
37
  import {
32
38
  UpdateIndex,
@@ -41,6 +47,13 @@ const CMD_MAP: any = {
41
47
 
42
48
  const BASE = 'node_modules/@voxgig/sdkgen'
43
49
 
50
+ // The `.sdk` folder a bundled feature comes from — the value recorded as its
51
+ // provenance. Still hardcoded, like the path above: giving `feature add` the
52
+ // ref grammar `target add` already has is the next step, and this becomes
53
+ // whatever the ref resolved to.
54
+ const SDKFOLDER = BASE + '/project/.sdk'
55
+
56
+
44
57
 
45
58
  async function action_feature(args: string[], actx: ActionContext): Promise<ActionResult> {
46
59
 
@@ -89,6 +102,9 @@ async function feature_add(features: string[], actx: ActionContext): Promise<Act
89
102
  control: {
90
103
  dryrun: !!actx.opts.dryrun
91
104
  },
105
+ // Per-call for the same reason, and covering the same accident: see
106
+ // helpers/junk.
107
+ cmp: copyOpts(),
92
108
  }
93
109
 
94
110
  opts.log.info({
@@ -123,28 +139,56 @@ const FeatureRoot = cmp(function FeatureRoot(props: any) {
123
139
  const target = model.main[KIT].target
124
140
 
125
141
  Project({}, () => {
142
+ // The names as INSTALLED, which is what the index must list. A ref is
143
+ // not a name: `feature add @acme/sdkgen-iot/circuitbreaker` installs
144
+ // `circuitbreaker`, and writing the raw ref into feature-index.aontu
145
+ // would produce an include of a file that does not exist.
146
+ const fnames: string[] = []
147
+
126
148
  each(features, (n) => {
127
- const fname = n.val$
149
+ const fref = n.val$
128
150
  // TODO: validate feature is a-z0-9-_. only
129
151
 
152
+ // Resolution, the alias policy, and the bare-name fallback to what the
153
+ // model records are the same for every kind — see action/kind.
154
+ //
155
+ // A name that no longer resolves must not abort the RUN: `target add`
156
+ // re-runs this action for every active feature, so one feature whose
157
+ // source has moved (package uninstalled, checkout relocated) would
158
+ // otherwise stop every other feature — including `test`, which every
159
+ // generated target needs — from being copied at all.
160
+ let source: any
161
+ try {
162
+ source = resolveKind(fref, 'feature', ctx$)
163
+ }
164
+ catch (err: any) {
165
+ if (err instanceof SdkGenError) {
166
+ throw err
167
+ }
168
+
169
+ log.warn({
170
+ point: 'feature-source-unresolved', feature: fref,
171
+ err: err.message,
172
+ note: fref + ': cannot find its source (' + err.message +
173
+ '); skipping, the already-copied files are left alone'
174
+ })
175
+ return
176
+ }
177
+
178
+ const fname = source.name
179
+ fnames.push(fname)
180
+
130
181
  log.info({
131
182
  point: 'feature-build',
132
183
  feature: fname,
133
- note: fname
184
+ note: fname + (fname === fref ? '' : ' ref:' + fref)
134
185
  })
135
186
 
136
187
 
137
- Folder({ name: 'model/feature' }, () => {
138
- Copy({
139
- // TODO: these paths needs to be parameterised
140
- from: BASE + '/project/.sdk/model/feature/' + fname + '.aontu',
141
- exclude: true
142
- })
143
- File({ name: 'feature-index.aontu' }, () => UpdateIndex({
144
- content: ctx$.meta.content.feature_index,
145
- names: features,
146
- }))
147
- })
188
+ Folder({ name: 'model/feature' }, () => kindModel({
189
+ ctx$, kind: 'feature', source, names: fnames,
190
+ content: ctx$.meta.content.feature_index,
191
+ }))
148
192
 
149
193
  // Bring in the feature's source for every target already in the model.
150
194
  // Where that source lives is language-specific — `src/feature/<name>/`
@@ -154,10 +198,51 @@ const FeatureRoot = cmp(function FeatureRoot(props: any) {
154
198
  // `src/feature/<name>` meant `feature add` silently added nothing for
155
199
  // every target that keeps feature source elsewhere.
156
200
  each(target, (t) => {
157
- const sdkfolder = t.base || Path.join(BASE, 'project/.sdk')
158
- const tmfolder = Path.join(sdkfolder, 'tm', t.name)
201
+ // The target's OWN tree, under the name it has in ITS source — an
202
+ // aliased target's templates live at `tm/<origname>`, so searching
203
+ // `tm/<t.name>` missed them entirely.
204
+ const sdkfolder = t.base || SDKFOLDER
205
+ const torigname = t.origname || t.name
206
+ const owntm = Path.join(sdkfolder, 'tm', torigname)
207
+
208
+ // Two places a feature's per-target source can live, in order:
209
+ // the FEATURE package's own overlay for this target, then the
210
+ // target's own tree. A feature shipped by one package for a target
211
+ // shipped by another has nowhere else to put it.
212
+ //
213
+ // First hit wins at DISCOVERY rather than by copy order: jostraca
214
+ // writes last-write-wins, so copying both would silently invert the
215
+ // precedence.
216
+ const featuretm = Path.join(source.folder, 'tm', torigname)
217
+ const overlay = featuretm === owntm ? [] :
218
+ findFeatureSources(fs, featuretm, [fname])
219
+
220
+ const own = 0 < overlay.length ?
221
+ findFeatureSources(fs, owntm, [fname]) : []
222
+
223
+ // Both trees carrying source for one feature means two packages claim
224
+ // the same name. The overlay wins, but the target's own files were
225
+ // already copied by `target add` and Copy never removes, so the
226
+ // project is left holding BOTH — duplicate symbols, or stale feature
227
+ // code that still runs. Detecting the name collision at add time is
228
+ // what actually fixes this (it belongs with manifest validation); say
229
+ // so loudly until then, rather than leaving a silent hybrid.
230
+ if (0 < own.length) {
231
+ log.warn({
232
+ point: 'feature-source-shadowed', feature: fname, target: t.name,
233
+ overlay: featuretm, own: owntm,
234
+ note: fname + ': both ' + featuretm + ' and ' + owntm +
235
+ ' provide source for target ' + t.name +
236
+ '; the overlay is used, but the files already copied from the ' +
237
+ "target's own tree are NOT removed — check tm/" + t.name +
238
+ ' for a mix of the two'
239
+ })
240
+ }
241
+
242
+ const sources = 0 < overlay.length ? overlay :
243
+ findFeatureSources(fs, owntm, [fname])
159
244
 
160
- const sources = findFeatureSources(fs, tmfolder, [fname])
245
+ const tmfolder = 0 < overlay.length ? featuretm : owntm
161
246
 
162
247
  if (0 === sources.length) {
163
248
  log.warn({
@@ -176,7 +261,14 @@ const FeatureRoot = cmp(function FeatureRoot(props: any) {
176
261
  Folder({ name: 'tm/' + t.name + '/' + dest }, () => {
177
262
  Copy({
178
263
  from: Path.join(tmfolder, source.path),
179
- exclude: true
264
+ // The SAME map `target add` writes `tm/<t>` with. Without it
265
+ // this copy laid RAW template text over files the target add
266
+ // had already substituted, so `ProjectName` / `PROJECTVERSION`
267
+ // survived into the project depending only on which action
268
+ // wrote the file last — the writer/writer disagreement
269
+ // helpers/stdrep.ts exists to prevent, in the one place that
270
+ // did not share the map.
271
+ replace: templateReplacements(model, t.name),
180
272
  })
181
273
  })
182
274
  }
@@ -0,0 +1,375 @@
1
+ // KINDS: the things an `add` can install.
2
+ //
3
+ // `target` and `feature` are two of them, `docs` and others are meant to
4
+ // follow (docs/design/sdkgen-packages.md §8). They were two hand-written
5
+ // pipelines that had already drifted apart — only one took path refs, only
6
+ // one applied a replace map, only one recorded provenance — and a third kind
7
+ // would have been a third copy of the same drift.
8
+ //
9
+ // What every kind shares is declared here and executed once:
10
+ //
11
+ // - the ref grammar and resolution, including the fallback to what the
12
+ // model already records for a bare name;
13
+ // - whether the kind may be installed under a different name (`~alias`);
14
+ // - the definition file: `model/<kind>/<name>.aontu`, stamped with
15
+ // provenance and landing under the INSTALLED name;
16
+ // - the include list: `model/<kind>/<kind>-index.aontu`.
17
+ //
18
+ // What a kind adds on top — a target's component and template trees, a
19
+ // feature's per-target source fan-out — stays in that kind's own action. Those
20
+ // are genuinely different work, not the same work with different strings, and
21
+ // pretending otherwise would buy generality nobody can use.
22
+
23
+ import Path from 'node:path'
24
+
25
+ import { File, Copy, Content, template } from 'jostraca'
26
+
27
+ import { KIT } from '../types'
28
+
29
+ import { SdkGenError } from '../utility'
30
+
31
+ import { provenanceReplace } from '../helpers/stdrep'
32
+
33
+ import { resolveSource } from './resolve'
34
+ import type { Source } from './resolve'
35
+
36
+ import { UpdateIndex } from './action'
37
+
38
+
39
+ type KindDef = {
40
+ // The kind's name, which is also its model subdirectory and the prefix of
41
+ // its index file.
42
+ name: string
43
+
44
+ // May it be installed under a different name? Targets yes — that is how a
45
+ // project gets two Go modules from one target. Features no: a feature's
46
+ // name is part of the generated `options.feature.<name>` config key and of
47
+ // the hook wiring in every target, so renaming one at install time is real
48
+ // work with no customer.
49
+ alias: boolean
50
+
51
+ // Rewrite the definition's own text when it is installed under a different
52
+ // name (targets rewrite their `main: kit: target: <name>:` key). Only
53
+ // reached when `alias` is true.
54
+ rename?: (src: string, origname: string, name: string) => string
55
+
56
+ // Is the copied definition PROJECT-OWNED once written? An alias exists to
57
+ // be differentiated, so its model file is created and then never
58
+ // overwritten; everything else is toolchain-owned and resyncs.
59
+ ownedWhenAliased?: boolean
60
+
61
+ // Everything BESIDES the definition file that an item of this kind must
62
+ // have in its package, `{name}`-templated and relative to the `.sdk`
63
+ // folder. `validateManifest` checks these, which is how a manifest claiming
64
+ // a target it does not ship fails at `package add` rather than partway
65
+ // through installing it.
66
+ //
67
+ // A feature has none: its definition is the whole of it. Its per-target
68
+ // SOURCE is deliberately not required — a feature package ships overlays
69
+ // only for the targets it supports, and `targetsSupported` is where that
70
+ // coverage is declared (design §7).
71
+ trees?: TreeDef[]
72
+ }
73
+
74
+
75
+ // One tree an item of this kind owns, `{name}`-templated and relative to the
76
+ // `.sdk` folder.
77
+ type TreeDef = {
78
+ path: string
79
+
80
+ // What the copy substitutes on the way in — and therefore what doctor must
81
+ // re-apply before comparing, or every templated file reads as edited.
82
+ // none — copied verbatim, so a byte compare is the truth
83
+ // template — through jostraca's template() with ProjectName et al
84
+ replace: 'none' | 'template'
85
+
86
+ // Must it be there for the item to be installable? A missing REQUIRED tree
87
+ // fails manifest validation before anything is written; a missing optional
88
+ // one is simply not copied.
89
+ required: boolean
90
+ }
91
+
92
+
93
+ // Rewrite the ITEM KEY in a copied model file, for an aliased install.
94
+ //
95
+ // Parameterised by kind because `main: kit: docs: <n>:` is the same rewrite
96
+ // with a different word, and a second copy of this regex is exactly the
97
+ // same-rule-written-twice defect the registry exists to prevent.
98
+ //
99
+ // Two forms are in use across the shipped models — bare (`target: go:`) and
100
+ // quoted (`target: 'go-cli':`) — and two paths carry the key: the item
101
+ // block itself (`main: kit: target: <t>:`) and the per-target feature-deps
102
+ // slot every target model declares
103
+ // (`main: kit: feature: &: target: <t>: deps: &:`). Both belong to the
104
+ // installed item, so both move. Matching on `<kind>: ` rather than on the
105
+ // bare name is what keeps the rewrite off the item's own values — `ext: go`
106
+ // and `module: name: '$$name$$'` must not change.
107
+ //
108
+ // ONE regex, with the quote optional and captured, rather than two entries in
109
+ // jostraca's `replace` map: that map canonicalises each key into a regex
110
+ // group NAME, and the bare and quoted spellings of the same key reduce to the
111
+ // same name — so one silently overwrote the other and `go-cli~cli2` came out
112
+ // as the BARE `target: cli2:`, losing the quoting a hyphenated key needs.
113
+ //
114
+ // The alias may also NEED quoting when the origin did not: aontu rejects a
115
+ // bare key containing a hyphen (`unexpected character(s): -`), so
116
+ // `target add go~go-alt` emitting the origin's unquoted style produced a
117
+ // model that could not compile at all. Quote when the origin was quoted OR
118
+ // the alias is not a bare identifier.
119
+ const BARE_KEY_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/
120
+
121
+ function aliasModelKey(kind: string) {
122
+ return function aliasModelText(
123
+ src: string, origname: string, name: string,
124
+ ): string {
125
+ const mustQuote = !BARE_KEY_RE.test(name)
126
+
127
+ return src.replace(
128
+ new RegExp(kind + ":(\\s*)('?)" + escapeRe(origname) + "\\2:", 'g'),
129
+ (_m: string, gap: string, quote: string) => {
130
+ const q = ('' !== quote || mustQuote) ? "'" : ''
131
+ return kind + ':' + gap + q + name + q + ':'
132
+ })
133
+ }
134
+ }
135
+
136
+
137
+ function escapeRe(s: string): string {
138
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
139
+ }
140
+
141
+
142
+ const KINDS: Record<string, KindDef> = Object.assign(Object.create(null), {
143
+ target: {
144
+ name: 'target', alias: true, ownedWhenAliased: true,
145
+ rename: aliasModelKey('target'),
146
+ // Components are dispatched by the convention `cmp/<t>/Main_<t>`, and the
147
+ // template tree is what `target add` copies — a target missing either is
148
+ // not installable, however complete its model file looks.
149
+ trees: [
150
+ { path: 'src/cmp/{name}', replace: 'none', required: true },
151
+ { path: 'tm/{name}', replace: 'template', required: true },
152
+ ],
153
+ },
154
+
155
+ feature: { name: 'feature', alias: false },
156
+
157
+ // DOCS — the third kind. See docs/design/sdkgen-packages.md §20.
158
+ //
159
+ // Its trees are NESTED under the kind name (`src/cmp/docs/<n>`, not
160
+ // `src/cmp/<n>`) so a docs item and a target may share a name without
161
+ // sharing a directory. Everything that composes those paths takes them
162
+ // from here.
163
+ //
164
+ // `tm/docs/{name}` is NOT required: a docs item whose every emitted byte
165
+ // depends on the API — a catalogue entry, a config file — legitimately
166
+ // ships no template tree, while a static site needs one. So it is
167
+ // copy-if-present, and `package check` does not demand it.
168
+ docs: {
169
+ name: 'docs', alias: true, ownedWhenAliased: true,
170
+ rename: aliasModelKey('docs'),
171
+ trees: [
172
+ { path: 'src/cmp/docs/{name}', replace: 'none', required: true },
173
+ { path: 'tm/docs/{name}', replace: 'template', required: false },
174
+ ],
175
+ },
176
+ })
177
+
178
+
179
+ // The trees a kind's add copies, with `{name}` resolved. ONE composition of
180
+ // these paths, read by the add, by doctor's drift walk and (through
181
+ // `requires`) by manifest validation — three readers that must agree about
182
+ // where an item's content lives, and previously would each have spelled it.
183
+ function kindTrees(kind: string, name: string): TreeDef[] {
184
+ return (kindDef(kind).trees ?? []).map((t: TreeDef) => ({
185
+ ...t,
186
+ path: t.path.split('{name}').join(name),
187
+ }))
188
+ }
189
+
190
+
191
+ function kindDef(kind: string): KindDef {
192
+ const def = KINDS[kind]
193
+ if (null == def) {
194
+ throw new SdkGenError('Unknown kind: ' + kind)
195
+ }
196
+ return def
197
+ }
198
+
199
+
200
+ // Resolve one ref for a kind, applying that kind's alias policy.
201
+ //
202
+ // A BARE name resolves against what the model already RECORDS. Without that,
203
+ // provenance would be write-only: the add actions re-run with the model's own
204
+ // keys (`circuitbreaker`, not the ref it was installed from), a bare name
205
+ // falls back to the bundled scaffold — whose `.sdk` folder exists, so
206
+ // resolution SUCCEEDS — and the copy then throws on a definition that is not
207
+ // there. An explicit ref always wins: that is how something is moved to a new
208
+ // source.
209
+ function resolveKind(ref: string, kind: string, ctx$: any): Source {
210
+ const def = kindDef(kind)
211
+ const model = ctx$.model
212
+
213
+ const declared: any = model?.main?.[KIT]?.[kind]?.[ref]
214
+ const recorded = (isBare(ref) && recordedRef(declared, ref)) || ref
215
+
216
+ const source = resolveSource(recorded, kind, ctx$)
217
+
218
+ // Asked of the RESOLVER rather than by re-reading the ref. `~` separates an
219
+ // alias only in the last segment, and a check that looked for one anywhere
220
+ // rejected every ref whose PATH contains a tilde — a Windows 8.3 short name
221
+ // like `C:\Users\RUNNER~1\...` is one. Parsing the ref in two places is what
222
+ // let that defect come back after it was fixed.
223
+ if (!def.alias && source.name !== source.origname) {
224
+ throw new SdkGenError(
225
+ capitalise(kind) + ' aliasing is not supported: ' + ref +
226
+ '\n A ' + kind + ' name is part of the generated config ' +
227
+ '(options.' + kind + '.<name>) and of the hook wiring in every target, ' +
228
+ 'so it cannot be renamed at install time.')
229
+ }
230
+
231
+ // The DEFINITION has to be there, not just the folder that would hold it.
232
+ //
233
+ // Resolution stops at the `.sdk` folder, and a bare name resolves to the
234
+ // bundled scaffold — whose folder always exists — so a name with no
235
+ // definition anywhere resolved "successfully" and then failed much later
236
+ // inside jostraca's Copy, as a shape validation error naming a path the
237
+ // caller never wrote.
238
+ //
239
+ // Where that bites is the fan-out: `target add` re-runs `feature add` for
240
+ // every feature key in the model, and a key with no definition installed
241
+ // aborted the ENTIRE target add — no templates, no components, nothing —
242
+ // over one feature. A plain Error, not an SdkGenError, so the fan-out's
243
+ // per-item guard skips it with a warning; the add path for an explicit ref
244
+ // does not catch, so `feature add nosuch` still fails, now saying what is
245
+ // actually missing.
246
+ if (!ctx$.fs().existsSync(source.model)) {
247
+ throw new Error(
248
+ capitalise(kind) + ' definition not found: ' + source.model)
249
+ }
250
+
251
+ return source
252
+ }
253
+
254
+
255
+ // Emit the kind's definition file and its index entry.
256
+ //
257
+ // Called inside a `Folder({ name: 'model/<kind>' })`, so both land together.
258
+ // `names` is every INSTALLED name seen so far in this run: the index File is
259
+ // re-rendered per item and the last render wins, so each render has to carry
260
+ // all of them.
261
+ function kindModel(props: {
262
+ ctx$: any,
263
+ kind: string,
264
+ source: Source,
265
+ names: string[],
266
+ content: string,
267
+ }) {
268
+ const { ctx$, kind, source, names, content } = props
269
+ const def = kindDef(kind)
270
+ const fs = ctx$.fs()
271
+ const log = ctx$.log
272
+
273
+ const aliased = source.name !== source.origname
274
+
275
+ const replace = provenanceReplace({
276
+ base: source.base,
277
+ origname: source.origname,
278
+ name: source.name,
279
+ package: source.package,
280
+ })
281
+
282
+ if (aliased) {
283
+ // The copy lands under the INSTALLED name AND declares it. Left alone it
284
+ // kept the origin basename (jostraca defaults a single-file Copy's
285
+ // destination to the source's), so the index named a file that does not
286
+ // exist — which fails the whole model compile, not just the alias.
287
+ //
288
+ // `exclude: true` — CREATE, never overwrite, for a kind whose aliased
289
+ // definition is project-owned: an alias exists to be differentiated,
290
+ // which is why doctor reports its diffs as informational and why
291
+ // add-a-target tells the project to edit it.
292
+ const owned = true === def.ownedWhenAliased
293
+
294
+ if (owned) {
295
+ const dest = Path.join(
296
+ ctx$.folder ?? '.', 'model', kind, source.name + '.aontu')
297
+
298
+ if (fs.existsSync(dest)) {
299
+ log.info({
300
+ point: kind + '-alias-model-kept', [kind]: source.name, file: dest,
301
+ note: source.name + ': keeping the existing aliased ' + kind +
302
+ ' model (project-owned — an alias is differentiated by editing it)'
303
+ })
304
+ }
305
+ }
306
+
307
+ const src = fs.readFileSync(source.model, 'utf8')
308
+ const text = null == def.rename ? src :
309
+ def.rename(src, source.origname, source.name)
310
+
311
+ File({ name: source.name + '.aontu', exclude: owned }, () =>
312
+ Content(template(text, ctx$.model, { replace })))
313
+ }
314
+ else {
315
+ Copy({ from: source.model, replace })
316
+ }
317
+
318
+ File({ name: def.name + '-index.aontu' }, () => UpdateIndex({
319
+ content,
320
+ names,
321
+ }))
322
+ }
323
+
324
+
325
+ // The ref that reinstalls what the model already records — `base` says which
326
+ // `.sdk` folder, `origname` says what it is called there.
327
+ //
328
+ // The ALIAS has to be carried back through. Rebuilding only `<base>/../<orig>`
329
+ // resolves to the ORIGIN name, so `target add go2` (after installing
330
+ // `go~go2`) would refresh and index a new `go` target and leave `go2` stale —
331
+ // losing the differentiated identity the alias exists for, and the
332
+ // project-owned model file with it.
333
+ //
334
+ // ONE definition, used by the add actions and by doctor. This reconstruction
335
+ // was written twice and the two copies had already diverged on exactly this
336
+ // point, which is the drift the kind spine exists to end.
337
+ function recordedRef(declared: any, name: string): string | undefined {
338
+ if (null == declared?.base || '' === declared.base) {
339
+ return undefined
340
+ }
341
+
342
+ const origname = declared.origname || name
343
+
344
+ return Path.join(declared.base, '..', origname) +
345
+ (origname === name ? '' : '~' + name)
346
+ }
347
+
348
+
349
+ // A bare NAME, as opposed to a ref that locates a source.
350
+ function isBare(ref: string): boolean {
351
+ return !ref.includes('/') && !ref.includes(Path.sep)
352
+ }
353
+
354
+
355
+ function capitalise(s: string): string {
356
+ return s.charAt(0).toUpperCase() + s.slice(1)
357
+ }
358
+
359
+
360
+ export type {
361
+ KindDef,
362
+ TreeDef,
363
+ }
364
+
365
+ export {
366
+ KINDS,
367
+ recordedRef,
368
+ aliasModelKey,
369
+ kindTrees,
370
+ escapeRe,
371
+ kindDef,
372
+ resolveKind,
373
+ kindModel,
374
+ isBare,
375
+ }