@voxgig/sdkgen 3.6.0 → 3.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 (100) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/AgentGuideTop.js +17 -0
  3. package/dist/cmp/AgentGuideTop.js.map +1 -1
  4. package/dist/cmp/Readme.js +2 -0
  5. package/dist/cmp/Readme.js.map +1 -1
  6. package/dist/cmp/ReadmeStation.d.ts +2 -0
  7. package/dist/cmp/ReadmeStation.js +57 -0
  8. package/dist/cmp/ReadmeStation.js.map +1 -0
  9. package/dist/helpers/naming.d.ts +3 -1
  10. package/dist/helpers/naming.js +39 -2
  11. package/dist/helpers/naming.js.map +1 -1
  12. package/dist/sdkgen.d.ts +2 -2
  13. package/dist/sdkgen.js +4 -2
  14. package/dist/sdkgen.js.map +1 -1
  15. package/dist/tsconfig.tsbuildinfo +1 -1
  16. package/dist/utility.d.ts +1 -1
  17. package/dist/utility.js +16 -2
  18. package/dist/utility.js.map +1 -1
  19. package/package.json +1 -1
  20. package/project/.sdk/src/cmp/c/Config_c.ts +34 -13
  21. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +9 -46
  22. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +7 -1
  23. package/project/.sdk/src/cmp/dart/Config_dart.ts +14 -2
  24. package/project/.sdk/src/cmp/dart/fragment/Config.data.fragment.dart +5 -0
  25. package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +6 -0
  26. package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +13 -4
  27. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +12 -2
  28. package/project/.sdk/src/cmp/elixir/Package_elixir.ts +41 -2
  29. package/project/.sdk/src/cmp/go/Config_go.ts +8 -1
  30. package/project/.sdk/src/cmp/java/Config_java.ts +8 -1
  31. package/project/.sdk/src/cmp/js/Config_js.ts +9 -1
  32. package/project/.sdk/src/cmp/js/fragment/Config.data.fragment.js +7 -0
  33. package/project/.sdk/src/cmp/js/fragment/Config.fragment.js +8 -0
  34. package/project/.sdk/src/cmp/js/fragment/Main.fragment.js +33 -5
  35. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +8 -1
  36. package/project/.sdk/src/cmp/lua/Config_lua.ts +11 -2
  37. package/project/.sdk/src/cmp/lua/Package_lua.ts +24 -1
  38. package/project/.sdk/src/cmp/perl/Config_perl.ts +8 -60
  39. package/project/.sdk/src/cmp/php/Config_php.ts +11 -2
  40. package/project/.sdk/src/cmp/php/Main_php.ts +25 -0
  41. package/project/.sdk/src/cmp/php/fragment/Main.fragment.php +23 -6
  42. package/project/.sdk/src/cmp/py/Config_py.ts +11 -2
  43. package/project/.sdk/src/cmp/py/Main_py.ts +17 -7
  44. package/project/.sdk/src/cmp/py/fragment/Main.fragment.py +23 -6
  45. package/project/.sdk/src/cmp/rb/Config_rb.ts +7 -1
  46. package/project/.sdk/src/cmp/rust/Config_rust.ts +5 -2
  47. package/project/.sdk/src/cmp/swift/Config_swift.ts +8 -54
  48. package/project/.sdk/src/cmp/swift/Package_swift.ts +52 -10
  49. package/project/.sdk/src/cmp/ts/Config_ts.ts +9 -1
  50. package/project/.sdk/src/cmp/ts/EntityTypes_ts.ts +5 -2
  51. package/project/.sdk/src/cmp/ts/Entity_ts.ts +8 -2
  52. package/project/.sdk/src/cmp/ts/fragment/Config.data.fragment.ts +7 -0
  53. package/project/.sdk/src/cmp/ts/fragment/Config.fragment.ts +8 -0
  54. package/project/.sdk/src/cmp/ts/fragment/Entity.fragment.ts +1 -1
  55. package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +12 -4
  56. package/project/.sdk/tm/c/Makefile +6 -1
  57. package/project/.sdk/tm/c/utility/make_options.c +12 -1
  58. package/project/.sdk/tm/cpp/utility/pipeline.hpp +9 -1
  59. package/project/.sdk/tm/csharp/utility/Fetcher.cs +23 -5
  60. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +13 -0
  61. package/project/.sdk/tm/dart/lib/utility/FetcherUtility.dart +9 -0
  62. package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +17 -0
  63. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +39 -3
  64. package/project/.sdk/tm/elixir/test/feature_test.exs +19 -0
  65. package/project/.sdk/tm/elixir/test/primary_utility_test.exs +72 -0
  66. package/project/.sdk/tm/go/test/primary_utility_test.go +42 -20
  67. package/project/.sdk/tm/go/test/runner_test.go +1 -1
  68. package/project/.sdk/tm/go/utility/fetcher.go +14 -0
  69. package/project/.sdk/tm/go/utility/make_options.go +31 -4
  70. package/project/.sdk/tm/java/utility/Fetcher.java +11 -1
  71. package/project/.sdk/tm/java/utility/MakeOptions.java +11 -0
  72. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +20 -2
  73. package/project/.sdk/tm/kotlin/utility/Fetcher.kt +10 -1
  74. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +11 -0
  75. package/project/.sdk/tm/lean/test/TPrimaryUtility.lean +13 -2
  76. package/project/.sdk/tm/lua/utility/make_options.lua +44 -0
  77. package/project/.sdk/tm/perl/utility/fetcher.pm +10 -1
  78. package/project/.sdk/tm/perl/utility/make_options.pm +36 -0
  79. package/project/.sdk/tm/php/test/PipelineTest.php +20 -0
  80. package/project/.sdk/tm/php/utility/Fetcher.php +12 -1
  81. package/project/.sdk/tm/php/utility/MakeOptions.php +58 -0
  82. package/project/.sdk/tm/py/pkg/utility/fetcher.py +13 -1
  83. package/project/.sdk/tm/py/pkg/utility/make_options.py +23 -0
  84. package/project/.sdk/tm/rb/utility/make_options.rb +18 -0
  85. package/project/.sdk/tm/rust/tests/common/mod.rs +1 -1
  86. package/project/.sdk/tm/rust/tests/feature_test.rs +27 -0
  87. package/project/.sdk/tm/rust/utility/fetcher.rs +8 -0
  88. package/project/.sdk/tm/rust/utility/make_options.rs +15 -0
  89. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +24 -1
  90. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +12 -0
  91. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +20 -2
  92. package/project/.sdk/tm/ts/test/utility/Corpus.test.ts +85 -0
  93. package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +10 -17
  94. package/project/sdkgen-package.json +1 -1
  95. package/src/cmp/AgentGuideTop.ts +18 -0
  96. package/src/cmp/Readme.ts +2 -0
  97. package/src/cmp/ReadmeStation.ts +69 -0
  98. package/src/helpers/naming.ts +45 -2
  99. package/src/sdkgen.ts +3 -1
  100. package/src/utility.ts +17 -2
@@ -31,6 +31,7 @@ import {
31
31
  // instantiate features named in the options (mirrors Config_rust).
32
32
  const Config = cmp(async function Config(props: any) {
33
33
  const ctx$ = props.ctx$
34
+ const target = props.target
34
35
 
35
36
  const model: Model = ctx$.model
36
37
 
@@ -72,20 +73,25 @@ const Config = cmp(async function Config(props: any) {
72
73
  relations: n.relations,
73
74
  }, true), a), {})
74
75
 
76
+ // The same config as an OBJECT, built by the shared helper so this target's
77
+ // literal and the data that replaces it above the threshold are the same
78
+ // config by construction. The JSON is what the threshold is measured on -
79
+ // emitted source size varies by language, the model does not. Passing
80
+ // target.name opts this target into the main slug/version/target identity
81
+ // fields (read by station's descriptor - see configDefinition).
82
+ const { def: configDef, json: configJson } = configDefinition(model, target.name)
83
+ const asData = isConfigData(configJson, configReprSetting(model))
84
+
75
85
  const config = {
76
- main: { name: model.const.Name },
86
+ // main from configDefinition's def, not re-derived here, so the literal
87
+ // rep and the data rep cannot disagree on identity (the Config_ts
88
+ // #MainMeta discipline): name plus slug/version/target.
89
+ main: configDef.main,
77
90
  feature: featureConfig,
78
91
  options,
79
92
  entity: entityConfig,
80
93
  }
81
94
 
82
- // The same config as an OBJECT, built by the shared helper so this target's
83
- // literal and the data that replaces it above the threshold are the same
84
- // config by construction. The JSON is what the threshold is measured on -
85
- // emitted source size varies by language, the model does not.
86
- const { json: configJson } = configDefinition(model)
87
- const asData = isConfigData(configJson, configReprSetting(model))
88
-
89
95
  File({ name: 'config.c' }, () => {
90
96
 
91
97
  // ABOVE THE THRESHOLD: emit the model as DATA.
@@ -137,8 +143,6 @@ voxgig_value* shared_config(void) {
137
143
  }
138
144
  return shared_config_val;
139
145
  }
140
-
141
- Feature* make_feature(const char* name) {
142
146
  `)
143
147
  }
144
148
  else {
@@ -174,8 +178,6 @@ voxgig_value* shared_config(void) {
174
178
  }
175
179
  return shared_config_val;
176
180
  }
177
-
178
- Feature* make_feature(const char* name) {
179
181
  `)
180
182
  }
181
183
 
@@ -192,7 +194,26 @@ Feature* make_feature(const char* name) {
192
194
  each(feature, (f: any) => {
193
195
  if (f.name && f.name !== 'base') featureNames.add(f.name)
194
196
  })
195
- for (const fname of Array.from(featureNames).sort()) {
197
+ const sortedNames = Array.from(featureNames).sort()
198
+
199
+ // Constructor prototypes for every declared feature. sdk.h declares the
200
+ // BUNDLED set, but an externally-installed feature (e.g. station, whose
201
+ // source arrives by package overlay) is not in sdk.h — and without a
202
+ // prototype its make_feature arm is an implicit int-returning
203
+ // declaration: a warning that happens to link on gcc 13, a hard error on
204
+ // gcc 14+/clang 15+. Redeclaring the bundled ones is identical-prototype
205
+ // C, which is legal and keeps this a single sorted list.
206
+ Content(`
207
+ `)
208
+ for (const fname of sortedNames) {
209
+ Content(`Feature* feature_${fname}_new(void);
210
+ `)
211
+ }
212
+
213
+ Content(`
214
+ Feature* make_feature(const char* name) {
215
+ `)
216
+ for (const fname of sortedNames) {
196
217
  Content(` if (strcmp(name, "${fname}") == 0) return feature_${fname}_new();
197
218
  `)
198
219
  }
@@ -3,9 +3,8 @@ import {
3
3
  Content,
4
4
  File,
5
5
  cmp,
6
+ configDefinition,
6
7
  each,
7
- isAuthActive,
8
- resolveAuthPrefix,
9
8
  } from '@voxgig/sdkgen'
10
9
 
11
10
 
@@ -17,7 +16,6 @@ import {
17
16
 
18
17
 
19
18
  import {
20
- cleanModel,
21
19
  cppConfigLiterals,
22
20
  } from './utility_cpp'
23
21
 
@@ -32,50 +30,15 @@ const Config = cmp(async function Config(props: any) {
32
30
 
33
31
  const model: Model = ctx$.model
34
32
 
35
- const entity = getModelPath(model, `main.${KIT}.entity`)
36
33
  const feature = getModelPath(model, `main.${KIT}.feature`)
37
34
 
38
- const headers = getModelPath(model, `main.${KIT}.config.headers`) || {}
39
-
40
- const authActive = isAuthActive(model)
41
- const authPrefix = resolveAuthPrefix(model)
42
-
43
- let baseUrl = ''
44
- try { baseUrl = getModelPath(model, `main.${KIT}.info.servers.0.url`) } catch (_e) { }
45
-
46
- const featureConfig: Record<string, any> = {}
47
- each(feature, (f: any) => {
48
- featureConfig[f.name] = cleanModel(f.config || {})
49
- })
50
-
51
- const optionsEntity: Record<string, any> = {}
52
- each(entity, (ent: any) => {
53
- optionsEntity[ent.name] = {}
54
- })
55
-
56
- const options: Record<string, any> = {
57
- base: baseUrl,
58
- }
59
- if (authActive) {
60
- options.auth = { prefix: authPrefix }
61
- }
62
- options.headers = headers
63
- options.entity = optionsEntity
64
-
65
- const entityConfig = Object.values(entity || {}).reduce((a: any, n: any) => (
66
- a[n.name] = cleanModel({
67
- fields: n.fields,
68
- name: n.name,
69
- op: n.op,
70
- relations: n.relations,
71
- }, true), a), {})
72
-
73
- const config = {
74
- main: { name: model.const.Name },
75
- feature: featureConfig,
76
- options,
77
- entity: entityConfig,
78
- }
35
+ // The embedded config, built by the shared helper so this target's shapes
36
+ // and identity fields stay in step with the ts reference by construction.
37
+ // Passing target.name opts cpp into main.slug/version/target (the station
38
+ // descriptor identity, ts/src/utility.ts configDefinition) - cpp has only
39
+ // the data rep (one chunked JSON literal), so this is the whole #MainMeta
40
+ // story for this target.
41
+ const { def: configDef } = configDefinition(model, target.name)
79
42
 
80
43
  File({ name: 'config.' + target.ext }, () => {
81
44
 
@@ -104,7 +67,7 @@ namespace sdk {
104
67
 
105
68
  inline const char* config_json() {
106
69
  return
107
- ${cppConfigLiterals(config)};
70
+ ${cppConfigLiterals(configDef)};
108
71
  }
109
72
 
110
73
  inline Value makeConfig() { return vs::parse_json(config_json()); }
@@ -39,7 +39,13 @@ const Config = cmp(async function Config(props: any) {
39
39
  // literal and the data that replaces it above the threshold are the same
40
40
  // config by construction. The JSON is what the threshold is measured on -
41
41
  // emitted source size varies by language, the model does not.
42
- const { def: configDef, json: configJson } = configDefinition(model)
42
+ //
43
+ // Passing target.name opts this target into main.slug / main.version /
44
+ // main.target (the three station descriptor fields, station design §4):
45
+ // both reps flow from this one call - the literal via formatCsMap(configDef)
46
+ // and the data rep via csStringLiteral(configJson) - so they cannot
47
+ // disagree on identity (mirrors Config_ts's #MainMeta block).
48
+ const { def: configDef, json: configJson } = configDefinition(model, target.name)
43
49
  const asData = isConfigData(configJson, configReprSetting(model))
44
50
 
45
51
  File({ name: 'Config.' + target.ext }, () => {
@@ -71,8 +71,10 @@ const Config = cmp(async function Config(props: any) {
71
71
  // The same config as an OBJECT, built by the shared helper so this target's
72
72
  // literal and the data that replaces it above the threshold are the same
73
73
  // config by construction. The JSON is what the threshold is measured on -
74
- // emitted source size varies by language, the model does not.
75
- const { def: configDef, json: configJson } = configDefinition(model)
74
+ // emitted source size varies by language, the model does not. Passing
75
+ // target.name opts this target into the main slug/version/target identity
76
+ // fields (read by station's descriptor - see configDefinition).
77
+ const { def: configDef, json: configJson } = configDefinition(model, target.name)
76
78
  const asData = isConfigData(configJson, configReprSetting(model))
77
79
 
78
80
  File({ name: 'Config.' + target.ext }, () => {
@@ -115,6 +117,16 @@ const Config = cmp(async function Config(props: any) {
115
117
  // these; this one did not.
116
118
  ...ctx$.stdrep,
117
119
 
120
+ // Identity beyond the camel Name: slug/version/target (station
121
+ // descriptor inputs). Values from configDefinition's def, not
122
+ // re-derived here, so the literal rep and the data rep cannot
123
+ // disagree (the Config_ts #MainMeta discipline).
124
+ '// #MainMeta': () => {
125
+ Line(` 'slug': ${dartValue(configDef.main.slug)},`)
126
+ Line(` 'version': ${dartValue(configDef.main.version)},`)
127
+ Line(` 'target': ${dartValue(configDef.main.target)},`)
128
+ },
129
+
118
130
  // The whole options map from the canonical definition. Assembling it
119
131
  // slot by slot lost `options.server` entirely, so a spec with a
120
132
  // templated server URL described a different config either side of the
@@ -52,6 +52,11 @@ class Config {
52
52
  return fc();
53
53
  }
54
54
 
55
+ // False for a feature added at runtime via options.extend (station's
56
+ // adopt path) - the constructor uses this to skip makeFeature for names
57
+ // no generated class backs.
58
+ bool hasFeature(String fn) => null != FEATURE_CLASS[fn];
59
+
55
60
  // The same fields the literal declares - same names, same types, same
56
61
  // finalness - so callers cannot tell which representation they were given.
57
62
  final Map<String, dynamic> main;
@@ -16,8 +16,14 @@ class Config {
16
16
  return fc();
17
17
  }
18
18
 
19
+ // False for a feature added at runtime via options.extend (station's
20
+ // adopt path) - the constructor uses this to skip makeFeature for names
21
+ // no generated class backs.
22
+ bool hasFeature(String fn) => null != FEATURE_CLASS[fn];
23
+
19
24
  final Map<String, dynamic> main = <String, dynamic>{
20
25
  'name': 'ProjectName',
26
+ // #MainMeta
21
27
  };
22
28
 
23
29
  final Map<String, dynamic> feature = <String, dynamic>{
@@ -69,19 +69,28 @@ class ProjectNameSDK {
69
69
  // `test` feature installs the base mock transport and the transport
70
70
  // features (retry/cache/netsim/proxy/ratelimit) wrap whatever is current,
71
71
  // so `test` must be added before them to sit at the base of the chain.
72
+ final List extendList =
73
+ _options['extend'] is List ? _options['extend'] : [];
74
+
72
75
  final featureorder =
73
76
  struct.getpath(_options, '__derived__.featureorder') ?? [];
74
77
  for (final fname in featureorder) {
75
78
  final fopts = _options['feature'][fname];
76
79
  if (fopts is Map && true == fopts['active']) {
80
+ // An active name with no generated class is legal when an
81
+ // extend-supplied instance carries that name (station's adopt
82
+ // path): the instance is added below, positioned by its own
83
+ // __after__ entry, so skip it here rather than fail construction.
84
+ if (!config.hasFeature(fname.toString()) &&
85
+ extendList.any((f) => fname.toString() == f.name.toString())) {
86
+ continue;
87
+ }
77
88
  featureAdd(rootctx, config.makeFeature(fname.toString()));
78
89
  }
79
90
  }
80
91
 
81
- if (null != _options['extend']) {
82
- for (final f in _options['extend']) {
83
- featureAdd(rootctx, f);
84
- }
92
+ for (final f in extendList) {
93
+ featureAdd(rootctx, f);
85
94
  }
86
95
 
87
96
  for (final f in features) {
@@ -29,6 +29,7 @@ import {
29
29
 
30
30
  const Config = cmp(async function Config(props: any) {
31
31
  const ctx$ = props.ctx$
32
+ const target = props.target
32
33
  const model: Model = ctx$.model
33
34
 
34
35
  const Name = model.const.Name
@@ -60,7 +61,11 @@ const Config = cmp(async function Config(props: any) {
60
61
  // target's literal and the data that replaces it above the threshold are
61
62
  // the same config by construction. The JSON is what the threshold is
62
63
  // measured on - emitted source size varies by language, the model does not.
63
- const { def: configDef, json: configJson } = configDefinition(model)
64
+ // Passing target.name opts in to the main slug/version/target identity
65
+ // fields (station descriptor input, mirrors Config_ts) - both reps below
66
+ // render from this same def, so the data and literal branches pick the
67
+ // fields up together.
68
+ const { def: configDef, json: configJson } = configDefinition(model, target.name)
64
69
  const asData = isConfigData(configJson, configReprSetting(model))
65
70
 
66
71
  File({ name: 'config.ex' }, () => {
@@ -159,7 +164,12 @@ end
159
164
  defmodule ${Name}.Config do
160
165
  def make_config do
161
166
  ${Name}.Helpers.deep(%{
162
- "main" => %{"name" => ${elixirString(Name)}},
167
+ "main" => %{
168
+ "name" => ${elixirString(Name)},
169
+ "slug" => ${elixirString(String(configDef.main.slug))},
170
+ "version" => ${elixirString(String(configDef.main.version))},
171
+ "target" => ${elixirString(String(configDef.main.target))}
172
+ },
163
173
  "feature" => %{
164
174
  `)
165
175
 
@@ -3,6 +3,7 @@ import {
3
3
  Content,
4
4
  File,
5
5
  cmp,
6
+ collectDeps,
6
7
  pkgDescription,
7
8
  repoInfo,
8
9
  packageVersion,
@@ -15,7 +16,11 @@ import type {
15
16
 
16
17
 
17
18
  // Generate mix.exs for the Elixir SDK. The vendored struct library has zero
18
- // third-party runtime deps, so the SDK ships with none either.
19
+ // third-party runtime deps, so a bare SDK ships with none - but dependencies
20
+ // declared by the model DO flow: the target's own `deps` block plus any
21
+ // feature-declared elixir deps (e.g. the station feature's voxgig_station),
22
+ // via the shared collectDeps helper, exactly like rust/dart/go. A model with
23
+ // no deps emits the byte-identical `defp deps, do: []` it always has.
19
24
  const Package = cmp(async function Package(props: any) {
20
25
  const ctx$ = props.ctx$
21
26
  const target = props.target
@@ -27,6 +32,31 @@ const Package = cmp(async function Package(props: any) {
27
32
  const app = String(model.const.name).replace(/-/g, '_')
28
33
  const { repoUrl } = repoInfo(model)
29
34
 
35
+ // Render one mix deps entry per collected dependency. Hex package names
36
+ // share the atom alphabet, but a hyphen in a model dep name is mapped to
37
+ // '_' rather than emitted broken. Version -> Mix requirement: an explicit
38
+ // operator is kept (spaced, the mix convention: '>=0.0.1' -> '>= 0.0.1');
39
+ // a bare version gets hex's customary '~>'; a target dep with no version
40
+ // accepts anything. kind: 'dev' scopes to [:dev, :test]; 'peer'/'prod'
41
+ // (and anything else) are runtime deps - mix has no peer notion.
42
+ const entries: string[] = []
43
+ for (const d of collectDeps(model, target.name, target.deps, ctx$.log)) {
44
+ const atom = String(d.name).replace(/-/g, '_')
45
+ const version = null == d.version || '' === String(d.version).trim()
46
+ ? '>= 0.0.0'
47
+ : mixRequirement(String(d.version).trim())
48
+ const scope = 'dev' === (d.raw as any)?.kind ? ', only: [:dev, :test]' : ''
49
+ entries.push(`{:${atom}, ${JSON.stringify(version)}${scope}}`)
50
+ }
51
+
52
+ const depsBlock = 0 === entries.length
53
+ ? ` defp deps, do: []`
54
+ : ` defp deps do
55
+ [
56
+ ${entries.join(',\n ')}
57
+ ]
58
+ end`
59
+
30
60
  File({ name: 'mix.exs' }, () => {
31
61
  Content(`defmodule ${Name}.MixProject do
32
62
  use Mix.Project
@@ -46,7 +76,7 @@ const Package = cmp(async function Package(props: any) {
46
76
 
47
77
  def application, do: [extra_applications: [:inets, :ssl]]
48
78
 
49
- defp deps, do: []
79
+ ${depsBlock}
50
80
 
51
81
  defp elixirc_paths(:test), do: ["lib", "test/support"]
52
82
  defp elixirc_paths(_), do: ["lib"]
@@ -63,6 +93,15 @@ end
63
93
  })
64
94
 
65
95
 
96
+ function mixRequirement(version: string): string {
97
+ const m = version.match(/^(>=|<=|~>|==|!=|>|<)\s*(.+)$/)
98
+ if (null != m) {
99
+ return m[1] + ' ' + m[2]
100
+ }
101
+ return '~> ' + version
102
+ }
103
+
104
+
66
105
  export {
67
106
  Package
68
107
  }
@@ -69,7 +69,7 @@ const Config = cmp(async function Config(props: any) {
69
69
  // literal and the data that replaces it above the threshold are the same
70
70
  // config by construction. The JSON is what the threshold is measured on -
71
71
  // emitted source size varies by language, the model does not.
72
- const { json: configJson } = configDefinition(model)
72
+ const { def: configDef, json: configJson } = configDefinition(model, target.name)
73
73
  const asData = isConfigData(configJson, configReprSetting(model))
74
74
 
75
75
  File({ name: 'config.' + target.ext }, () => {
@@ -154,6 +154,10 @@ import (
154
154
 
155
155
  `)
156
156
 
157
+ // main slug/version/target come from configDefinition's def, not
158
+ // re-derived here, so the literal rep and the data rep cannot disagree
159
+ // on identity (mirrors Config_ts's #MainMeta block; station's
160
+ // descriptor reads all three).
157
161
  Content(`// MakeConfig builds a fresh, fully materialised config map. Every call
158
162
  // rebuilds the whole structure, so prefer SharedConfig unless you need a
159
163
  // private copy you intend to mutate.
@@ -161,6 +165,9 @@ func MakeConfig() map[string]any {
161
165
  return map[string]any{
162
166
  "main": map[string]any{
163
167
  "name": "${model.const.Name}",
168
+ "slug": ${JSON.stringify(configDef.main.slug)},
169
+ "version": ${JSON.stringify(configDef.main.version)},
170
+ "target": ${JSON.stringify(configDef.main.target)},
164
171
  },
165
172
  "feature": map[string]any{
166
173
  `)
@@ -3,6 +3,7 @@ import {
3
3
  Content,
4
4
  File,
5
5
  cmp,
6
+ configDefinition,
6
7
  each,
7
8
  isAuthActive,
8
9
  resolveAuthPrefix,
@@ -75,8 +76,14 @@ const Config = cmp(async function Config(props: any) {
75
76
  relations: n.relations,
76
77
  }, true), a), {})
77
78
 
79
+ // Identity comes from configDefinition's def, not re-derived here, so
80
+ // this target cannot disagree with the shared emitter on main.slug /
81
+ // main.version / main.target (the three station descriptor fields,
82
+ // station design §4) — passing target.name is what opts this target in.
83
+ const { def: configDef } = configDefinition(model, target.name)
84
+
78
85
  const config = {
79
- main: { name: model.const.Name },
86
+ main: configDef.main,
80
87
  feature: featureConfig,
81
88
  options,
82
89
  entity: entityConfig,
@@ -81,7 +81,7 @@ const Config = cmp(async function Config(props: any) {
81
81
  // literal and the data that replaces it above the threshold are the same
82
82
  // config by construction. The JSON is what the threshold is measured on -
83
83
  // emitted source size varies by language, the model does not.
84
- const { json: configJson } = configDefinition(model)
84
+ const { def: configDef, json: configJson } = configDefinition(model, target.name)
85
85
  const asData = isConfigData(configJson, configReprSetting(model))
86
86
 
87
87
  File({ name: 'Config.' + target.ext }, () => {
@@ -128,6 +128,14 @@ const Config = cmp(async function Config(props: any) {
128
128
  `require('./feature/${f.name}/${nom(f, 'Name')}Feature')`)
129
129
  }),
130
130
 
131
+ // Values from configDefinition's def, not re-derived here, so the
132
+ // literal rep and the data rep cannot disagree on identity.
133
+ '// #MainMeta': () => {
134
+ Line(` slug: ${JSON.stringify(configDef.main.slug)},`)
135
+ Line(` version: ${JSON.stringify(configDef.main.version)},`)
136
+ Line(` target: ${JSON.stringify(configDef.main.target)},`)
137
+ },
138
+
131
139
  '// #FeatureClasses': () => each(feature, (f: any) => {
132
140
  // Trailing comma: the map has one entry per feature, so entries
133
141
  // must be comma-separated (a single feature hid this until now).
@@ -34,6 +34,13 @@ class Config {
34
34
  return fi
35
35
  }
36
36
 
37
+ // False for a feature added at runtime via options.extend (station's
38
+ // adopt path) - the constructor uses this to skip makeFeature for names
39
+ // no generated class backs.
40
+ hasFeature(fn) {
41
+ return null != FEATURE_CLASS[fn]
42
+ }
43
+
37
44
  }
38
45
 
39
46
 
@@ -17,9 +17,17 @@ class Config {
17
17
  return fi
18
18
  }
19
19
 
20
+ // False for a feature added at runtime via options.extend (station's
21
+ // adopt path) - the constructor uses this to skip makeFeature for names
22
+ // no generated class backs.
23
+ hasFeature(fn) {
24
+ return null != FEATURE_CLASS[fn]
25
+ }
26
+
20
27
 
21
28
  main = {
22
29
  name: '$$const.Name$$',
30
+ // #MainMeta
23
31
  }
24
32
 
25
33
 
@@ -50,18 +50,26 @@ class ProjectNameSDK {
50
50
  // the `test` feature installs the base mock transport and the transport
51
51
  // features (retry/cache/netsim/proxy/ratelimit) wrap whatever is current,
52
52
  // so `test` must be added before them to sit at the base of the chain.
53
+ const extend = this._options.extend || []
54
+
53
55
  const featureorder = getpath(this._options, '__derived__.featureorder') || []
54
56
  for (const fname of featureorder) {
55
57
  const fopts = this._options.feature[fname] || {}
56
58
  if (fopts.active) {
59
+ // An active name with no generated class is legal when an
60
+ // extend-supplied instance carries that name (station's adopt
61
+ // path): the instance is added below, positioned by its own
62
+ // __after__ entry, so skip it here rather than fail construction.
63
+ if (!this._rootctx.config.hasFeature(fname) &&
64
+ extend.some((f) => fname === f.name)) {
65
+ continue
66
+ }
57
67
  featureAdd(this._rootctx, this._rootctx.config.makeFeature(fname))
58
68
  }
59
69
  }
60
70
 
61
- if (null != this._options.extend) {
62
- for (let f of this._options.extend) {
63
- featureAdd(this._rootctx, f)
64
- }
71
+ for (let f of extend) {
72
+ featureAdd(this._rootctx, f)
65
73
  }
66
74
 
67
75
  for (let f of this._features) {
@@ -185,7 +193,27 @@ class ProjectNameSDK {
185
193
  }
186
194
 
187
195
  const status = fetched.status
188
- const json = 'function' === typeof fetched.json ? await fetched.json() : fetched.json
196
+
197
+ // No body responses (204 No Content, 304 Not Modified) and explicit
198
+ // zero content-length must skip JSON parsing — fetched.json() would
199
+ // throw `Unexpected end of JSON input` on an empty body.
200
+ const headers = fetched.headers
201
+ const contentLength = headers && 'function' === typeof headers.get
202
+ ? headers.get('content-length')
203
+ : (headers || {})['content-length']
204
+ const noBody = 204 === status || 304 === status || '0' === String(contentLength)
205
+
206
+ let json = undefined
207
+ if (!noBody) {
208
+ try {
209
+ json = 'function' === typeof fetched.json ? await fetched.json() : fetched.json
210
+ }
211
+ catch (parseErr) {
212
+ // Body wasn't valid JSON — surface the raw response rather than
213
+ // throwing. data stays undefined; callers can inspect status/headers.
214
+ json = undefined
215
+ }
216
+ }
189
217
 
190
218
  return {
191
219
  ok: status >= 200 && status < 300,
@@ -3,6 +3,7 @@ import {
3
3
  Content,
4
4
  File,
5
5
  cmp,
6
+ configDefinition,
6
7
  each,
7
8
  isAuthActive,
8
9
  resolveAuthPrefix,
@@ -72,8 +73,14 @@ const Config = cmp(async function Config(props: any) {
72
73
  relations: n.relations,
73
74
  }, true), a), {})
74
75
 
76
+ // Identity comes from configDefinition's def, not re-derived here, so
77
+ // this target cannot disagree with the shared emitter on main.slug /
78
+ // main.version / main.target (the three station descriptor fields,
79
+ // station design §4) — passing target.name is what opts this target in.
80
+ const { def: configDef } = configDefinition(model, target.name)
81
+
75
82
  const config = {
76
- main: { name: model.const.Name },
83
+ main: configDef.main,
77
84
  feature: featureConfig,
78
85
  options,
79
86
  entity: entityConfig,
@@ -68,8 +68,11 @@ const Config = cmp(async function Config(props: any) {
68
68
  // The same config as an OBJECT, built by the shared helper so this target's
69
69
  // literal and the data that replaces it above the threshold are the same
70
70
  // config by construction. The JSON is what the threshold is measured on -
71
- // emitted source size varies by language, the model does not.
72
- const { json: configJson } = configDefinition(model)
71
+ // emitted source size varies by language, the model does not. Passing the
72
+ // target name opts this target into the main slug/version/target identity
73
+ // fields (station descriptor v1 reads all three) - the literal branch
74
+ // below emits them too, so the two representations cannot diverge.
75
+ const { def: configDef, json: configJson } = configDefinition(model, target.name)
73
76
  const asData = isConfigData(configJson, configReprSetting(model))
74
77
 
75
78
  File({ name: 'config.' + target.ext }, () => {
@@ -112,6 +115,9 @@ end
112
115
  }
113
116
  else {
114
117
 
118
+ // Identity values from configDefinition's def, not re-derived here, so
119
+ // the literal rep and the data rep cannot disagree (the ts #MainMeta
120
+ // discipline).
115
121
  Content(`-- Build a fresh, fully materialised config table. Every call rebuilds the
116
122
  -- whole structure, so prefer require("config_shared") unless you need a
117
123
  -- private copy you intend to mutate.
@@ -119,6 +125,9 @@ local function make_config()
119
125
  return {
120
126
  main = {
121
127
  name = "${model.const.Name}",
128
+ slug = ${JSON.stringify(configDef.main.slug)},
129
+ version = ${JSON.stringify(configDef.main.version)},
130
+ target = ${JSON.stringify(configDef.main.target)},
122
131
  },
123
132
  feature = {
124
133
  `)