@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
@@ -6,17 +6,19 @@ import {
6
6
  Folder,
7
7
  Copy,
8
8
  File,
9
+ Content,
9
10
  cmp,
10
11
  each,
12
+ template,
11
13
  } from 'jostraca'
12
14
 
13
15
  import { showChanges } from '@voxgig/util'
14
16
 
15
17
  import { showDryrun } from '../helpers/dryrun'
16
18
 
17
- import { templateReplacements } from '../helpers/stdrep'
19
+ import { templateReplacements, provenanceReplace } from '../helpers/stdrep'
18
20
 
19
- import { getelem } from '@voxgig/struct'
21
+ import { isJunk, copyOpts } from '../helpers/junk'
20
22
 
21
23
  import { Aontu } from 'aontu'
22
24
 
@@ -32,6 +34,7 @@ import type {
32
34
  import { SdkGenError } from '../utility'
33
35
 
34
36
  import {
37
+ BASE_FEATURE,
35
38
  availableFeatures,
36
39
  findFeatureSources,
37
40
  featureExcludes,
@@ -48,6 +51,10 @@ import {
48
51
  loadContent,
49
52
  } from './action'
50
53
 
54
+ import { kindModel, resolveKind, escapeRe } from './kind'
55
+
56
+ import { BUNDLED, resolveSource, registerInstalled } from './resolve'
57
+
51
58
 
52
59
  const CMD_MAP: any = {
53
60
  add: cmd_target_add
@@ -99,6 +106,10 @@ async function target_add(targets: string[], actx: ActionContext): Promise<Actio
99
106
  control: {
100
107
  dryrun: !!actx.opts.dryrun
101
108
  },
109
+ // Per-call for the same reason `control` is: this action runs on whatever
110
+ // Jostraca instance the caller handed it, and a template tree must not
111
+ // carry a maintainer's build droppings into a project. See helpers/junk.
112
+ cmp: copyOpts(),
102
113
  }
103
114
 
104
115
  opts.log.info({
@@ -131,9 +142,21 @@ async function target_add(targets: string[], actx: ActionContext): Promise<Actio
131
142
  showDryrun(opts.log, 'target-result', jres, actx.folder)
132
143
  }
133
144
 
134
- // feature_add copies feature templates for targets already registered in
135
- // the model. The targets added above are not in the in-memory model yet,
136
- // so TargetRoot copies their feature templates itself.
145
+ // The targets just written are not in the in-memory model — nothing
146
+ // recompiles `model/sdk.aontu` mid-process so put them there before the
147
+ // fan-out below.
148
+ //
149
+ // TargetRoot copies each new target's feature source from ITS OWN TREE,
150
+ // which is the whole story only while every feature ships in the same
151
+ // package as every target. For a feature supplied by a DIFFERENT package,
152
+ // the source lives in that package's overlay, and only `feature_add`'s
153
+ // two-tree lookup consults it — for targets in the model. So a target added
154
+ // while such a feature was already active got no source for it at all:
155
+ // TargetRoot could not find it, and the fan-out could not see the target.
156
+ registerInstalled('target', targets, actx)
157
+
158
+ // feature_add copies feature templates for every target in the model,
159
+ // which now includes the ones just added.
137
160
  await feature_add(features, actx)
138
161
 
139
162
  opts.log.info({
@@ -148,9 +171,15 @@ async function target_add(targets: string[], actx: ActionContext): Promise<Actio
148
171
 
149
172
 
150
173
  const TargetRoot = cmp(function TargetRoot(props: any) {
151
- const { ctx$, targets, features } = props
174
+ const { ctx$, targets, features, actx } = props
152
175
  const { model, log } = ctx$
153
176
 
177
+ const fs = ctx$.fs()
178
+
179
+ // The prune below writes through `fs` directly rather than through
180
+ // jostraca, so it has to be told about the dry run itself.
181
+ const dryrun = !!actx?.opts?.dryrun
182
+
154
183
  // TODO: jostraca - make from value easier to specify
155
184
  // const tfolder = 'node_modules/@voxgig/sdkgen/project/.sdk'
156
185
 
@@ -169,7 +198,13 @@ const TargetRoot = cmp(function TargetRoot(props: any) {
169
198
  note: tref
170
199
  })
171
200
 
172
- const { tname, tfolder, torigname, base } = resolveTarget(tref, ctx$)
201
+ // Resolved through the shared kind spine, so a BARE name follows what
202
+ // the model records exactly as a feature's does. Without that, a target
203
+ // installed from an external package resolved back to the bundled
204
+ // scaffold on its next `target add` — the same write-only-provenance
205
+ // trap features had.
206
+ const source = resolveKind(tref, 'target', ctx$)
207
+ const { name: tname, folder: tfolder, origname: torigname, base } = source
173
208
  tnames.push(tname)
174
209
  const targetNote = tname + (tname != tref ? ' ref:' + tref : '')
175
210
 
@@ -180,26 +215,34 @@ const TargetRoot = cmp(function TargetRoot(props: any) {
180
215
  note: tname + (tname != torigname ? 'original' + torigname : '') + ' from:' + tfolder
181
216
  })
182
217
 
183
- Folder({ name: 'model/target' }, () => {
184
- Copy({
185
- from: tfolder + '/model/target/' + torigname + '.aontu',
186
- // exclude: true
187
- replace: {
188
- "'BASE'": "'" + base + "'"
189
- }
190
- })
191
- File({ name: 'target-index.aontu' }, () => UpdateIndex({
192
- content: ctx$.meta.content.target_index,
193
- names: tnames,
194
- }))
195
- })
196
-
197
- Folder({ name: 'src/cmp/' + tname }, () => {
198
- Copy({
199
- from: tfolder + '/src/cmp/' + torigname,
200
- // exclude: true
218
+ // An ALIASED add (`target add go~go2`) installs the target under a new
219
+ // name, and every one of the three trees has to agree about that name.
220
+ const aliased = tname !== torigname
221
+
222
+ // The definition file and the index entry: the same for every kind, so
223
+ // they are emitted once, in action/kind.
224
+ Folder({ name: 'model/target' }, () => kindModel({
225
+ ctx$, kind: 'target', source, names: tnames,
226
+ content: ctx$.meta.content.target_index,
227
+ }))
228
+
229
+ if (aliased) {
230
+ // Components are dispatched by CONVENTION — `cmp/<t>/Main_<t>` — so
231
+ // an aliased tree whose files keep the origin suffix resolves
232
+ // nothing: `src/cmp/go2/Main_go.ts` is invisible to a lookup for
233
+ // `cmp/go2/Main_go2`. jostraca's tree Copy has no per-entry rename
234
+ // hook, so an aliased tree is emitted file by file instead.
235
+ aliasCmpTree(ctx$, tfolder + '/src/cmp/' + torigname,
236
+ 'src/cmp/' + tname, torigname, tname)
237
+ }
238
+ else {
239
+ Folder({ name: 'src/cmp/' + tname }, () => {
240
+ Copy({
241
+ from: tfolder + '/src/cmp/' + torigname,
242
+ // exclude: true
243
+ })
201
244
  })
202
- })
245
+ }
203
246
 
204
247
  // Copy the whole template tree MINUS the source of every feature the
205
248
  // model did not ask for. Which files those are is discovered from the
@@ -221,7 +264,8 @@ const TargetRoot = cmp(function TargetRoot(props: any) {
221
264
  // package failed to compile — "fhHasFeature redeclared in this block".
222
265
  //
223
266
  // The invariant this restores: tm/<target> == source tree MINUS trim.
224
- pruneStaleTemplates(ctx$, tfolder + '/tm/' + torigname, 'tm/' + tname, trim)
267
+ pruneStaleTemplates(
268
+ ctx$, tfolder + '/tm/' + torigname, 'tm/' + tname, trim, dryrun)
225
269
 
226
270
  Folder({ name: 'tm/' + tname }, () => {
227
271
  Copy({
@@ -241,6 +285,127 @@ const TargetRoot = cmp(function TargetRoot(props: any) {
241
285
  })
242
286
 
243
287
 
288
+
289
+
290
+ // `<Cmp>_<origname>.<ext>` -> `<Cmp>_<tname>.<ext>`, for an aliased install.
291
+ // Anything not carrying the suffix (tsconfig.json, the fragment sources)
292
+ // keeps its name — the `.<ext>` anchor is what keeps this off
293
+ // `Main.fragment.go`, whose `go` is a file extension.
294
+ //
295
+ // Shared with doctor for the same reason as aliasCmpText: doctor walks the
296
+ // ORIGIN tree to decide what should be present, so it has to land each file
297
+ // under the same name the writer gave it, or it reports the whole tree as
298
+ // missing.
299
+ function aliasCmpName(name: string, torigname: string, tname: string): string {
300
+ return name.replace(
301
+ new RegExp('_' + escapeRe(torigname) + '(\\.[^.]+)$'), '_' + tname + '$1')
302
+ }
303
+
304
+
305
+ // The origin name a component carries INSIDE its source, rewritten for an
306
+ // aliased install. Shared with doctor, which re-applies it before comparing —
307
+ // same discipline as templateReplacements: a writer and a reader that
308
+ // disagree by a character make every file read as a fork.
309
+ //
310
+ // Rewritten here rather than through jostraca's `replace` map, because that
311
+ // map canonicalises each key into a regex group NAME — `_go'` and `_go"` both
312
+ // reduce to the same name, so the later entry silently won and every
313
+ // single-quoted import came out as `from './Package_go2"`. One explicit regex
314
+ // keeps the quote it matched.
315
+ // `cmpbase` is the component tree's PREFIX — `src/cmp/` for a target,
316
+ // `src/cmp/docs/` for a docs item. Taken from the caller rather than fixed
317
+ // here: a docs item's components live one level deeper, and a rewrite that
318
+ // looked for `src/cmp/<orig>/` left an aliased docs component pointing at the
319
+ // ORIGIN's fragments — reading them silently when both are installed.
320
+ function aliasCmpText(
321
+ src: string, torigname: string, tname: string, cmpbase = 'src/cmp/',
322
+ ): string {
323
+ const orig = escapeRe(torigname)
324
+
325
+ return src
326
+ // The fragment directory, read relative to __dirname. The fragments are
327
+ // copied into the ALIAS's folder, so leaving the origin path would miss —
328
+ // or, if the origin target is also installed, silently read ITS fragments.
329
+ .replace(new RegExp(escapeRe(cmpbase) + orig + '/', 'g'), cmpbase + tname + '/')
330
+ // Sibling imports: `'./Package_go'` -> `'./Package_go2'`. Anchored on the
331
+ // closing quote (captured, so the style is preserved) to keep it off file
332
+ // EXTENSIONS — `Main.fragment.go` must not become `Main.fragment.go2`.
333
+ .replace(new RegExp('_' + orig + '([\'"])', 'g'), '_' + tname + '$1')
334
+ }
335
+
336
+
337
+ // Emit an aliased `src/cmp` tree: every file renamed from the origin suffix
338
+ // to the installed one, and its CONTENT rewritten to match.
339
+ //
340
+ // Renaming alone would break the tree, because a component names its origin
341
+ // twice over: sibling imports (`from './Package_go'`) and the fragment
342
+ // directory it reads through `__dirname` (`/../../../src/cmp/go/fragment/`,
343
+ // in 67 of the shipped components). Both are rewritten here — the fragment
344
+ // path because the fragments are copied to the ALIAS's folder, so the origin
345
+ // path would either miss or, worse, silently read the origin target's
346
+ // fragments if that target is also installed.
347
+ //
348
+ // Files are emitted through jostraca (`File`/`Content`) rather than copied
349
+ // with `fs`, so a dry run reports them and writes nothing, exactly as the
350
+ // tree Copy on the unaliased path does.
351
+ function aliasCmpTree(
352
+ ctx$: any,
353
+ fromDir: string,
354
+ toRel: string,
355
+ torigname: string,
356
+ tname: string,
357
+ cmpbase = 'src/cmp/',
358
+ ) {
359
+ const fs = ctx$.fs()
360
+
361
+ const aliasText = (src: string) =>
362
+ aliasCmpText(src, torigname, tname, cmpbase)
363
+
364
+ const emit = (dir: string, rel: string) => {
365
+ let entries: any[]
366
+ try {
367
+ entries = fs.readdirSync(dir, { withFileTypes: true })
368
+ }
369
+ catch (e: any) {
370
+ return
371
+ }
372
+
373
+ // Sorted, so an aliased tree is emitted in the same byte-stable order
374
+ // everything else in this toolchain is. Junk is dropped here because this
375
+ // walk stands in for a tree Copy, which drops it through
376
+ // `cmp.Copy.ignore` — an aliased install must not be the one path that
377
+ // ships a maintainer's `__pycache__`. See helpers/junk.
378
+ const names = entries
379
+ .map((ent: any) => ent.name)
380
+ .filter((name: string) => !isJunk(name))
381
+ .sort()
382
+
383
+ for (const name of names) {
384
+ const child = Path.join(dir, name)
385
+ const ent = entries.find((e: any) => e.name === name)
386
+
387
+ if (ent.isDirectory()) {
388
+ Folder({ name }, () => emit(child, rel + '/' + name))
389
+ continue
390
+ }
391
+
392
+ const renamed = aliasCmpName(name, torigname, tname)
393
+
394
+ // `template` against the model with no replace map, matching what Copy
395
+ // does for the unaliased tree — jostraca's Copy always interpolates
396
+ // `$$ref$$` against the model, so an aliased tree must too.
397
+ const src = fs.readFileSync(child, 'utf8')
398
+
399
+ File({ name: renamed }, () => Content(template(aliasText(src), ctx$.model)))
400
+ }
401
+ }
402
+
403
+ Folder({ name: toRel }, () => emit(fromDir, toRel))
404
+ }
405
+
406
+
407
+
408
+
244
409
  // Path patterns that keep a target's unwanted feature source out of the
245
410
  // project: the source of every AVAILABLE feature the model did not select,
246
411
  // plus the templates that only compile with the complete feature set.
@@ -274,6 +439,7 @@ function pruneStaleTemplates(
274
439
  fromDir: string,
275
440
  toRel: string,
276
441
  trim: RegExp[],
442
+ dryrun?: boolean,
277
443
  ) {
278
444
  const { log } = ctx$
279
445
  const fs = ctx$.fs()
@@ -291,6 +457,16 @@ function pruneStaleTemplates(
291
457
  return
292
458
  }
293
459
  for (const ent of entries) {
460
+ // Junk is invisible to this listing on BOTH sides, and it has to be
461
+ // both. In the source it is never copied, so counting it would want a
462
+ // file that can never arrive. In the destination it was never written
463
+ // by this toolchain, so counting it as stale would have `target add`
464
+ // delete a maintainer's own `__pycache__` out of their SDK repo —
465
+ // this prune's remit is the tree it writes, not the tree it finds.
466
+ if (isJunk(ent.name)) {
467
+ continue
468
+ }
469
+
294
470
  const child = Path.join(dir, ent.name)
295
471
  const childRel = '' === rel ? ent.name : rel + '/' + ent.name
296
472
  if (ent.isDirectory()) {
@@ -324,6 +500,29 @@ function pruneStaleTemplates(
324
500
  return
325
501
  }
326
502
 
503
+ // A DRY RUN must not delete. This prune calls `fs.unlinkSync` directly, and
504
+ // jostraca enforces `control.dryrun` only inside its own write layer — so
505
+ // `-y target add <t>` previewed the copies and then really removed every
506
+ // stale template, which is the opposite of what the flag promises and
507
+ // exactly the blast radius a maintainer runs `-y` to inspect. Report the
508
+ // deletions instead, in the same shape the copies are reported.
509
+ if (dryrun) {
510
+ log.info({
511
+ point: 'target-template-prune', target: toRel, count: stale.length,
512
+ files: stale, dryrun: true,
513
+ note: toRel + ': would remove ' + stale.length +
514
+ ' stale template(s) — ** DRY RUN **, nothing was written'
515
+ })
516
+ for (const rel of stale) {
517
+ log.info({
518
+ point: 'target-template-prune-file', target: toRel,
519
+ file: toRel + '/' + rel, dryrun: true,
520
+ note: 'would remove ' + toRel + '/' + rel
521
+ })
522
+ }
523
+ return
524
+ }
525
+
327
526
  const removed: string[] = []
328
527
  for (const rel of stale) {
329
528
  try {
@@ -349,6 +548,52 @@ function pruneStaleTemplates(
349
548
  }
350
549
 
351
550
 
551
+ // WHICH NAMES COUNT AS FEATURE SOURCE, when deciding what to trim.
552
+ //
553
+ // This used to be the SOURCE folder's own `model/feature/` listing, which is
554
+ // right only while every target ships in the same package as every feature.
555
+ // An external target package declares no feature models of its own — it has
556
+ // no reason to — so nothing was discovered, nothing was trimmed, and the
557
+ // consumer received the target's source for EVERY feature regardless of what
558
+ // its model selected. Measured on a target copied from the bundled `go`: the
559
+ // bundled one keeps 2 feature source files, the external one kept all 18.
560
+ //
561
+ // That is the failure `helpers/featureSource` was written to end (272 stray
562
+ // files in one repo), arriving again by the one route it did not cover.
563
+ //
564
+ // The catalogue is therefore the union of every place a feature this project
565
+ // could select can come from:
566
+ //
567
+ // - the bundled scaffold, which is what a bare `feature add <name>` means;
568
+ // - the source package's own declarations, for a package shipping both;
569
+ // - the consumer's OWN installed feature models, which is how an EXTERNAL
570
+ // feature's source becomes trimmable at all.
571
+ //
572
+ // Every term is a place feature DEFINITIONS live, deliberately. Taking the
573
+ // third from the model's feature KEYS instead would make any name a project
574
+ // happens to declare a trim candidate — and a file in a `feature/` directory
575
+ // is not necessarily a feature. `tm/rust/feature/support.rs` and its siblings
576
+ // are shared machinery that `Main_rust` emits unconditionally (`pub mod
577
+ // support`), so a project that declared a feature called `support` would have
578
+ // had that file pruned and produced a crate that cannot compile. A definition
579
+ // file is evidence that the name really denotes a feature; a model key is not.
580
+ //
581
+ // For a bundled add this is `bundled ∪ bundled ∪ (⊆ bundled)`, so the trim is
582
+ // byte-identical to what it always was — the goldens hold it to that.
583
+ function featureCatalogue(ctx$: any, tfolder: string): string[] {
584
+ const fs = ctx$.fs()
585
+ const root = ctx$.folder ?? '.'
586
+
587
+ const names = new Set<string>([
588
+ ...availableFeatures(fs, Path.join(root, BUNDLED)),
589
+ ...availableFeatures(fs, tfolder),
590
+ ...availableFeatures(fs, root),
591
+ ])
592
+
593
+ return Array.from(names).sort()
594
+ }
595
+
596
+
352
597
  function trimFeatures(
353
598
  ctx$: any,
354
599
  tfolder: string,
@@ -371,9 +616,9 @@ function trimFeatures(
371
616
 
372
617
  // `base` is not a declared feature — it is the always-present foundation
373
618
  // every other feature builds on — so it is never a trim candidate.
374
- const selected = new Set(['base', ...(features ?? [])])
619
+ const selected = new Set([BASE_FEATURE, ...(features ?? [])])
375
620
 
376
- const available = availableFeatures(fs, tfolder)
621
+ const available = featureCatalogue(ctx$, tfolder)
377
622
  const drop = findFeatureSources(fs, tfolder + '/tm/' + torigname, available)
378
623
  .filter((s) => !selected.has(s.name))
379
624
 
@@ -440,95 +685,32 @@ function readTargetFeature(
440
685
  }
441
686
 
442
687
 
443
- // Last path segment of a ref. A ref may be a bare target name ('go'), a
444
- // package-relative path ('@acme/kit/go'), or an ABSOLUTE path and on Windows
445
- // an absolute path is separated by `\`, so splitting on '/' alone hands back
446
- // the whole path as the target name and every tree lookup below then misses.
447
- // On POSIX Path.sep IS '/', so this is the same split it always was.
448
- function lastSegment(ref: string): string {
449
- return getelem(ref.split('/').flatMap((p: string) => p.split(Path.sep)), -1)
450
- }
451
-
452
-
688
+ // `target add`'s view of the shared resolver: the same resolution every kind
689
+ // uses, with this action's historical field names. Kept as a wrapper so its
690
+ // callers (TargetRoot, doctor) and their tests do not have to move with the
691
+ // extraction.
453
692
  function resolveTarget(tref: string, ctx$: any) {
454
- let tname = tref
455
- let torigname = tref
456
- let tfolder = 'node_modules/@voxgig/sdkgen/project/.sdk'
457
-
458
- const root = ctx$.folder
459
- const fs = ctx$.fs()
460
-
461
- let fulltfolder = Path.normalize(Path.join(root, tfolder))
462
- tname = lastSegment(tref)
693
+ const src = resolveSource(tref, 'target', ctx$)
463
694
 
464
- let aliasref = tref
465
- torigname = lastSegment(aliasref)
466
- const aliasing = tref.split('~')
467
- if (1 < aliasing.length) {
468
- aliasref = aliasing[0]
469
- tname = aliasing.slice(1).join('~')
470
- torigname = lastSegment(aliasref)
471
- }
472
-
473
- const search: string[] = []
474
- let found = false
475
- // Windows: an absolute ref is `D:\a\...` or `D:/a/...`, and a Path.join'd
476
- // one carries backslashes, so neither `includes('/')` nor `startsWith('/')`
477
- // recognises it. Path.isAbsolute and Path.sep are platform-correct and
478
- // reduce to the same answers on POSIX.
479
- if (aliasref.includes('/') || aliasref.includes(Path.sep)) {
480
- // NOTE: the last path element of the ref is the target name, not a folder.
481
- const aliasbase = Path.dirname(aliasref)
482
-
483
- if (!Path.isAbsolute(aliasref)) {
484
- fulltfolder = Path.normalize(Path.join(root, 'node_modules', aliasbase, '.sdk'))
485
- search.push(fulltfolder)
486
- found = fs.existsSync(fulltfolder)
487
-
488
- if (!found) {
489
- fulltfolder = Path.normalize(Path.join(root, aliasbase, '.sdk'))
490
- search.push(fulltfolder)
491
- found = fs.existsSync(fulltfolder)
492
- }
493
- }
494
- else {
495
- fulltfolder = Path.normalize(Path.join(aliasbase, '.sdk'))
496
- search.push(fulltfolder)
497
- found = fs.existsSync(fulltfolder)
498
- }
499
- }
500
- else {
501
- search.push(fulltfolder)
502
- found = fs.existsSync(fulltfolder)
503
- }
504
-
505
- if (!found) {
506
- throw new Error('Target folder not found in:\n' + search.join('\n '))
507
- }
508
-
509
- // `base` is the target folder relative to the project root. Compare with the
510
- // PLATFORM separator: on Windows `root + '/'` never prefixes a normalised
511
- // absolute path, so the root would not be stripped and `base` would stay
512
- // absolute. Normalise both sides first for the same reason.
513
- const nroot = Path.normalize(root)
514
- const rootslash = nroot.endsWith(Path.sep) ? nroot : nroot + Path.sep
515
- const out = {
516
- tname,
517
- tfolder: fulltfolder,
518
- torigname,
519
- base: fulltfolder.startsWith(rootslash)
520
- ? fulltfolder.slice(rootslash.length)
521
- : fulltfolder
695
+ return {
696
+ tname: src.name,
697
+ tfolder: src.folder,
698
+ torigname: src.origname,
699
+ base: src.base,
700
+ package: src.package,
522
701
  }
523
-
524
- return out
525
702
  }
526
703
 
527
704
 
528
705
  export {
529
706
  action_target,
707
+ featureCatalogue,
530
708
  target_add,
531
709
  resolveTarget,
532
710
  trimFeatures,
533
711
  readTargetFeature,
712
+ aliasCmpText,
713
+ aliasCmpName,
714
+ aliasCmpTree,
715
+ pruneStaleTemplates,
534
716
  }
@@ -0,0 +1,125 @@
1
+ // GENERATING A DOCS ITEM. See docs/design/sdkgen-packages.md §20.3.
2
+ //
3
+ // A docs item is dispatched by the same convention a target is —
4
+ // `cmp/docs/<n>/Main_<n>` — so an item's package supplies the emitter and
5
+ // sdkgen supplies only the call.
6
+ //
7
+ // WHY THIS IS NOT RENDERED BY THE CONSUMER'S Root.ts
8
+ //
9
+ // A project's `Root.ts` is written once, by create-sdkgen, and is never
10
+ // touched again (doctor has an `unwired` category precisely because of it).
11
+ // So a kind introduced later can reach generation in one of two ways: every
12
+ // existing project edits its Root, or sdkgen runs the kind's own pass. The
13
+ // second is what happens here — `docs add` in a project scaffolded years ago
14
+ // generates with no scaffold change at all, which is the whole point of
15
+ // putting the destinations in packages.
16
+ //
17
+ // The cost is honest and small: a second `generate()` call over the same
18
+ // root, so the changes report arrives in two parts.
19
+
20
+ import { cmp, each, names, Folder } from 'jostraca'
21
+
22
+ import { requirePath } from '../utility'
23
+
24
+ import { ensureStdrep } from '../helpers/stdrep'
25
+
26
+ import { KIT } from '../types'
27
+
28
+
29
+ // The model preamble every pass performs before rendering: the case-variant
30
+ // name constants (`Name`, `NAME`, …) and the ProjectName replacement map that
31
+ // template substitution reads.
32
+ //
33
+ // Done HERE rather than relied upon, because a docs pass does NOT go through
34
+ // the consumer's Root — nothing else has set `ctx$.model`, and when there are
35
+ // out-of-tree items the Root was handed a FILTERED COPY, so the preamble it
36
+ // performed did not touch this object at all.
37
+ function prepareModel(model: any, ctx$: any) {
38
+ ctx$.model = model
39
+
40
+ model.const = model.const || { name: model.name }
41
+ names(model.const, model.name)
42
+ if (null == model.const.year) {
43
+ model.const.year = new Date().getFullYear()
44
+ }
45
+ names(model, model.name)
46
+
47
+ ctx$.stdrep = ctx$.stdrep || {}
48
+ names(ctx$.stdrep, model.Name, 'Project' + 'Name')
49
+ }
50
+
51
+
52
+ // One item: its emitter, called with the item and the shared replace map.
53
+ //
54
+ // No Folder here — WHERE it lands is the caller's business, because that is
55
+ // exactly what differs between the two passes: in-tree the item owns
56
+ // `<sdk-repo>/<name>/`, out-of-tree the destination IS the item's root.
57
+ const DocsItem = cmp(function DocsItem(props: any) {
58
+ const { item, ctx$ } = props
59
+ const log = ctx$.log
60
+ const model = props.model ?? ctx$.model
61
+
62
+ const stdrep = ensureStdrep(ctx$)
63
+
64
+ // The MODEL KEY is the fallback, and it is not paranoia: `name` comes from
65
+ // the base schema's `name: key()`, so an item whose project has not
66
+ // included that schema — or whose own definition omits it — reached
67
+ // `require('cmp/docs/undefined/Main_undefined')`, a message naming nothing
68
+ // the author wrote. Seen in the first real install of a docs package.
69
+ const name = item.name ?? item.key$
70
+
71
+ if (null == name || '' === name) {
72
+ throw new Error(
73
+ 'Docs item has no name and no model key, so its component cannot be ' +
74
+ 'located: ' + JSON.stringify(Object.keys(item ?? {})))
75
+ }
76
+
77
+ const Main_docs = requirePath(ctx$, `cmp/docs/${name}/Main_${name}`)
78
+
79
+ Main_docs['Main']({ model, docs: item, stdrep })
80
+
81
+ log.info({
82
+ point: 'generate-docs', docs: name, note: 'docs:' + name
83
+ })
84
+ })
85
+
86
+
87
+ // Every IN-TREE docs item, each in its own folder.
88
+ //
89
+ // Items with `output: path` are excluded: they get their own pass rooted at
90
+ // that path, and rendering them here as well would ALSO write them into
91
+ // `<sdk-repo>/<name>/` — the same reason `withoutExternal` exists for
92
+ // targets.
93
+ const Docs = cmp(function Docs(props: any) {
94
+ const { ctx$ } = props
95
+ const model = props.model ?? ctx$.model
96
+
97
+ prepareModel(model, ctx$)
98
+
99
+ const items = model?.main?.[KIT]?.docs ?? {}
100
+
101
+ each(items, (item: any) => {
102
+ if (false === item.active) {
103
+ return
104
+ }
105
+
106
+ const path = item.output?.path
107
+
108
+ if (null != path && '' !== path) {
109
+ return
110
+ }
111
+
112
+ const name = item.name ?? item.key$
113
+
114
+ names(item, name)
115
+
116
+ Folder({ name }, () => DocsItem({ item }))
117
+ })
118
+ })
119
+
120
+
121
+ export {
122
+ Docs,
123
+ DocsItem,
124
+ prepareModel,
125
+ }