@voxgig/sdkgen 3.4.3 → 3.4.5
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.
- package/dist/cmp/ReadmeTop.js +10 -1
- package/dist/cmp/ReadmeTop.js.map +1 -1
- package/dist/sdkgen.d.ts +2 -2
- package/dist/sdkgen.js +6 -2
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.d.ts +5 -1
- package/dist/utility.js +62 -1
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.aontu +11 -0
- package/package.json +1 -1
- package/project/.sdk/src/cmp/go/Config_go.ts +127 -0
- package/project/.sdk/src/cmp/ts/Config_ts.ts +88 -0
- package/project/.sdk/src/cmp/ts/fragment/Config.data.fragment.ts +50 -0
- package/project/.sdk/src/cmp/ts/fragment/Config.fragment.ts +1 -1
- package/src/cmp/ReadmeTop.ts +10 -1
- package/src/sdkgen.ts +6 -1
- package/src/utility.ts +69 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
cmp,
|
|
11
11
|
each,
|
|
12
12
|
isAuthActive,
|
|
13
|
+
isConfigData,
|
|
13
14
|
resolveAuthPrefix,
|
|
14
15
|
serverVariables,
|
|
15
16
|
} from '@voxgig/sdkgen'
|
|
@@ -63,8 +64,131 @@ const Config = cmp(async function Config(props: any) {
|
|
|
63
64
|
: ''
|
|
64
65
|
|
|
65
66
|
// Config is now in core/ package
|
|
67
|
+
// The same config as an OBJECT. Built regardless of which representation
|
|
68
|
+
// wins, because the JSON is what the threshold is measured on - the emitted
|
|
69
|
+
// source size varies by language, the model does not.
|
|
70
|
+
const entityDefs: any = {}
|
|
71
|
+
each(entity, (e: any) => {
|
|
72
|
+
entityDefs[e.name] = clean({
|
|
73
|
+
fields: e.fields,
|
|
74
|
+
name: e.name,
|
|
75
|
+
op: e.op,
|
|
76
|
+
relations: e.relations,
|
|
77
|
+
}, true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const featureDefs: any = {}
|
|
81
|
+
each(feature, (f: any) => {
|
|
82
|
+
featureDefs[f.name] = f.config || {}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const entityStubs: any = {}
|
|
86
|
+
each(entity, (e: any) => {
|
|
87
|
+
entityStubs[e.name] = {}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const optionsDef: any = { base: baseUrl }
|
|
91
|
+
if (0 < svars.length) {
|
|
92
|
+
optionsDef.server = svars.reduce(
|
|
93
|
+
(a: any, v: any) => (a[v.name] = v.dflt, a), {})
|
|
94
|
+
}
|
|
95
|
+
if (authActive) {
|
|
96
|
+
optionsDef.auth = { prefix: authPrefix }
|
|
97
|
+
}
|
|
98
|
+
optionsDef.headers = headers
|
|
99
|
+
optionsDef.entity = entityStubs
|
|
100
|
+
|
|
101
|
+
// Key order here is the order `each` produced, which is sorted, so the
|
|
102
|
+
// emitted JSON is byte-stable across runs like the literal it replaces.
|
|
103
|
+
const configDef = {
|
|
104
|
+
main: { name: model.const.Name },
|
|
105
|
+
feature: featureDefs,
|
|
106
|
+
options: optionsDef,
|
|
107
|
+
entity: entityDefs,
|
|
108
|
+
}
|
|
109
|
+
const configJson = JSON.stringify(configDef)
|
|
110
|
+
|
|
111
|
+
// `auto` decides by size; 'data'/'literal' pin it for this SDK.
|
|
112
|
+
let configReprSetting = 'auto'
|
|
113
|
+
try {
|
|
114
|
+
configReprSetting = getModelPath(model, `main.${KIT}.config.repr`) || 'auto'
|
|
115
|
+
} catch (_e) { }
|
|
116
|
+
const asData = isConfigData(configJson, configReprSetting)
|
|
117
|
+
|
|
66
118
|
File({ name: 'config.' + target.ext }, () => {
|
|
67
119
|
|
|
120
|
+
// ABOVE THE THRESHOLD: emit the model as DATA.
|
|
121
|
+
//
|
|
122
|
+
// A composite literal makes the compiler walk every node of the model;
|
|
123
|
+
// a string constant is one token. On the real gitlab model that is 30.8 s
|
|
124
|
+
// and 2.49 GB of compiler memory versus 0.34 s and 0.06 GB, and a binary
|
|
125
|
+
// 2.1x smaller. MakeConfig still returns the same map, so nothing
|
|
126
|
+
// downstream can tell which representation it got.
|
|
127
|
+
//
|
|
128
|
+
// JSON.stringify output is a valid Go interpreted string literal: JSON
|
|
129
|
+
// escapes are a subset of Go's, and Go source is UTF-8 so non-ASCII needs
|
|
130
|
+
// no escaping. A raw (backtick) literal could NOT be used - the model
|
|
131
|
+
// contains backticks in values like `$STRING`.
|
|
132
|
+
if (asData) {
|
|
133
|
+
Content(`package core
|
|
134
|
+
|
|
135
|
+
import (
|
|
136
|
+
"encoding/json"
|
|
137
|
+
"math"
|
|
138
|
+
"sync"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// The API model, emitted as data rather than as a composite literal: see
|
|
142
|
+
// sdkgen rung L1. Parsed by MakeConfig, and parsed once by SharedConfig.
|
|
143
|
+
const configJSON = ${JSON.stringify(configJson)}
|
|
144
|
+
|
|
145
|
+
// json.Unmarshal decodes EVERY JSON number as float64, but the literal
|
|
146
|
+
// representation emits an integer token as an untyped constant that lands in
|
|
147
|
+
// map[string]any as an int. MakeConfig is public API and consumers type-assert
|
|
148
|
+
// against it, so the two representations must not disagree about the type of
|
|
149
|
+
// a whole number just because the model crossed a size threshold.
|
|
150
|
+
//
|
|
151
|
+
// Whole values become int; anything fractional, or too large to be exact in a
|
|
152
|
+
// float64, stays float64 - which is what the literal branch does too.
|
|
153
|
+
func configNormalise(val any) any {
|
|
154
|
+
switch v := val.(type) {
|
|
155
|
+
case map[string]any:
|
|
156
|
+
for k, c := range v {
|
|
157
|
+
v[k] = configNormalise(c)
|
|
158
|
+
}
|
|
159
|
+
return v
|
|
160
|
+
case []any:
|
|
161
|
+
for i, c := range v {
|
|
162
|
+
v[i] = configNormalise(c)
|
|
163
|
+
}
|
|
164
|
+
return v
|
|
165
|
+
case float64:
|
|
166
|
+
if v == math.Trunc(v) && math.Abs(v) <= 1<<53 {
|
|
167
|
+
return int(v)
|
|
168
|
+
}
|
|
169
|
+
return v
|
|
170
|
+
}
|
|
171
|
+
return val
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// MakeConfig parses a fresh, fully materialised config map. Every call
|
|
175
|
+
// re-parses, so prefer SharedConfig unless you need a private copy you
|
|
176
|
+
// intend to mutate.
|
|
177
|
+
func MakeConfig() map[string]any {
|
|
178
|
+
var out map[string]any
|
|
179
|
+
if err := json.Unmarshal([]byte(configJSON), &out); err != nil {
|
|
180
|
+
// Unreachable: the constant is generated by json.Marshal's
|
|
181
|
+
// counterpart and never edited by hand. Panic rather than return a
|
|
182
|
+
// silently empty config, which would fail far from the cause.
|
|
183
|
+
panic("${model.const.Name}: embedded config is not valid JSON: " + err.Error())
|
|
184
|
+
}
|
|
185
|
+
out, _ = configNormalise(out).(map[string]any)
|
|
186
|
+
return out
|
|
187
|
+
}
|
|
188
|
+
`)
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
|
|
68
192
|
Content(`package core
|
|
69
193
|
|
|
70
194
|
import (
|
|
@@ -113,7 +237,10 @@ ${serverBlock}${authBlock} "headers": ${formatGoMap(headers, 3)},
|
|
|
113
237
|
}, true), a), {}), 2)},
|
|
114
238
|
}
|
|
115
239
|
}
|
|
240
|
+
`)
|
|
241
|
+
}
|
|
116
242
|
|
|
243
|
+
Content(`
|
|
117
244
|
var (
|
|
118
245
|
sharedConfigOnce sync.Once
|
|
119
246
|
sharedConfigVal map[string]any
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
each,
|
|
12
12
|
indent,
|
|
13
13
|
isAuthActive,
|
|
14
|
+
isConfigData,
|
|
14
15
|
resolveAuthPrefix,
|
|
15
16
|
serverVariables,
|
|
16
17
|
} from '@voxgig/sdkgen'
|
|
@@ -62,13 +63,100 @@ const Config = cmp(async function Config(props: any) {
|
|
|
62
63
|
svars.map((v: any) => ` ${JSON.stringify(v.name)}: ${JSON.stringify(v.dflt)},\n`).join('') +
|
|
63
64
|
' },\n\n '
|
|
64
65
|
|
|
66
|
+
// Read the base URL here rather than leaving it to a `$$...$$` stdrep
|
|
67
|
+
// placeholder in the fragment. stdrep can only substitute a path the model
|
|
68
|
+
// actually has: a model with no `info.servers` left the placeholder itself in
|
|
69
|
+
// the generated source, so `options.base` came out as the literal string
|
|
70
|
+
// '$main.kit.info.servers.0.url$'. Reading it explicitly yields '' in that
|
|
71
|
+
// case, which is what every other target already emits, and is identical to
|
|
72
|
+
// the old output whenever the model does define a server.
|
|
73
|
+
let baseUrl = ''
|
|
74
|
+
try {
|
|
75
|
+
baseUrl = getModelPath(model, `main.${KIT}.info.servers.0.url`)
|
|
76
|
+
} catch (_e) { }
|
|
77
|
+
|
|
78
|
+
// The same config as an OBJECT, so it can be measured and, above the
|
|
79
|
+
// threshold, emitted as data instead of as class field literals. Mirrors the
|
|
80
|
+
// literal branch below exactly - feature config is NOT cleaned here, because
|
|
81
|
+
// it is not cleaned there either.
|
|
82
|
+
const entityDefs: any = {}
|
|
83
|
+
each(entity, (e: any) => {
|
|
84
|
+
entityDefs[e.name] = clean({
|
|
85
|
+
fields: e.fields,
|
|
86
|
+
name: e.name,
|
|
87
|
+
op: e.op,
|
|
88
|
+
relations: e.relations,
|
|
89
|
+
}, true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const featureDefs: any = {}
|
|
93
|
+
each(feature, (f: any) => {
|
|
94
|
+
featureDefs[f.name] = f.config
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const entityStubs: any = {}
|
|
98
|
+
each(entity, (e: any) => {
|
|
99
|
+
entityStubs[e.name] = {}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const optionsDef: any = { base: baseUrl }
|
|
103
|
+
if (0 < svars.length) {
|
|
104
|
+
optionsDef.server = svars.reduce(
|
|
105
|
+
(a: any, v: any) => (a[v.name] = v.dflt, a), {})
|
|
106
|
+
}
|
|
107
|
+
if (authActive) {
|
|
108
|
+
optionsDef.auth = { prefix: authPrefix }
|
|
109
|
+
}
|
|
110
|
+
optionsDef.headers = headers
|
|
111
|
+
optionsDef.entity = entityStubs
|
|
112
|
+
|
|
113
|
+
const configDef = {
|
|
114
|
+
main: { name: model.const.Name },
|
|
115
|
+
feature: featureDefs,
|
|
116
|
+
options: optionsDef,
|
|
117
|
+
entity: entityDefs,
|
|
118
|
+
}
|
|
119
|
+
const configJson = JSON.stringify(configDef)
|
|
120
|
+
|
|
121
|
+
// `auto` decides by size; 'data'/'literal' pin it for this SDK.
|
|
122
|
+
let configReprSetting = 'auto'
|
|
123
|
+
try {
|
|
124
|
+
configReprSetting = getModelPath(model, `main.${KIT}.config.repr`) || 'auto'
|
|
125
|
+
} catch (_e) { }
|
|
126
|
+
|
|
65
127
|
File({ name: 'Config.' + target.ext }, () => {
|
|
66
128
|
|
|
129
|
+
if (isConfigData(configJson, configReprSetting)) {
|
|
130
|
+
Fragment({
|
|
131
|
+
from: ff + 'Config.data.fragment.ts',
|
|
132
|
+
|
|
133
|
+
replace: {
|
|
134
|
+
|
|
135
|
+
'// #ImportFeatures': () => each(feature, (f: any) => {
|
|
136
|
+
Line(`import { ${nom(f, 'Name')}Feature } from ` +
|
|
137
|
+
`'./feature/${f.name}/${nom(f, 'Name')}Feature'`)
|
|
138
|
+
}),
|
|
139
|
+
|
|
140
|
+
'// #FeatureClasses': () => each(feature, (f: any) => {
|
|
141
|
+
Line(` ${f.name}: ${nom(f, 'Name')}Feature,`)
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
// A JS string literal, so the JSON survives verbatim. JSON.stringify
|
|
145
|
+
// escapes the quotes and backslashes the model contains (values like
|
|
146
|
+
// `$STRING` carry backticks, which a template literal could not).
|
|
147
|
+
"'CONFIGJSON'": JSON.stringify(configJson),
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
67
153
|
Fragment({
|
|
68
154
|
from: ff + 'Config.fragment.ts',
|
|
69
155
|
|
|
70
156
|
replace: {
|
|
71
157
|
|
|
158
|
+
"'BASEURL'": JSON.stringify(baseUrl),
|
|
159
|
+
|
|
72
160
|
"'SERVERBLOCK'": serverBlock,
|
|
73
161
|
|
|
74
162
|
"'AUTHBLOCK'": authBlock,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
import { BaseFeature } from './feature/base/BaseFeature'
|
|
3
|
+
// #ImportFeatures
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const FEATURE_CLASS: Record<string, typeof BaseFeature> = {
|
|
7
|
+
// #FeatureClasses
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
// THE API MODEL, EMBEDDED AS DATA (sdkgen rung L1).
|
|
12
|
+
//
|
|
13
|
+
// The literal form of this file declares the whole model as nested object
|
|
14
|
+
// literals on the class. For a large API that is megabytes of source that the
|
|
15
|
+
// TypeScript compiler must parse and infer a type for on every build, and that
|
|
16
|
+
// the JavaScript engine must build node by node on every module load.
|
|
17
|
+
//
|
|
18
|
+
// As a single string constant it is one token to the compiler, and V8's JSON
|
|
19
|
+
// parser builds the object far faster than the equivalent literal - JSON.parse
|
|
20
|
+
// is the well-worn trick for exactly this shape of data.
|
|
21
|
+
//
|
|
22
|
+
// Emitted only above a size threshold, or when `main.kit.config.repr` pins it:
|
|
23
|
+
// for a small model the literal is smaller, loads no slower, and is far easier
|
|
24
|
+
// to read when debugging.
|
|
25
|
+
const CONFIG_DATA = 'CONFIGJSON'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Config {
|
|
29
|
+
|
|
30
|
+
makeFeature(this: any, fn: string) {
|
|
31
|
+
const fc = FEATURE_CLASS[fn]
|
|
32
|
+
const fi = new fc()
|
|
33
|
+
// TODO: errors etc
|
|
34
|
+
return fi
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// Parsed ONCE, at module load, exactly like the literal form was built once.
|
|
41
|
+
//
|
|
42
|
+
// The parsed data is assigned onto an instance rather than replacing it, so
|
|
43
|
+
// `config.main` / `.feature` / `.options` / `.entity` read exactly as they did
|
|
44
|
+
// when they were field initialisers, and `makeFeature` stays where it was on
|
|
45
|
+
// the prototype. Callers cannot tell which representation they were given.
|
|
46
|
+
const config: any = Object.assign(new Config(), JSON.parse(CONFIG_DATA))
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
config
|
|
50
|
+
}
|
package/src/cmp/ReadmeTop.ts
CHANGED
|
@@ -577,7 +577,16 @@ The OpenAPI spec(s) this SDK was generated from are kept in the
|
|
|
577
577
|
|
|
578
578
|
`)
|
|
579
579
|
if (upstreamUrl) {
|
|
580
|
-
|
|
580
|
+
// A per-tenant server URL carries an OpenAPI server variable —
|
|
581
|
+
// `https://{instance}.dreamapply.com/api` — and is the right thing to
|
|
582
|
+
// SHOW, because it tells the reader the host is theirs to fill in. It is
|
|
583
|
+
// not a thing to LINK: the braces are not a resolvable address, so a
|
|
584
|
+
// markdown link renders as clickable and dead. Show it as code instead.
|
|
585
|
+
const templated = /[{}]/.test(upstreamUrl)
|
|
586
|
+
Content(templated
|
|
587
|
+
? `- Upstream API: \`${upstreamUrl}\`
|
|
588
|
+
`
|
|
589
|
+
: `- Upstream API: [${upstreamUrl}](${upstreamUrl})
|
|
581
590
|
`)
|
|
582
591
|
}
|
|
583
592
|
if (docsUrl && docsUrl !== upstreamUrl) {
|
package/src/sdkgen.ts
CHANGED
|
@@ -19,7 +19,8 @@ import type {
|
|
|
19
19
|
ActionResult,
|
|
20
20
|
} from './types'
|
|
21
21
|
|
|
22
|
-
import { SdkGenError, requirePath, isAuthActive, resolveAuthPrefix
|
|
22
|
+
import { SdkGenError, requirePath, isAuthActive, resolveAuthPrefix,
|
|
23
|
+
CONFIG_DATA_THRESHOLD, CONFIG_REPR_VALUES, isConfigData, configRepr } from './utility'
|
|
23
24
|
|
|
24
25
|
import { Main } from './cmp/Main'
|
|
25
26
|
import { ExternalTarget } from './cmp/ExternalTarget'
|
|
@@ -776,6 +777,10 @@ export {
|
|
|
776
777
|
requirePath,
|
|
777
778
|
isAuthActive,
|
|
778
779
|
resolveAuthPrefix,
|
|
780
|
+
CONFIG_DATA_THRESHOLD,
|
|
781
|
+
CONFIG_REPR_VALUES,
|
|
782
|
+
isConfigData,
|
|
783
|
+
configRepr,
|
|
779
784
|
|
|
780
785
|
// Scaffold components need this to fail a generation with an actionable
|
|
781
786
|
// message rather than a bare Error (py-data guards on its sibling `py`).
|
package/src/utility.ts
CHANGED
|
@@ -96,4 +96,73 @@ export {
|
|
|
96
96
|
isAuthActive,
|
|
97
97
|
resolveAuthPrefix,
|
|
98
98
|
SdkGenError,
|
|
99
|
+
CONFIG_DATA_THRESHOLD,
|
|
100
|
+
CONFIG_REPR_VALUES,
|
|
101
|
+
isConfigData,
|
|
102
|
+
configRepr,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
// CONFIG REPRESENTATION (design rung L1, threshold from design Q7).
|
|
107
|
+
//
|
|
108
|
+
// Above a size threshold the API model is emitted as DATA - a JSON string
|
|
109
|
+
// constant parsed once - rather than as a composite literal. Below it the
|
|
110
|
+
// literal stays, because for a small model the literal is smaller, simpler,
|
|
111
|
+
// faster to load and far easier to debug, and a symbol table would be pure
|
|
112
|
+
// complexity.
|
|
113
|
+
//
|
|
114
|
+
// The threshold is on the JSON, not the emitted source, because the emitted
|
|
115
|
+
// source size varies by language while the model does not. It is measured in
|
|
116
|
+
// UTF-8 BYTES rather than string length: `.length` counts UTF-16 code units,
|
|
117
|
+
// so a CJK-heavy model would read as roughly a third of its real size and
|
|
118
|
+
// stay on the expensive literal path well past the point where it hurts.
|
|
119
|
+
//
|
|
120
|
+
// Measured on the real gitlab model (923.5 KB of JSON), Go, cold cache,
|
|
121
|
+
// recompiling only the config package:
|
|
122
|
+
//
|
|
123
|
+
// composite literal JSON string constant
|
|
124
|
+
// compile+link wall 30.80 s 0.34 s 91x faster
|
|
125
|
+
// peak compiler RSS 2.49 GB 0.06 GB 39x less
|
|
126
|
+
// binary 7.44 MB 3.51 MB 2.1x smaller
|
|
127
|
+
//
|
|
128
|
+
// The reader side is unchanged either way: make_config returns the same map,
|
|
129
|
+
// so nothing downstream can tell which representation it got.
|
|
130
|
+
const CONFIG_DATA_THRESHOLD = 256 * 1024
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
// Should this model be emitted as data rather than as a literal?
|
|
134
|
+
//
|
|
135
|
+
// `repr` is the per-SDK override from `main.kit.config.repr`: 'auto' (the
|
|
136
|
+
// default) decides by size, 'data' and 'literal' pin it. The override is what
|
|
137
|
+
// lets a small fixture exercise the data path - by size alone no test model
|
|
138
|
+
// comes near the threshold, so the branch every large SDK depends on would
|
|
139
|
+
// never be generated, compiled or run in CI.
|
|
140
|
+
const CONFIG_REPR_VALUES = ['auto', 'data', 'literal']
|
|
141
|
+
|
|
142
|
+
function isConfigData(configJson: string, repr?: string): boolean {
|
|
143
|
+
// An unknown value is REJECTED, not ignored. The aontu declaration
|
|
144
|
+
// documents the closed set but does not enforce it here, and silently
|
|
145
|
+
// treating `repr: 'date'` as `auto` would quietly restore the compile cost
|
|
146
|
+
// this exists to remove - the failure mode being a slow build nobody
|
|
147
|
+
// connects to a typo.
|
|
148
|
+
if (null != repr && '' !== repr && !CONFIG_REPR_VALUES.includes(repr)) {
|
|
149
|
+
throw new SdkGenError(
|
|
150
|
+
'sdkgen: main.kit.config.repr must be one of ' +
|
|
151
|
+
CONFIG_REPR_VALUES.join(', ') + ' (got: ' + repr + ')', {})
|
|
152
|
+
}
|
|
153
|
+
if ('data' === repr) {
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
if ('literal' === repr) {
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
return CONFIG_DATA_THRESHOLD < Buffer.byteLength(configJson, 'utf8')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
// The chosen representation, as a word - for generation logs and for the
|
|
164
|
+
// per-SDK reporting the fleet regen needs, so a model crossing the threshold
|
|
165
|
+
// is visible rather than showing up as an unexplained whole-file diff.
|
|
166
|
+
function configRepr(configJson: string, repr?: string): string {
|
|
167
|
+
return isConfigData(configJson, repr) ? 'data' : 'literal'
|
|
99
168
|
}
|