@shepherdjerred/helm-types 1.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.
- package/README.md +200 -0
- package/dist/cli.js +20718 -0
- package/dist/index.js +20600 -0
- package/package.json +63 -0
- package/src/chart-fetcher.ts +150 -0
- package/src/chart-info-parser.ts +52 -0
- package/src/cli.ts +197 -0
- package/src/code-generator.ts +191 -0
- package/src/comment-parser.ts +156 -0
- package/src/config.ts +140 -0
- package/src/helm-types.ts +46 -0
- package/src/index.ts +15 -0
- package/src/interface-generator.ts +203 -0
- package/src/reset.d.ts +1 -0
- package/src/schemas.ts +37 -0
- package/src/type-converter.ts +536 -0
- package/src/type-inference.ts +440 -0
- package/src/types.ts +38 -0
- package/src/utils.ts +76 -0
- package/src/yaml-comments.ts +799 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import type { JSONSchemaProperty, TypeScriptInterface, TypeProperty } from "./types.js";
|
|
2
|
+
import type { HelmValue } from "./schemas.js";
|
|
3
|
+
import {
|
|
4
|
+
StringSchema,
|
|
5
|
+
ActualNumberSchema,
|
|
6
|
+
ActualBooleanSchema,
|
|
7
|
+
NullSchema,
|
|
8
|
+
UndefinedSchema,
|
|
9
|
+
ArraySchema,
|
|
10
|
+
StringBooleanSchema,
|
|
11
|
+
HelmValueSchema,
|
|
12
|
+
} from "./schemas.js";
|
|
13
|
+
import { shouldAllowArbitraryProps, isK8sResourceSpec } from "./config.js";
|
|
14
|
+
import { sanitizePropertyName, sanitizeTypeName, capitalizeFirst } from "./utils.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Augment a Kubernetes resource spec interface with both requests and limits.
|
|
18
|
+
* If only one is present, copy its type structure to the other.
|
|
19
|
+
*/
|
|
20
|
+
function augmentK8sResourceSpec(iface: TypeScriptInterface): void {
|
|
21
|
+
const hasRequests = "requests" in iface.properties;
|
|
22
|
+
const hasLimits = "limits" in iface.properties;
|
|
23
|
+
|
|
24
|
+
// If we have requests but not limits, add limits with the same structure
|
|
25
|
+
if (hasRequests && !hasLimits) {
|
|
26
|
+
const requestsProp = iface.properties["requests"];
|
|
27
|
+
if (requestsProp) {
|
|
28
|
+
// Create limits property with the same type but different name for the nested interface
|
|
29
|
+
const limitsTypeName = requestsProp.type.replace("Requests", "Limits");
|
|
30
|
+
|
|
31
|
+
// If there's a nested interface, create a copy for limits
|
|
32
|
+
if (requestsProp.nested) {
|
|
33
|
+
const limitsNested: TypeScriptInterface = {
|
|
34
|
+
name: limitsTypeName,
|
|
35
|
+
properties: { ...requestsProp.nested.properties },
|
|
36
|
+
allowArbitraryProps: requestsProp.nested.allowArbitraryProps,
|
|
37
|
+
};
|
|
38
|
+
iface.properties["limits"] = {
|
|
39
|
+
type: limitsTypeName,
|
|
40
|
+
optional: true,
|
|
41
|
+
nested: limitsNested,
|
|
42
|
+
description: "Kubernetes resource limits (memory, cpu, etc.)",
|
|
43
|
+
};
|
|
44
|
+
} else {
|
|
45
|
+
// No nested interface, just copy the type
|
|
46
|
+
iface.properties["limits"] = {
|
|
47
|
+
type: requestsProp.type,
|
|
48
|
+
optional: true,
|
|
49
|
+
description: "Kubernetes resource limits (memory, cpu, etc.)",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If we have limits but not requests, add requests with the same structure
|
|
56
|
+
if (hasLimits && !hasRequests) {
|
|
57
|
+
const limitsProp = iface.properties["limits"];
|
|
58
|
+
if (limitsProp) {
|
|
59
|
+
const requestsTypeName = limitsProp.type.replace("Limits", "Requests");
|
|
60
|
+
|
|
61
|
+
if (limitsProp.nested) {
|
|
62
|
+
const requestsNested: TypeScriptInterface = {
|
|
63
|
+
name: requestsTypeName,
|
|
64
|
+
properties: { ...limitsProp.nested.properties },
|
|
65
|
+
allowArbitraryProps: limitsProp.nested.allowArbitraryProps,
|
|
66
|
+
};
|
|
67
|
+
iface.properties["requests"] = {
|
|
68
|
+
type: requestsTypeName,
|
|
69
|
+
optional: true,
|
|
70
|
+
nested: requestsNested,
|
|
71
|
+
description: "Kubernetes resource requests (memory, cpu, etc.)",
|
|
72
|
+
};
|
|
73
|
+
} else {
|
|
74
|
+
iface.properties["requests"] = {
|
|
75
|
+
type: limitsProp.type,
|
|
76
|
+
optional: true,
|
|
77
|
+
description: "Kubernetes resource requests (memory, cpu, etc.)",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Convert JSON schema type to TypeScript type string
|
|
86
|
+
*/
|
|
87
|
+
export function jsonSchemaToTypeScript(schema: JSONSchemaProperty): string {
|
|
88
|
+
// Handle oneOf - union of types
|
|
89
|
+
if (schema.oneOf) {
|
|
90
|
+
const types = schema.oneOf.map((s) => jsonSchemaToTypeScript(s));
|
|
91
|
+
return types.join(" | ");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle anyOf - union of types
|
|
95
|
+
if (schema.anyOf) {
|
|
96
|
+
const types = schema.anyOf.map((s) => jsonSchemaToTypeScript(s));
|
|
97
|
+
return types.join(" | ");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle enum
|
|
101
|
+
if (schema.enum) {
|
|
102
|
+
return schema.enum.map((v) => (StringSchema.safeParse(v).success ? `"${String(v)}"` : String(v))).join(" | ");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Handle array type
|
|
106
|
+
if (schema.type === "array" && schema.items) {
|
|
107
|
+
const itemType = jsonSchemaToTypeScript(schema.items);
|
|
108
|
+
return `${itemType}[]`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle basic types
|
|
112
|
+
if (StringSchema.safeParse(schema.type).success) {
|
|
113
|
+
switch (schema.type) {
|
|
114
|
+
case "string":
|
|
115
|
+
return "string";
|
|
116
|
+
case "number":
|
|
117
|
+
case "integer":
|
|
118
|
+
return "number";
|
|
119
|
+
case "boolean":
|
|
120
|
+
return "boolean";
|
|
121
|
+
case "object":
|
|
122
|
+
return "object";
|
|
123
|
+
case "array":
|
|
124
|
+
return "unknown[]";
|
|
125
|
+
case "null":
|
|
126
|
+
return "null";
|
|
127
|
+
default:
|
|
128
|
+
return "unknown";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle multiple types
|
|
133
|
+
const arrayTypeCheck = ArraySchema.safeParse(schema.type);
|
|
134
|
+
if (arrayTypeCheck.success) {
|
|
135
|
+
return arrayTypeCheck.data
|
|
136
|
+
.map((t: unknown) => {
|
|
137
|
+
if (!StringSchema.safeParse(t).success) return "unknown";
|
|
138
|
+
const typeStr = String(t);
|
|
139
|
+
switch (typeStr) {
|
|
140
|
+
case "string":
|
|
141
|
+
return "string";
|
|
142
|
+
case "number":
|
|
143
|
+
case "integer":
|
|
144
|
+
return "number";
|
|
145
|
+
case "boolean":
|
|
146
|
+
return "boolean";
|
|
147
|
+
case "object":
|
|
148
|
+
return "object";
|
|
149
|
+
case "array":
|
|
150
|
+
return "unknown[]";
|
|
151
|
+
case "null":
|
|
152
|
+
return "null";
|
|
153
|
+
default:
|
|
154
|
+
return "unknown";
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
.join(" | ");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return "unknown";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Infer TypeScript type from actual runtime value
|
|
165
|
+
*/
|
|
166
|
+
export function inferTypeFromValue(value: unknown): string | null {
|
|
167
|
+
// Check null/undefined
|
|
168
|
+
if (NullSchema.safeParse(value).success || UndefinedSchema.safeParse(value).success) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check for actual boolean
|
|
173
|
+
if (ActualBooleanSchema.safeParse(value).success) {
|
|
174
|
+
return "boolean";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check for actual number
|
|
178
|
+
if (ActualNumberSchema.safeParse(value).success) {
|
|
179
|
+
return "number";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if it's a string that looks like a boolean
|
|
183
|
+
if (StringBooleanSchema.safeParse(value).success) {
|
|
184
|
+
return "boolean";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check if it's a string that looks like a number
|
|
188
|
+
const stringCheck = StringSchema.safeParse(value);
|
|
189
|
+
if (stringCheck.success) {
|
|
190
|
+
const trimmed = stringCheck.data.trim();
|
|
191
|
+
if (trimmed !== "" && !isNaN(Number(trimmed)) && isFinite(Number(trimmed))) {
|
|
192
|
+
return "number";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check for array
|
|
197
|
+
if (ArraySchema.safeParse(value).success) {
|
|
198
|
+
return "array";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check for object
|
|
202
|
+
if (HelmValueSchema.safeParse(value).success) {
|
|
203
|
+
return "object";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Plain string
|
|
207
|
+
if (StringSchema.safeParse(value).success) {
|
|
208
|
+
return "string";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return "unknown";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if inferred type is compatible with schema type
|
|
216
|
+
*/
|
|
217
|
+
export function typesAreCompatible(inferredType: string, schemaType: string): boolean {
|
|
218
|
+
// Exact match
|
|
219
|
+
if (inferredType === schemaType) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check if the inferred type is part of a union in the schema
|
|
224
|
+
// For example: schemaType might be "number | \"default\"" and inferredType is "string"
|
|
225
|
+
const schemaTypes = schemaType.split("|").map((t) => t.trim().replace(/^["']|["']$/g, ""));
|
|
226
|
+
|
|
227
|
+
// If schema is a union, check if inferred type is compatible with any part
|
|
228
|
+
if (schemaTypes.length > 1) {
|
|
229
|
+
for (const st of schemaTypes) {
|
|
230
|
+
// Handle quoted strings in unions (like "default")
|
|
231
|
+
if (st.startsWith('"') && st.endsWith('"') && inferredType === "string") {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (st === inferredType) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
// Arrays
|
|
238
|
+
if (st.endsWith("[]") && inferredType === "array") {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Handle array types
|
|
245
|
+
if (schemaType.endsWith("[]") && inferredType === "array") {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Handle specific string literals - if schema expects specific strings and value is a string
|
|
250
|
+
if (schemaType.includes('"') && inferredType === "string") {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// unknown is compatible with everything (schema might be less specific)
|
|
255
|
+
if (schemaType === "unknown" || inferredType === "unknown") {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Convert Helm values to TypeScript interface
|
|
264
|
+
*/
|
|
265
|
+
export function convertToTypeScriptInterface(
|
|
266
|
+
values: HelmValue,
|
|
267
|
+
interfaceName: string,
|
|
268
|
+
schema?: JSONSchemaProperty | null,
|
|
269
|
+
yamlComments?: Map<string, string>,
|
|
270
|
+
keyPrefix = "",
|
|
271
|
+
chartName?: string,
|
|
272
|
+
): TypeScriptInterface {
|
|
273
|
+
const properties: Record<string, TypeProperty> = {};
|
|
274
|
+
const schemaProps = schema?.properties;
|
|
275
|
+
|
|
276
|
+
for (const [key, value] of Object.entries(values)) {
|
|
277
|
+
const sanitizedKey = sanitizePropertyName(key);
|
|
278
|
+
const typeNameSuffix = sanitizeTypeName(key);
|
|
279
|
+
const propertySchema = schemaProps?.[key];
|
|
280
|
+
const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key;
|
|
281
|
+
const yamlComment = yamlComments?.get(fullKey);
|
|
282
|
+
|
|
283
|
+
properties[sanitizedKey] = convertValueToProperty(
|
|
284
|
+
value,
|
|
285
|
+
`${interfaceName}${capitalizeFirst(typeNameSuffix)}`,
|
|
286
|
+
propertySchema,
|
|
287
|
+
key, // Pass the property name for better warnings
|
|
288
|
+
yamlComment,
|
|
289
|
+
yamlComments,
|
|
290
|
+
fullKey,
|
|
291
|
+
chartName,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check if this interface should allow arbitrary properties
|
|
296
|
+
const allowArbitraryProps = chartName
|
|
297
|
+
? shouldAllowArbitraryProps(keyPrefix, chartName, keyPrefix.split(".").pop() ?? "", yamlComments?.get(keyPrefix))
|
|
298
|
+
: false;
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
name: interfaceName,
|
|
302
|
+
properties,
|
|
303
|
+
allowArbitraryProps,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function convertValueToProperty(
|
|
308
|
+
value: unknown,
|
|
309
|
+
nestedTypeName: string,
|
|
310
|
+
schema?: JSONSchemaProperty,
|
|
311
|
+
propertyName?: string,
|
|
312
|
+
yamlComment?: string,
|
|
313
|
+
yamlComments?: Map<string, string>,
|
|
314
|
+
fullKey?: string,
|
|
315
|
+
chartName?: string,
|
|
316
|
+
): TypeProperty {
|
|
317
|
+
// If we have a JSON schema for this property, prefer it over inference
|
|
318
|
+
if (schema) {
|
|
319
|
+
// First, infer the type from the actual value for comparison
|
|
320
|
+
const inferredType = inferTypeFromValue(value);
|
|
321
|
+
const schemaType = jsonSchemaToTypeScript(schema);
|
|
322
|
+
|
|
323
|
+
// Check if schema and inferred types are in agreement
|
|
324
|
+
if (inferredType && !typesAreCompatible(inferredType, schemaType)) {
|
|
325
|
+
const propName = propertyName ? `'${propertyName}': ` : "";
|
|
326
|
+
console.warn(
|
|
327
|
+
` ⚠️ Type mismatch for ${propName}Schema says '${schemaType}' but value suggests '${inferredType}' (value: ${String(value).substring(0, 50)})`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Merge description from schema and YAML comments
|
|
332
|
+
let description = schema.description;
|
|
333
|
+
if (yamlComment) {
|
|
334
|
+
if (description) {
|
|
335
|
+
// If both exist, combine them
|
|
336
|
+
description = `${yamlComment}\n\n${description}`;
|
|
337
|
+
} else {
|
|
338
|
+
description = yamlComment;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const defaultValue = schema.default !== undefined ? schema.default : value;
|
|
342
|
+
|
|
343
|
+
// If schema defines it as an object with properties, recurse
|
|
344
|
+
const helmValueCheckForProps = HelmValueSchema.safeParse(value);
|
|
345
|
+
if (schema.properties && helmValueCheckForProps.success) {
|
|
346
|
+
const nestedInterface = convertToTypeScriptInterface(
|
|
347
|
+
helmValueCheckForProps.data,
|
|
348
|
+
nestedTypeName,
|
|
349
|
+
schema,
|
|
350
|
+
yamlComments,
|
|
351
|
+
fullKey,
|
|
352
|
+
chartName,
|
|
353
|
+
);
|
|
354
|
+
return {
|
|
355
|
+
type: nestedTypeName,
|
|
356
|
+
optional: true,
|
|
357
|
+
nested: nestedInterface,
|
|
358
|
+
description,
|
|
359
|
+
default: defaultValue,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Otherwise, use the schema type directly
|
|
364
|
+
const tsType = schemaType;
|
|
365
|
+
|
|
366
|
+
// Handle object types without explicit properties
|
|
367
|
+
const helmValueCheckForObject = HelmValueSchema.safeParse(value);
|
|
368
|
+
if (tsType === "object" && helmValueCheckForObject.success) {
|
|
369
|
+
const nestedInterface = convertToTypeScriptInterface(
|
|
370
|
+
helmValueCheckForObject.data,
|
|
371
|
+
nestedTypeName,
|
|
372
|
+
undefined,
|
|
373
|
+
yamlComments,
|
|
374
|
+
fullKey,
|
|
375
|
+
chartName,
|
|
376
|
+
);
|
|
377
|
+
return {
|
|
378
|
+
type: nestedTypeName,
|
|
379
|
+
optional: true,
|
|
380
|
+
nested: nestedInterface,
|
|
381
|
+
description,
|
|
382
|
+
default: defaultValue,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return { type: tsType, optional: true, description, default: defaultValue };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Fall back to runtime type inference when no schema is available
|
|
390
|
+
// Use Zod schemas for robust type detection
|
|
391
|
+
// IMPORTANT: Check for complex types (arrays, objects) BEFORE primitive types with coercion
|
|
392
|
+
|
|
393
|
+
// Check for null/undefined first
|
|
394
|
+
if (NullSchema.safeParse(value).success || UndefinedSchema.safeParse(value).success) {
|
|
395
|
+
return { type: "unknown", optional: true };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Check for array (before coercion checks, as coercion can convert arrays to true)
|
|
399
|
+
const arrayResult = ArraySchema.safeParse(value);
|
|
400
|
+
if (arrayResult.success) {
|
|
401
|
+
const arrayValue = arrayResult.data;
|
|
402
|
+
if (arrayValue.length === 0) {
|
|
403
|
+
return { type: "unknown[]", optional: true };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Sample multiple elements for better type inference
|
|
407
|
+
const elementTypes = new Set<string>();
|
|
408
|
+
const elementTypeProps: TypeProperty[] = [];
|
|
409
|
+
const sampleSize = Math.min(arrayValue.length, 3); // Check up to 3 elements
|
|
410
|
+
|
|
411
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
412
|
+
const elementType = convertValueToProperty(arrayValue[i], nestedTypeName);
|
|
413
|
+
elementTypes.add(elementType.type);
|
|
414
|
+
elementTypeProps.push(elementType);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// If all elements have the same type, use that
|
|
418
|
+
if (elementTypes.size === 1) {
|
|
419
|
+
const elementType = Array.from(elementTypes)[0];
|
|
420
|
+
const elementProp = elementTypeProps[0];
|
|
421
|
+
if (elementType && elementProp) {
|
|
422
|
+
// For object array elements, we need to create a proper interface for the array element
|
|
423
|
+
if (elementProp.nested) {
|
|
424
|
+
// Create a new interface name for array elements
|
|
425
|
+
const arrayElementTypeName = `${nestedTypeName}Element`;
|
|
426
|
+
|
|
427
|
+
// Check if array elements should allow arbitrary properties
|
|
428
|
+
// Array elements inherit extensibility from their parent array
|
|
429
|
+
const allowArbitraryProps =
|
|
430
|
+
chartName && fullKey
|
|
431
|
+
? shouldAllowArbitraryProps(fullKey, chartName, propertyName ?? "", yamlComment)
|
|
432
|
+
: false;
|
|
433
|
+
|
|
434
|
+
const arrayElementInterface: TypeScriptInterface = {
|
|
435
|
+
name: arrayElementTypeName,
|
|
436
|
+
properties: elementProp.nested.properties,
|
|
437
|
+
allowArbitraryProps,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
type: `${arrayElementTypeName}[]`,
|
|
442
|
+
optional: true,
|
|
443
|
+
nested: arrayElementInterface,
|
|
444
|
+
};
|
|
445
|
+
} else {
|
|
446
|
+
return {
|
|
447
|
+
type: `${elementType}[]`,
|
|
448
|
+
optional: true,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// If mixed types, use union type for common cases
|
|
455
|
+
const types = Array.from(elementTypes).sort();
|
|
456
|
+
if (types.length <= 3 && types.every((t) => ["string", "number", "boolean"].includes(t))) {
|
|
457
|
+
return {
|
|
458
|
+
type: `(${types.join(" | ")})[]`,
|
|
459
|
+
optional: true,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Otherwise fall back to unknown[]
|
|
464
|
+
return { type: "unknown[]", optional: true };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Check for object (before primitive coercion checks)
|
|
468
|
+
const objectResult = HelmValueSchema.safeParse(value);
|
|
469
|
+
if (objectResult.success) {
|
|
470
|
+
const nestedInterface = convertToTypeScriptInterface(
|
|
471
|
+
objectResult.data,
|
|
472
|
+
nestedTypeName,
|
|
473
|
+
undefined,
|
|
474
|
+
yamlComments,
|
|
475
|
+
fullKey,
|
|
476
|
+
chartName,
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// Augment K8s resource specs with both requests and limits
|
|
480
|
+
if (propertyName && isK8sResourceSpec(propertyName)) {
|
|
481
|
+
augmentK8sResourceSpec(nestedInterface);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
type: nestedTypeName,
|
|
486
|
+
optional: true,
|
|
487
|
+
nested: nestedInterface,
|
|
488
|
+
description: yamlComment,
|
|
489
|
+
default: value,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Now check for primitives - first actual runtime types, then coerced string types
|
|
494
|
+
// This prevents objects from being coerced to booleans
|
|
495
|
+
|
|
496
|
+
// Check for actual runtime boolean (true/false)
|
|
497
|
+
if (ActualBooleanSchema.safeParse(value).success) {
|
|
498
|
+
return { type: "boolean", optional: true, description: yamlComment, default: value };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Check for actual runtime number
|
|
502
|
+
if (ActualNumberSchema.safeParse(value).success) {
|
|
503
|
+
return { type: "number", optional: true, description: yamlComment, default: value };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Check if it's a string that represents a boolean ("true", "FALSE", etc.)
|
|
507
|
+
if (StringBooleanSchema.safeParse(value).success) {
|
|
508
|
+
return { type: "boolean", optional: true, description: yamlComment, default: value };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Check if it's a string that represents a number ("15", "0", etc.)
|
|
512
|
+
// Only treat non-empty strings that parse as numbers as numbers
|
|
513
|
+
const stringCheckForNumber = StringSchema.safeParse(value);
|
|
514
|
+
if (stringCheckForNumber.success) {
|
|
515
|
+
const trimmed = stringCheckForNumber.data.trim();
|
|
516
|
+
// Don't treat empty strings or purely whitespace as numbers
|
|
517
|
+
if (trimmed !== "" && !isNaN(Number(trimmed)) && isFinite(Number(trimmed))) {
|
|
518
|
+
return { type: "number", optional: true, description: yamlComment, default: value };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Check for plain string (strings that don't look like numbers or booleans)
|
|
523
|
+
const stringCheckForPlain = StringSchema.safeParse(value);
|
|
524
|
+
if (stringCheckForPlain.success) {
|
|
525
|
+
// Special case: "default" is often used as a sentinel value in Helm charts
|
|
526
|
+
// that can be overridden with actual typed values (numbers, booleans, etc.)
|
|
527
|
+
if (stringCheckForPlain.data === "default") {
|
|
528
|
+
return { type: "string | number | boolean", optional: true, description: yamlComment, default: value };
|
|
529
|
+
}
|
|
530
|
+
return { type: "string", optional: true, description: yamlComment, default: value };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Fallback for any unrecognized type
|
|
534
|
+
console.warn(`Unrecognized value type for: ${String(value)}, using 'unknown'`);
|
|
535
|
+
return { type: "unknown", optional: true, description: yamlComment };
|
|
536
|
+
}
|