@voxgig/sdkgen 3.0.0 → 3.1.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 (40) hide show
  1. package/dist/action/doctor.js +65 -10
  2. package/dist/action/doctor.js.map +1 -1
  3. package/dist/cmp/ExternalTarget.d.ts +2 -0
  4. package/dist/cmp/ExternalTarget.js +104 -0
  5. package/dist/cmp/ExternalTarget.js.map +1 -0
  6. package/dist/helpers/packageMeta.d.ts +9 -1
  7. package/dist/helpers/packageMeta.js +40 -0
  8. package/dist/helpers/packageMeta.js.map +1 -1
  9. package/dist/sdkgen.d.ts +2 -2
  10. package/dist/sdkgen.js +229 -2
  11. package/dist/sdkgen.js.map +1 -1
  12. package/dist/tsconfig.tsbuildinfo +1 -1
  13. package/dist/types.d.ts +12 -0
  14. package/dist/utility.js +10 -1
  15. package/dist/utility.js.map +1 -1
  16. package/model/sdkgen.aontu +120 -0
  17. package/package.json +1 -1
  18. package/project/.sdk/model/target/go-cli.aontu +0 -1
  19. package/project/.sdk/model/target/go-mcp.aontu +0 -1
  20. package/project/.sdk/model/target/java.aontu +0 -1
  21. package/project/.sdk/model/target/kotlin.aontu +0 -1
  22. package/project/.sdk/model/target/lean.aontu +0 -1
  23. package/project/.sdk/model/target/py-data.aontu +0 -1
  24. package/project/.sdk/model/target/scala.aontu +0 -1
  25. package/project/.sdk/model/target/seneca-provider.aontu +118 -0
  26. package/project/.sdk/model/target/zig.aontu +0 -1
  27. package/project/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +3915 -0
  28. package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +960 -0
  29. package/project/.sdk/tm/seneca-provider/CODE_OF_CONDUCT.md +132 -0
  30. package/project/.sdk/tm/seneca-provider/LICENSE +21 -0
  31. package/project/.sdk/tm/seneca-provider/Makefile +15 -0
  32. package/project/.sdk/tm/seneca-provider/src/tsconfig.json +18 -0
  33. package/project/.sdk/tm/seneca-provider/test/tsconfig.json +16 -0
  34. package/project/.sdk/tm/seneca-provider/tsfmt.json +3 -0
  35. package/src/action/doctor.ts +77 -12
  36. package/src/cmp/ExternalTarget.ts +125 -0
  37. package/src/helpers/packageMeta.ts +48 -0
  38. package/src/sdkgen.ts +280 -1
  39. package/src/types.ts +24 -1
  40. package/src/utility.ts +10 -1
package/src/sdkgen.ts CHANGED
@@ -22,6 +22,9 @@ import type {
22
22
  import { SdkGenError, requirePath, isAuthActive, resolveAuthPrefix } from './utility'
23
23
 
24
24
  import { Main } from './cmp/Main'
25
+ import { ExternalTarget } from './cmp/ExternalTarget'
26
+
27
+ import { KIT } from '@voxgig/apidef'
25
28
  import { Deploy } from './cmp/Deploy'
26
29
  import { Entity } from './cmp/Entity'
27
30
  import { Feature } from './cmp/Feature'
@@ -78,6 +81,8 @@ import {
78
81
  pkgDescription,
79
82
  nonAffiliation,
80
83
  keywords,
84
+ authorInfo,
85
+ contributorList,
81
86
  envName,
82
87
  envToken,
83
88
  goModule,
@@ -224,10 +229,62 @@ function SdkGen(opts: SdkGenOptions) {
224
229
  },
225
230
  }
226
231
 
227
- const jres = await jostraca.generate(jopts, () => Root({ model }))
232
+ // Targets that write OUTSIDE the SDK repo (`output: path`) are generated
233
+ // by their own pass, rooted at that path — see cmp/ExternalTarget for why
234
+ // a folder name cannot do this. The in-tree pass must not see them, or
235
+ // the consumer Root would also emit them into `<sdk-repo>/<target>/`.
236
+ //
237
+ // `folder` may be relative (a consumer's build passes '..' from `.sdk`),
238
+ // so resolve it ONCE: every destination is compared against it, and a
239
+ // comparison between a relative and an absolute path is meaningless.
240
+ const root = Path.resolve(folder)
241
+ const external = externalTargets(model, root)
242
+
243
+ // Before ANY file is written, in-tree included: a destination that turns
244
+ // out to be wrong must abort the whole generation, not leave half of it
245
+ // done. See checkExternalFolders.
246
+ checkExternalFolders(external, root, fs)
247
+
248
+ const jres = await jostraca.generate(
249
+ jopts, () => Root({ model: 0 === external.length ? model : withoutTargets(model, external) }))
228
250
 
229
251
  showChanges(jopts.log, 'generate-result', jres, Path.dirname(process.cwd()))
230
252
 
253
+ for (const ext of external) {
254
+ // `active: false` is the project's only lever to stop the generator
255
+ // writing into a repo it does not own, so it has to be honoured HERE —
256
+ // the consumer Root iterates targets raw and does not check it. The
257
+ // target is still removed from the in-tree model above (withoutTargets
258
+ // takes every `output: path` target, active or not), so switching one
259
+ // off generates it nowhere rather than relocating it into
260
+ // `<sdk-repo>/<target>/`.
261
+ if (!ext.active) {
262
+ log.info({
263
+ point: 'generate-external-skip', target: ext.name, folder: ext.folder,
264
+ note: ext.name + ' inactive, not generated'
265
+ })
266
+ continue
267
+ }
268
+
269
+ log.info({
270
+ point: 'generate-external', target: ext.name, folder: ext.folder,
271
+ note: ext.name + ' -> ' + ext.folder
272
+ })
273
+
274
+ const eres = await jostraca.generate(
275
+ { ...jopts, folder: ext.folder },
276
+ () => ExternalTarget({
277
+ model, target: ext.target, cmpfolder: folder,
278
+ // How to walk BACK to the SDK project from the destination. A
279
+ // target generating out of tree usually sits beside the SDK in a
280
+ // known layout, and its own docs, scripts and live tests need to
281
+ // name that path.
282
+ sdkrelpath: externalSdkRel(ext, root, log),
283
+ }))
284
+
285
+ showChanges(jopts.log, 'generate-result', eres, Path.dirname(process.cwd()))
286
+ }
287
+
231
288
  const dlogs = dlog.log()
232
289
  if (0 < dlogs.length) {
233
290
  for (let dlogentry of dlogs) {
@@ -377,6 +434,226 @@ SdkGen.makeBuild = async function(opts: SdkGenOptions) {
377
434
 
378
435
 
379
436
 
437
+ type ExternalSpec = {
438
+ name: string
439
+ target: any
440
+ folder: string
441
+ active: boolean
442
+ }
443
+
444
+
445
+ // Targets declaring `output: path` — generated into their own repo rather
446
+ // than into `<sdk-repo>/<target>/`.
447
+ //
448
+ // A relative path resolves against the SDK repo root, so a sibling checkout
449
+ // is '../<repo>'. That is deliberately the SAME base the generator writes
450
+ // everything else against: a path in the model should not depend on the
451
+ // directory the command happened to be run from.
452
+ //
453
+ // An INACTIVE target is still listed: it must be taken out of the in-tree
454
+ // model (see withoutTargets) so that switching it off does not silently
455
+ // relocate it into `<sdk-repo>/<target>/`. The generate loop skips it.
456
+ function externalTargets(model: any, folder: string): ExternalSpec[] {
457
+ const targets = model?.main?.[KIT]?.target || {}
458
+
459
+ return Object.keys(targets).sort()
460
+ .map((name: string) => ({ name, target: targets[name] }))
461
+ .filter((t: any) => {
462
+ const path = t.target?.output?.path
463
+ return null != path && '' !== path
464
+ })
465
+ .map((t: any) => ({
466
+ ...t,
467
+ folder: Path.resolve(folder, String(t.target.output.path)),
468
+ active: false !== t.target.active,
469
+ }))
470
+ }
471
+
472
+
473
+ // The `.jostraca` bookkeeping tree (meta log + a duplicate of the last
474
+ // generated output) that jostraca leaves at an output root. It is the only
475
+ // on-disk evidence that this toolchain has generated into a directory
476
+ // before, so it doubles as the OWNERSHIP MARKER: a destination carrying one
477
+ // has been generated into already, whoever set it up.
478
+ const EXTERNAL_MARKER = '.jostraca'
479
+
480
+ // Entries that do not count as content when deciding whether a destination
481
+ // is empty. A `git init` (or a clone of an empty repo) leaves only `.git`,
482
+ // and that is exactly the destination a FIRST generation is aimed at.
483
+ const EXTERNAL_EMPTY = ['.git', '.DS_Store']
484
+
485
+
486
+ // Refuse a destination the project cannot have meant.
487
+ //
488
+ // The external pass is the one code path that writes outside the repo it was
489
+ // pointed at, at a filesystem path taken verbatim from the model — and
490
+ // generation is overwrite, not merge, while jostraca's ensureDir creates
491
+ // missing parents. So a mistyped `output: path` does not fail: it fabricates
492
+ // a package tree at an arbitrary location, or overwrites an unrelated repo's
493
+ // package.json, README.md, LICENSE and CI workflow in place. The only trace
494
+ // was one INFO line naming the resolved folder.
495
+ //
496
+ // A destination must therefore be:
497
+ // - outside the SDK project, in BOTH directions — inside it is what a
498
+ // typo like '.' or 'ts' produces (and the external pass runs SECOND, so
499
+ // it wins over what the in-tree pass just wrote), while a destination
500
+ // that CONTAINS the project is what '..' produces;
501
+ // - claimed by no other target;
502
+ // - and either absent, empty, or carrying the marker a previous
503
+ // generation left there.
504
+ //
505
+ // Anything else is refused with both paths named. NOT skipped: a project
506
+ // generating somewhere other than it believes must be told. A destination
507
+ // that legitimately holds other content first (a repo seeded with a README
508
+ // and LICENCE) says so once in the model, with `output: adopt: true`.
509
+ function checkExternalFolders(external: ExternalSpec[], root: string, fs: any) {
510
+ const claimed: Record<string, string> = {}
511
+
512
+ for (const ext of external) {
513
+ // An inactive target writes nothing, so its destination is not a hazard —
514
+ // and switching a target off must not require keeping its now-unused
515
+ // path valid.
516
+ if (!ext.active) continue
517
+
518
+ const where = 'Target "' + ext.name + '" has output path "' +
519
+ ext.target.output.path + '", which resolves to: ' + ext.folder +
520
+ '\n (SDK project: ' + root + ')'
521
+
522
+ if (ext.folder === root || folderContains(root, ext.folder)) {
523
+ throw new SdkGenError(
524
+ 'External target output path is inside the SDK project.\n ' + where +
525
+ '\n A target generating into the SDK project must leave `output: ' +
526
+ 'path` unset — it is then generated in-tree, as <sdk-project>/' +
527
+ ext.name + '/.')
528
+ }
529
+
530
+ if (folderContains(ext.folder, root)) {
531
+ throw new SdkGenError(
532
+ 'External target output path contains the SDK project.\n ' + where +
533
+ '\n Generation would write this package over the directory holding ' +
534
+ 'the SDK project itself.')
535
+ }
536
+
537
+ if (null != claimed[ext.folder]) {
538
+ throw new SdkGenError(
539
+ 'External target output path is already claimed by target "' +
540
+ claimed[ext.folder] + '".\n ' + where +
541
+ '\n Two targets generating into the same folder overwrite each ' +
542
+ 'other, in target-name order.')
543
+ }
544
+ claimed[ext.folder] = ext.name
545
+
546
+ if (!fs.existsSync(ext.folder)) continue
547
+
548
+ if (!fs.statSync(ext.folder).isDirectory()) {
549
+ throw new SdkGenError(
550
+ 'External target output path is not a folder.\n ' + where)
551
+ }
552
+
553
+ if (true === ext.target.output.adopt) continue
554
+
555
+ const entries: string[] = fs.readdirSync(ext.folder)
556
+ .map((entry: any) => String(entry))
557
+
558
+ if (entries.includes(EXTERNAL_MARKER)) continue
559
+
560
+ const content = entries.filter((entry) => !EXTERNAL_EMPTY.includes(entry))
561
+
562
+ if (0 < content.length) {
563
+ throw new SdkGenError(
564
+ 'External target output folder already holds content this generator ' +
565
+ 'did not write.\n ' + where +
566
+ '\n Found: ' + content.slice(0, 8).join(', ') +
567
+ (8 < content.length ? ', ...' : '') +
568
+ '\n Generation OVERWRITES, so this would replace that content. ' +
569
+ 'Check the path;\n if the folder is right, declare it: `main: kit: ' +
570
+ 'target: \'' + ext.name + '\': output: adopt: true`.')
571
+ }
572
+ }
573
+ }
574
+
575
+
576
+ // Is `path` inside `folder`? Both must already be resolved. Path.relative
577
+ // rather than a string prefix, so that a sibling whose name merely STARTS
578
+ // with the folder's ('/x/sdk' vs '/x/sdk-provider') is not read as nested.
579
+ function folderContains(folder: string, path: string): boolean {
580
+ const rel = Path.relative(folder, path)
581
+ return '' !== rel && !rel.startsWith('..' + Path.sep) && '..' !== rel &&
582
+ !Path.isAbsolute(rel)
583
+ }
584
+
585
+
586
+ // The path from the destination back to the SDK project, which the target's
587
+ // own docs, scripts and live tests name (the companion test server lives in
588
+ // the SDK repo and is not published).
589
+ //
590
+ // It is DERIVED from the two resolved folders by default, which is only
591
+ // honest while the walk back crosses nothing the model does not name. It
592
+ // does not for `output: path: '../<repo>'`; it does for anything ascending
593
+ // further. voxgig-solardemo-sdk declares '../../seneca/solardemo-provider'
594
+ // and the derived inverse came out as '../../voxgig-sdk/voxgig-solardemo-sdk'
595
+ // — where `voxgig-sdk` is the name of the WORKSPACE DIRECTORY holding the SDK
596
+ // checkout on one machine, no part of the model. That string is committed
597
+ // into the destination's README.md and three test files, so a second
598
+ // developer with the same two repos under a differently named parent
599
+ // regenerates a spurious diff in tracked files and an instruction path that
600
+ // is wrong on the first machine.
601
+ //
602
+ // So a project that ascends further declares the walk back explicitly, as
603
+ // `output: sdkrel`, and is warned until it does.
604
+ function externalSdkRel(ext: ExternalSpec, root: string, log: any): string {
605
+ const declared = String(ext.target.output?.sdkrel || '')
606
+ if ('' !== declared) return declared
607
+
608
+ const derived = Path.relative(ext.folder, root).split(Path.sep).join('/')
609
+
610
+ // Every segment that is not '..' is a real directory name on the way back
611
+ // down to the SDK project. The LAST is the project's own folder; any
612
+ // earlier one is a directory ABOVE it, which nothing in the model declares.
613
+ const named = derived.split('/').filter((seg) => '..' !== seg)
614
+
615
+ if (1 < named.length) {
616
+ log.warn({
617
+ point: 'external-sdkrel-derived', target: ext.name, sdkrel: derived,
618
+ note: ext.name + ': path back to the SDK project derived as \'' +
619
+ derived + '\', which names ' + (named.length - 1) +
620
+ ' directory(s) above the SDK project that the model does not ' +
621
+ 'declare — this machine\'s layout will be committed to the ' +
622
+ 'generated files. Declare `output: sdkrel` instead.'
623
+ })
624
+ }
625
+
626
+ return derived
627
+ }
628
+
629
+
630
+ // The model the IN-TREE pass sees: the same model with the out-of-tree
631
+ // targets taken out.
632
+ //
633
+ // A shallow clone down to `target` only — the model is large, entities and
634
+ // features are shared with the external pass, and a deep copy would both
635
+ // cost and quietly break identity comparisons.
636
+ function withoutTargets(model: any, external: { name: string }[]): any {
637
+ const drop = new Set(external.map((e) => e.name))
638
+ const targets = model?.main?.[KIT]?.target || {}
639
+
640
+ const kept: any = {}
641
+ for (const name of Object.keys(targets)) {
642
+ if (!drop.has(name)) {
643
+ kept[name] = targets[name]
644
+ }
645
+ }
646
+
647
+ return {
648
+ ...model,
649
+ main: {
650
+ ...model.main,
651
+ [KIT]: { ...model.main[KIT], target: kept },
652
+ },
653
+ }
654
+ }
655
+
656
+
380
657
  // Adapted from https://github.com/sindresorhus/import-fresh - Thanks!
381
658
  function clear(path: string) {
382
659
  if (null == path) {
@@ -564,6 +841,8 @@ export {
564
841
  pkgDescription,
565
842
  nonAffiliation,
566
843
  keywords,
844
+ authorInfo,
845
+ contributorList,
567
846
  envName,
568
847
  envToken,
569
848
  goModule,
package/src/types.ts CHANGED
@@ -56,8 +56,31 @@ type ModelTarget = NameCases & {
56
56
  active?: boolean
57
57
  title?: string
58
58
  base?: string
59
- module?: { name?: string }
59
+ module?: { name?: string, path?: string, package?: string, goversion?: string }
60
60
  srcfeature?: boolean
61
+
62
+ // Where this target's files land. Present means OUT OF TREE: the target
63
+ // gets its own generate() pass rooted at `path` rather than a folder inside
64
+ // the SDK repo — see cmp/ExternalTarget and
65
+ // docs/explanation/out-of-tree-targets.
66
+ //
67
+ // Typed rather than left to the index signature because `externalTargets()`
68
+ // decides from these keys whether to write OUTSIDE the repo, and a
69
+ // destination path read off a bare `any` is one a rename can silently
70
+ // change to undefined.
71
+ output?: {
72
+ path?: string
73
+ repo?: string
74
+ adopt?: boolean
75
+ sdkrel?: string
76
+ }
77
+
78
+ // Per-generation-phase activation. A consumer target (go-cli, go-mcp,
79
+ // py-data, seneca-provider) switches every phase off and emits its whole
80
+ // package from Main. Absent — or present with no `active` — means the phase
81
+ // runs: the defaults are inclusive.
82
+ phase?: Record<string, { active?: boolean }>
83
+
61
84
  [extra: string]: any
62
85
  }
63
86
 
package/src/utility.ts CHANGED
@@ -6,8 +6,17 @@ import { JostracaResult } from 'jostraca'
6
6
  import { KIT, getModelPath } from '@voxgig/apidef'
7
7
 
8
8
 
9
+ // Where a per-target component is loaded from: `<project>/.sdk/dist/<path>`.
10
+ //
11
+ // `ctx$.folder` is jostraca's OUTPUT folder, which is the project for an
12
+ // ordinary target but the destination repo for one generating out of tree
13
+ // (`output: path`). Components always live in the project that owns the
14
+ // model, never in the place its files land, so an external pass sets
15
+ // `ctx$.cmpfolder` and this prefers it. Without that, generating out of tree
16
+ // looks for `<destination>/.sdk/dist/cmp/...` and fails to resolve.
9
17
  function resolvePath(ctx$: any, path: string): any {
10
- const fullpath = Path.join(ctx$.folder, '.sdk', 'dist', path)
18
+ const base = null == ctx$.cmpfolder ? ctx$.folder : ctx$.cmpfolder
19
+ const fullpath = Path.join(base, '.sdk', 'dist', path)
11
20
  return fullpath
12
21
  }
13
22