@shepherdjerred/helm-types 1.7.0-dev.13412 → 1.7.0-dev.13599

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shepherdjerred/helm-types",
3
- "version": "1.7.0-dev.13412",
3
+ "version": "1.7.0-dev.13599",
4
4
  "description": "Generate TypeScript types from Helm chart values.yaml and values.schema.json",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,226 +0,0 @@
1
- import type { TypeScriptInterface } from "./types.ts";
2
- import {
3
- StringSchema,
4
- ArraySchema,
5
- RecordSchema,
6
- ActualNumberSchema,
7
- ActualBooleanSchema,
8
- } from "./schemas.ts";
9
- import { capitalizeFirst } from "./utils.ts";
10
-
11
- /**
12
- * Generate TypeScript code from interface definition
13
- */
14
- export function generateTypeScriptCode(
15
- mainInterface: TypeScriptInterface,
16
- chartName: string,
17
- ): string {
18
- const interfaces: TypeScriptInterface[] = [];
19
-
20
- // Collect all nested interfaces
21
- collectNestedInterfaces(mainInterface, interfaces);
22
-
23
- let code = `// Generated TypeScript types for ${chartName} Helm chart\n\n`;
24
-
25
- // Generate all interfaces
26
- for (const iface of interfaces) {
27
- code += generateInterfaceCode(iface);
28
- code += "\n";
29
- }
30
-
31
- // Generate parameter type (flattened dot notation)
32
- code += generateParameterType(mainInterface, chartName);
33
-
34
- // Add header comment for generated files
35
- if (code.includes(": any")) {
36
- code = `// Generated TypeScript types for ${chartName} Helm chart
37
-
38
- ${code.slice(Math.max(0, code.indexOf("\n\n") + 2))}`;
39
- }
40
-
41
- return code;
42
- }
43
-
44
- function collectNestedInterfaces(
45
- iface: TypeScriptInterface,
46
- collected: TypeScriptInterface[],
47
- ): void {
48
- for (const prop of Object.values(iface.properties)) {
49
- if (prop.nested) {
50
- collected.push(prop.nested);
51
- collectNestedInterfaces(prop.nested, collected);
52
- }
53
- }
54
-
55
- // Add main interface last so dependencies come first
56
- if (!collected.some((i) => i.name === iface.name)) {
57
- collected.push(iface);
58
- }
59
- }
60
-
61
- function generateInterfaceCode(iface: TypeScriptInterface): string {
62
- const hasProperties = Object.keys(iface.properties).length > 0;
63
-
64
- if (!hasProperties) {
65
- // Use 'object' for empty interfaces instead of '{}'
66
- return `export type ${iface.name} = object;\n`;
67
- }
68
-
69
- let code = `export type ${iface.name} = {\n`;
70
-
71
- for (const [key, prop] of Object.entries(iface.properties)) {
72
- const optional = prop.optional ? "?" : "";
73
-
74
- // Generate JSDoc comment if we have description or default
75
- if (
76
- (prop.description != null && prop.description !== "") ||
77
- prop.default !== undefined
78
- ) {
79
- code += ` /**\n`;
80
-
81
- if (prop.description != null && prop.description !== "") {
82
- // Format multi-line descriptions properly with " * " prefix
83
- // Escape */ sequences to prevent premature comment closure
84
- const escapedDescription = prop.description.replaceAll(
85
- "*/",
86
- String.raw`*\/`,
87
- );
88
- const descLines = escapedDescription.split("\n");
89
- for (const line of descLines) {
90
- code += ` * ${line}\n`;
91
- }
92
- }
93
-
94
- if (prop.default !== undefined) {
95
- const defaultStr = formatDefaultValue(prop.default);
96
- const hasDescription =
97
- prop.description != null && prop.description !== "";
98
- if (defaultStr !== "" && hasDescription && defaultStr != null) {
99
- code += ` *\n`;
100
- }
101
- if (defaultStr !== "" && defaultStr != null) {
102
- code += ` * @default ${defaultStr}\n`;
103
- }
104
- }
105
-
106
- code += ` */\n`;
107
- }
108
-
109
- code += ` ${key}${optional}: ${prop.type};\n`;
110
- }
111
-
112
- code += "};\n";
113
- return code;
114
- }
115
-
116
- /**
117
- * Format a default value for display in JSDoc
118
- */
119
- function formatDefaultValue(value: unknown): string | null {
120
- if (value === null) {
121
- return "null";
122
- }
123
- if (value === undefined) {
124
- return null;
125
- }
126
-
127
- // Handle arrays
128
- const arrayCheck = ArraySchema.safeParse(value);
129
- if (arrayCheck.success) {
130
- if (arrayCheck.data.length === 0) {
131
- return "[]";
132
- }
133
- if (arrayCheck.data.length <= 3) {
134
- try {
135
- return JSON.stringify(arrayCheck.data);
136
- } catch {
137
- return "[...]";
138
- }
139
- }
140
- return `[...] (${String(arrayCheck.data.length)} items)`;
141
- }
142
-
143
- // Handle objects
144
- const recordCheck = RecordSchema.safeParse(value);
145
- if (recordCheck.success) {
146
- const keys = Object.keys(recordCheck.data);
147
- if (keys.length === 0) {
148
- return "{}";
149
- }
150
- if (keys.length <= 3) {
151
- try {
152
- return JSON.stringify(recordCheck.data);
153
- } catch {
154
- return "{...}";
155
- }
156
- }
157
- return `{...} (${String(keys.length)} keys)`;
158
- }
159
-
160
- // Primitives
161
- const stringCheck = StringSchema.safeParse(value);
162
- if (stringCheck.success) {
163
- // Truncate long strings
164
- if (stringCheck.data.length > 50) {
165
- return `"${stringCheck.data.slice(0, 47)}..."`;
166
- }
167
- return `"${stringCheck.data}"`;
168
- }
169
-
170
- // Handle other primitives (numbers, booleans, etc.)
171
- const numberCheck = ActualNumberSchema.safeParse(value);
172
- if (numberCheck.success) {
173
- return String(numberCheck.data);
174
- }
175
-
176
- const booleanCheck = ActualBooleanSchema.safeParse(value);
177
- if (booleanCheck.success) {
178
- return String(booleanCheck.data);
179
- }
180
-
181
- // Fallback for unknown types - try JSON.stringify
182
- try {
183
- return JSON.stringify(value);
184
- } catch {
185
- return "unknown";
186
- }
187
- }
188
-
189
- function generateParameterType(
190
- iface: TypeScriptInterface,
191
- chartName: string,
192
- ): string {
193
- const parameterKeys = flattenInterfaceKeys(iface);
194
-
195
- const normalizedChartName = capitalizeFirst(chartName).replaceAll("-", "");
196
- let code = `export type ${normalizedChartName}HelmParameters = {\n`;
197
-
198
- for (const key of parameterKeys) {
199
- code += ` "${key}"?: string;\n`;
200
- }
201
-
202
- code += "};\n";
203
-
204
- return code;
205
- }
206
-
207
- function flattenInterfaceKeys(
208
- iface: TypeScriptInterface,
209
- prefix = "",
210
- ): string[] {
211
- const keys: string[] = [];
212
-
213
- for (const [key, prop] of Object.entries(iface.properties)) {
214
- // Remove quotes from key for parameter names
215
- const cleanKey = key.replaceAll('"', "");
216
- const fullKey = prefix ? `${prefix}.${cleanKey}` : cleanKey;
217
-
218
- if (prop.nested) {
219
- keys.push(...flattenInterfaceKeys(prop.nested, fullKey));
220
- } else {
221
- keys.push(fullKey);
222
- }
223
- }
224
-
225
- return keys;
226
- }
@@ -1,548 +0,0 @@
1
- import type {
2
- JSONSchemaProperty,
3
- TypeScriptInterface,
4
- TypeProperty,
5
- } from "./types.ts";
6
- import type { HelmValue } from "./schemas.ts";
7
- import {
8
- StringSchema,
9
- ActualNumberSchema,
10
- ActualBooleanSchema,
11
- NullSchema,
12
- UndefinedSchema,
13
- ArraySchema,
14
- HelmValueSchema,
15
- StringBooleanSchema,
16
- } from "./schemas.ts";
17
- import {
18
- capitalizeFirst,
19
- sanitizePropertyName,
20
- sanitizeTypeName,
21
- } from "./utils.ts";
22
-
23
- /**
24
- * Convert JSON schema type to TypeScript type string
25
- */
26
- export function jsonSchemaToTypeScript(schema: JSONSchemaProperty): string {
27
- // Handle oneOf - union of types
28
- if (schema.oneOf) {
29
- const types = schema.oneOf.map((s) => jsonSchemaToTypeScript(s));
30
- return types.join(" | ");
31
- }
32
-
33
- // Handle anyOf - union of types
34
- if (schema.anyOf) {
35
- const types = schema.anyOf.map((s) => jsonSchemaToTypeScript(s));
36
- return types.join(" | ");
37
- }
38
-
39
- // Handle enum
40
- if (schema.enum) {
41
- return schema.enum
42
- .map((v) =>
43
- StringSchema.safeParse(v).success ? `"${String(v)}"` : String(v),
44
- )
45
- .join(" | ");
46
- }
47
-
48
- // Handle array type
49
- if (schema.type === "array" && schema.items) {
50
- const itemType = jsonSchemaToTypeScript(schema.items);
51
- return `${itemType}[]`;
52
- }
53
-
54
- // Handle basic types
55
- const stringTypeCheck = StringSchema.safeParse(schema.type);
56
- if (stringTypeCheck.success) {
57
- switch (stringTypeCheck.data) {
58
- case "string":
59
- return "string";
60
- case "number":
61
- case "integer":
62
- return "number";
63
- case "boolean":
64
- return "boolean";
65
- case "object":
66
- return "object";
67
- case "array":
68
- return "unknown[]";
69
- case "null":
70
- return "null";
71
- default:
72
- return "unknown";
73
- }
74
- }
75
-
76
- // Handle multiple types
77
- const arrayTypeCheck = ArraySchema.safeParse(schema.type);
78
- if (arrayTypeCheck.success) {
79
- return arrayTypeCheck.data
80
- .map((t: unknown) => {
81
- if (!StringSchema.safeParse(t).success) {
82
- return "unknown";
83
- }
84
- const typeStr = String(t);
85
- switch (typeStr) {
86
- case "string":
87
- return "string";
88
- case "number":
89
- case "integer":
90
- return "number";
91
- case "boolean":
92
- return "boolean";
93
- case "object":
94
- return "object";
95
- case "array":
96
- return "unknown[]";
97
- case "null":
98
- return "null";
99
- default:
100
- return "unknown";
101
- }
102
- })
103
- .join(" | ");
104
- }
105
-
106
- return "unknown";
107
- }
108
-
109
- /**
110
- * Infer TypeScript type from actual runtime value
111
- */
112
- export function inferTypeFromValue(value: unknown): string | null {
113
- // Check null/undefined
114
- if (
115
- NullSchema.safeParse(value).success ||
116
- UndefinedSchema.safeParse(value).success
117
- ) {
118
- return null;
119
- }
120
-
121
- // Check for actual boolean
122
- if (ActualBooleanSchema.safeParse(value).success) {
123
- return "boolean";
124
- }
125
-
126
- // Check for actual number
127
- if (ActualNumberSchema.safeParse(value).success) {
128
- return "number";
129
- }
130
-
131
- // Check if it's a string that looks like a boolean
132
- if (StringBooleanSchema.safeParse(value).success) {
133
- return "boolean";
134
- }
135
-
136
- // Check if it's a string that looks like a number
137
- const stringCheck = StringSchema.safeParse(value);
138
- if (stringCheck.success) {
139
- const trimmed = stringCheck.data.trim();
140
- if (
141
- trimmed !== "" &&
142
- !Number.isNaN(Number(trimmed)) &&
143
- Number.isFinite(Number(trimmed))
144
- ) {
145
- return "number";
146
- }
147
- }
148
-
149
- // Check for array
150
- if (ArraySchema.safeParse(value).success) {
151
- return "array";
152
- }
153
-
154
- // Check for object
155
- if (HelmValueSchema.safeParse(value).success) {
156
- return "object";
157
- }
158
-
159
- // Plain string
160
- if (StringSchema.safeParse(value).success) {
161
- return "string";
162
- }
163
-
164
- return "unknown";
165
- }
166
-
167
- /**
168
- * Check if inferred type is compatible with schema type
169
- */
170
- export function typesAreCompatible(
171
- inferredType: string,
172
- schemaType: string,
173
- ): boolean {
174
- // Exact match
175
- if (inferredType === schemaType) {
176
- return true;
177
- }
178
-
179
- // Check if the inferred type is part of a union in the schema
180
- // For example: schemaType might be "number | \"default\"" and inferredType is "string"
181
- const schemaTypes = schemaType
182
- .split("|")
183
- .map((t) => t.trim().replaceAll(/^["']|["']$/g, ""));
184
-
185
- // If schema is a union, check if inferred type is compatible with any part
186
- if (schemaTypes.length > 1) {
187
- for (const st of schemaTypes) {
188
- // Handle quoted strings in unions (like "default")
189
- if (inferredType === "string" && st.startsWith('"') && st.endsWith('"')) {
190
- return true;
191
- }
192
- if (st === inferredType) {
193
- return true;
194
- }
195
- // Arrays
196
- if (inferredType === "array" && st.endsWith("[]")) {
197
- return true;
198
- }
199
- }
200
- }
201
-
202
- // Handle array types
203
- if (inferredType === "array" && schemaType.endsWith("[]")) {
204
- return true;
205
- }
206
-
207
- // Handle specific string literals - if schema expects specific strings and value is a string
208
- if (inferredType === "string" && schemaType.includes('"')) {
209
- return true;
210
- }
211
-
212
- // unknown is compatible with everything (schema might be less specific)
213
- if (schemaType === "unknown" || inferredType === "unknown") {
214
- return true;
215
- }
216
-
217
- return false;
218
- }
219
-
220
- /**
221
- * Convert Helm values to TypeScript interface
222
- */
223
- export function convertToTypeScriptInterface(options: {
224
- values: HelmValue;
225
- interfaceName: string;
226
- schema?: JSONSchemaProperty | null;
227
- yamlComments?: Map<string, string>;
228
- keyPrefix?: string;
229
- }): TypeScriptInterface {
230
- const keyPrefix = options.keyPrefix ?? "";
231
- const properties: Record<string, TypeProperty> = {};
232
- const schemaProps = options.schema?.properties;
233
-
234
- for (const [key, value] of Object.entries(options.values)) {
235
- const sanitizedKey = sanitizePropertyName(key);
236
- const typeNameSuffix = sanitizeTypeName(key);
237
- const propertySchema = schemaProps?.[key];
238
- const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key;
239
- const yamlComment = options.yamlComments?.get(fullKey);
240
-
241
- properties[sanitizedKey] = convertValueToProperty({
242
- value,
243
- nestedTypeName: `${options.interfaceName}${capitalizeFirst(typeNameSuffix)}`,
244
- schema: propertySchema,
245
- propertyName: key,
246
- yamlComment,
247
- yamlComments: options.yamlComments,
248
- fullKey,
249
- });
250
- }
251
-
252
- return {
253
- name: options.interfaceName,
254
- properties,
255
- };
256
- }
257
-
258
- type InferencePropertyContext = {
259
- value: unknown;
260
- nestedTypeName: string;
261
- schema?: JSONSchemaProperty;
262
- propertyName?: string;
263
- yamlComment?: string;
264
- yamlComments?: Map<string, string>;
265
- fullKey?: string;
266
- };
267
-
268
- /**
269
- * Merge description from schema and YAML comments
270
- */
271
- function mergeDescriptions(
272
- schemaDescription: string | undefined,
273
- yamlComment: string | undefined,
274
- ): string | undefined {
275
- if (yamlComment === "" || yamlComment == null) {
276
- return schemaDescription;
277
- }
278
- return schemaDescription !== "" && schemaDescription != null
279
- ? `${yamlComment}\n\n${schemaDescription}`
280
- : yamlComment;
281
- }
282
-
283
- /**
284
- * Convert a value to a TypeProperty using JSON schema information
285
- */
286
- function convertWithSchema(
287
- ctx: InferencePropertyContext & { schema: JSONSchemaProperty },
288
- ): TypeProperty {
289
- const {
290
- value,
291
- nestedTypeName,
292
- schema,
293
- propertyName,
294
- yamlComment,
295
- yamlComments,
296
- fullKey,
297
- } = ctx;
298
-
299
- const inferredType = inferTypeFromValue(value);
300
- const schemaType = jsonSchemaToTypeScript(schema);
301
-
302
- if (
303
- inferredType !== "" &&
304
- inferredType != null &&
305
- !typesAreCompatible(inferredType, schemaType)
306
- ) {
307
- const propName =
308
- propertyName !== "" && propertyName != null ? `'${propertyName}': ` : "";
309
- console.warn(
310
- ` ⚠️ Type mismatch for ${propName}Schema says '${schemaType}' but value suggests '${inferredType}' (value: ${String(value).slice(0, 50)})`,
311
- );
312
- }
313
-
314
- const description = mergeDescriptions(schema.description, yamlComment);
315
- const defaultValue = schema.default === undefined ? value : schema.default;
316
-
317
- // If schema defines it as an object with properties, recurse
318
- const helmValueCheckForProps = HelmValueSchema.safeParse(value);
319
- if (schema.properties && helmValueCheckForProps.success) {
320
- const nestedInterface = convertToTypeScriptInterface({
321
- values: helmValueCheckForProps.data,
322
- interfaceName: nestedTypeName,
323
- schema,
324
- yamlComments,
325
- keyPrefix: fullKey,
326
- });
327
- return {
328
- type: nestedTypeName,
329
- optional: true,
330
- nested: nestedInterface,
331
- description,
332
- default: defaultValue,
333
- };
334
- }
335
-
336
- // Handle object types without explicit properties
337
- const helmValueCheckForObject = HelmValueSchema.safeParse(value);
338
- if (schemaType === "object" && helmValueCheckForObject.success) {
339
- const nestedInterface = convertToTypeScriptInterface({
340
- values: helmValueCheckForObject.data,
341
- interfaceName: nestedTypeName,
342
- yamlComments,
343
- keyPrefix: fullKey,
344
- });
345
- return {
346
- type: nestedTypeName,
347
- optional: true,
348
- nested: nestedInterface,
349
- description,
350
- default: defaultValue,
351
- };
352
- }
353
-
354
- return {
355
- type: schemaType,
356
- optional: true,
357
- description,
358
- default: defaultValue,
359
- };
360
- }
361
-
362
- /**
363
- * Infer array element type from sampled elements
364
- */
365
- function inferArrayType(
366
- nestedTypeName: string,
367
- arrayValue: unknown[],
368
- ): TypeProperty {
369
- if (arrayValue.length === 0) {
370
- return { type: "unknown[]", optional: true };
371
- }
372
-
373
- const elementTypes = new Set<string>();
374
- const elementTypeProps: TypeProperty[] = [];
375
- const sampleSize = Math.min(arrayValue.length, 3);
376
-
377
- for (let i = 0; i < sampleSize; i++) {
378
- const elementType = convertValueToProperty({
379
- value: arrayValue[i],
380
- nestedTypeName,
381
- });
382
- elementTypes.add(elementType.type);
383
- elementTypeProps.push(elementType);
384
- }
385
-
386
- if (elementTypes.size === 1) {
387
- return inferUniformArrayType(
388
- elementTypes,
389
- elementTypeProps,
390
- nestedTypeName,
391
- );
392
- }
393
-
394
- const types = [...elementTypes].toSorted();
395
- if (
396
- types.length <= 3 &&
397
- types.every((t) => ["string", "number", "boolean"].includes(t))
398
- ) {
399
- return { type: `(${types.join(" | ")})[]`, optional: true };
400
- }
401
-
402
- return { type: "unknown[]", optional: true };
403
- }
404
-
405
- /**
406
- * Build TypeProperty for a uniform-type array
407
- */
408
- function inferUniformArrayType(
409
- elementTypes: Set<string>,
410
- elementTypeProps: TypeProperty[],
411
- nestedTypeName: string,
412
- ): TypeProperty {
413
- const elementType = [...elementTypes][0];
414
- const elementProp = elementTypeProps[0];
415
- if (elementType === "" || !elementProp || elementType == null) {
416
- return { type: "unknown[]", optional: true };
417
- }
418
-
419
- if (elementProp.nested) {
420
- const arrayElementTypeName = `${nestedTypeName}Element`;
421
- const arrayElementInterface: TypeScriptInterface = {
422
- name: arrayElementTypeName,
423
- properties: elementProp.nested.properties,
424
- };
425
- return {
426
- type: `${arrayElementTypeName}[]`,
427
- optional: true,
428
- nested: arrayElementInterface,
429
- };
430
- }
431
-
432
- return { type: `${elementType}[]`, optional: true };
433
- }
434
-
435
- /**
436
- * Infer a primitive TypeProperty from a runtime value (no schema)
437
- */
438
- function inferPrimitiveType(
439
- value: unknown,
440
- yamlComment?: string,
441
- ): TypeProperty {
442
- if (ActualBooleanSchema.safeParse(value).success) {
443
- return {
444
- type: "boolean",
445
- optional: true,
446
- description: yamlComment,
447
- default: value,
448
- };
449
- }
450
-
451
- if (ActualNumberSchema.safeParse(value).success) {
452
- return {
453
- type: "number",
454
- optional: true,
455
- description: yamlComment,
456
- default: value,
457
- };
458
- }
459
-
460
- if (StringBooleanSchema.safeParse(value).success) {
461
- return {
462
- type: "boolean",
463
- optional: true,
464
- description: yamlComment,
465
- default: value,
466
- };
467
- }
468
-
469
- const stringCheckForNumber = StringSchema.safeParse(value);
470
- if (stringCheckForNumber.success) {
471
- const trimmed = stringCheckForNumber.data.trim();
472
- if (
473
- trimmed !== "" &&
474
- !Number.isNaN(Number(trimmed)) &&
475
- Number.isFinite(Number(trimmed))
476
- ) {
477
- return {
478
- type: "number",
479
- optional: true,
480
- description: yamlComment,
481
- default: value,
482
- };
483
- }
484
- }
485
-
486
- const stringCheckForPlain = StringSchema.safeParse(value);
487
- if (stringCheckForPlain.success) {
488
- if (stringCheckForPlain.data === "default") {
489
- return {
490
- type: "string | number | boolean",
491
- optional: true,
492
- description: yamlComment,
493
- default: value,
494
- };
495
- }
496
- return {
497
- type: "string",
498
- optional: true,
499
- description: yamlComment,
500
- default: value,
501
- };
502
- }
503
-
504
- console.warn(
505
- `Unrecognized value type for: ${String(value)}, using 'unknown'`,
506
- );
507
- return { type: "unknown", optional: true, description: yamlComment };
508
- }
509
-
510
- function convertValueToProperty(opts: InferencePropertyContext): TypeProperty {
511
- const { value, nestedTypeName, schema, yamlComment, yamlComments, fullKey } =
512
- opts;
513
-
514
- if (schema) {
515
- return convertWithSchema({ ...opts, schema });
516
- }
517
-
518
- if (
519
- NullSchema.safeParse(value).success ||
520
- UndefinedSchema.safeParse(value).success
521
- ) {
522
- return { type: "unknown", optional: true };
523
- }
524
-
525
- const arrayResult = ArraySchema.safeParse(value);
526
- if (arrayResult.success) {
527
- return inferArrayType(nestedTypeName, arrayResult.data);
528
- }
529
-
530
- const objectResult = HelmValueSchema.safeParse(value);
531
- if (objectResult.success) {
532
- const nestedInterface = convertToTypeScriptInterface({
533
- values: objectResult.data,
534
- interfaceName: nestedTypeName,
535
- yamlComments,
536
- keyPrefix: fullKey,
537
- });
538
- return {
539
- type: nestedTypeName,
540
- optional: true,
541
- nested: nestedInterface,
542
- description: yamlComment,
543
- default: value,
544
- };
545
- }
546
-
547
- return inferPrimitiveType(value, yamlComment);
548
- }