@shepherdjerred/helm-types 0.0.0-dev.706

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.
@@ -0,0 +1,238 @@
1
+ import type { TypeScriptInterface } from "./types.ts";
2
+ import {
3
+ ArraySchema,
4
+ RecordSchema,
5
+ StringSchema,
6
+ ActualNumberSchema,
7
+ ActualBooleanSchema,
8
+ } from "./schemas.ts";
9
+
10
+ /**
11
+ * Generate TypeScript code from interface definition
12
+ */
13
+ export function generateTypeScriptCode(
14
+ mainInterface: TypeScriptInterface,
15
+ chartName: string,
16
+ ): string {
17
+ const interfaces: TypeScriptInterface[] = [];
18
+
19
+ // Collect all nested interfaces
20
+ collectNestedInterfaces(mainInterface, interfaces);
21
+
22
+ let code = `// Generated TypeScript types for ${chartName} Helm chart\n\n`;
23
+
24
+ // Generate all interfaces
25
+ for (const iface of interfaces) {
26
+ code += generateInterfaceCode(iface);
27
+ code += "\n";
28
+ }
29
+
30
+ // Generate parameter type (flattened dot notation)
31
+ code += generateParameterType(mainInterface, chartName);
32
+
33
+ // Add header comment for generated files
34
+ if (code.includes(": any")) {
35
+ code = `// Generated TypeScript types for ${chartName} Helm chart
36
+
37
+ ${code.slice(Math.max(0, code.indexOf("\n\n") + 2))}`;
38
+ }
39
+
40
+ return code;
41
+ }
42
+
43
+ function collectNestedInterfaces(
44
+ iface: TypeScriptInterface,
45
+ collected: TypeScriptInterface[],
46
+ ): void {
47
+ for (const prop of Object.values(iface.properties)) {
48
+ if (prop.nested) {
49
+ collected.push(prop.nested);
50
+ collectNestedInterfaces(prop.nested, collected);
51
+ }
52
+ }
53
+
54
+ // Add main interface last so dependencies come first
55
+ if (!collected.some((i) => i.name === iface.name)) {
56
+ collected.push(iface);
57
+ }
58
+ }
59
+
60
+ function generateInterfaceCode(iface: TypeScriptInterface): string {
61
+ const hasProperties = Object.keys(iface.properties).length > 0;
62
+
63
+ if (!hasProperties && iface.allowArbitraryProps !== true) {
64
+ // Use 'object' for empty interfaces instead of '{}'
65
+ return `export type ${iface.name} = object;\n`;
66
+ }
67
+
68
+ let code = `export type ${iface.name} = {\n`;
69
+
70
+ // Add index signature if this type allows arbitrary properties
71
+ if (iface.allowArbitraryProps === true) {
72
+ code += ` /**\n`;
73
+ code += ` * This type allows arbitrary additional properties beyond those defined below.\n`;
74
+ code += ` * This is common for config maps, custom settings, and extensible configurations.\n`;
75
+ code += ` */\n`;
76
+ code += ` [key: string]: unknown;\n`;
77
+ }
78
+
79
+ for (const [key, prop] of Object.entries(iface.properties)) {
80
+ const optional = prop.optional ? "?" : "";
81
+
82
+ // Generate JSDoc comment if we have description or default
83
+ if (
84
+ (prop.description != null && prop.description !== "") ||
85
+ prop.default !== undefined
86
+ ) {
87
+ code += ` /**\n`;
88
+
89
+ if (prop.description != null && prop.description !== "") {
90
+ // Format multi-line descriptions properly with " * " prefix
91
+ // Escape */ sequences to prevent premature comment closure
92
+ const escapedDescription = prop.description.replaceAll(
93
+ "*/",
94
+ String.raw`*\/`,
95
+ );
96
+ const descLines = escapedDescription.split("\n");
97
+ for (const line of descLines) {
98
+ code += ` * ${line}\n`;
99
+ }
100
+ }
101
+
102
+ if (prop.default !== undefined) {
103
+ const defaultStr = formatDefaultValue(prop.default);
104
+ const hasDescription =
105
+ prop.description != null && prop.description !== "";
106
+ if (defaultStr != null && defaultStr !== "" && hasDescription) {
107
+ code += ` *\n`;
108
+ }
109
+ if (defaultStr != null && defaultStr !== "") {
110
+ code += ` * @default ${defaultStr}\n`;
111
+ }
112
+ }
113
+
114
+ code += ` */\n`;
115
+ }
116
+
117
+ code += ` ${key}${optional}: ${prop.type};\n`;
118
+ }
119
+
120
+ code += "};\n";
121
+ return code;
122
+ }
123
+
124
+ /**
125
+ * Format a default value for display in JSDoc
126
+ */
127
+ function formatDefaultValue(value: unknown): string | null {
128
+ if (value === null) {
129
+ return "null";
130
+ }
131
+ if (value === undefined) {
132
+ return null;
133
+ }
134
+
135
+ // Handle arrays
136
+ const arrayCheck = ArraySchema.safeParse(value);
137
+ if (arrayCheck.success) {
138
+ if (arrayCheck.data.length === 0) {
139
+ return "[]";
140
+ }
141
+ if (arrayCheck.data.length <= 3) {
142
+ try {
143
+ return JSON.stringify(arrayCheck.data);
144
+ } catch {
145
+ return "[...]";
146
+ }
147
+ }
148
+ return `[...] (${String(arrayCheck.data.length)} items)`;
149
+ }
150
+
151
+ // Handle objects
152
+ const recordCheck = RecordSchema.safeParse(value);
153
+ if (recordCheck.success) {
154
+ const keys = Object.keys(recordCheck.data);
155
+ if (keys.length === 0) {
156
+ return "{}";
157
+ }
158
+ if (keys.length <= 3) {
159
+ try {
160
+ return JSON.stringify(recordCheck.data);
161
+ } catch {
162
+ return "{...}";
163
+ }
164
+ }
165
+ return `{...} (${String(keys.length)} keys)`;
166
+ }
167
+
168
+ // Primitives
169
+ const stringCheck = StringSchema.safeParse(value);
170
+ if (stringCheck.success) {
171
+ // Truncate long strings
172
+ if (stringCheck.data.length > 50) {
173
+ return `"${stringCheck.data.slice(0, 47)}..."`;
174
+ }
175
+ return `"${stringCheck.data}"`;
176
+ }
177
+
178
+ // Handle other primitives (numbers, booleans, etc.)
179
+ const numberCheck = ActualNumberSchema.safeParse(value);
180
+ if (numberCheck.success) {
181
+ return String(numberCheck.data);
182
+ }
183
+
184
+ const booleanCheck = ActualBooleanSchema.safeParse(value);
185
+ if (booleanCheck.success) {
186
+ return String(booleanCheck.data);
187
+ }
188
+
189
+ // Fallback for unknown types - try JSON.stringify
190
+ try {
191
+ return JSON.stringify(value);
192
+ } catch {
193
+ return "unknown";
194
+ }
195
+ }
196
+
197
+ function generateParameterType(
198
+ iface: TypeScriptInterface,
199
+ chartName: string,
200
+ ): string {
201
+ const parameterKeys = flattenInterfaceKeys(iface);
202
+
203
+ const normalizedChartName = capitalizeFirst(chartName).replaceAll("-", "");
204
+ let code = `export type ${normalizedChartName}HelmParameters = {\n`;
205
+
206
+ for (const key of parameterKeys) {
207
+ code += ` "${key}"?: string;\n`;
208
+ }
209
+
210
+ code += "};\n";
211
+
212
+ return code;
213
+ }
214
+
215
+ function flattenInterfaceKeys(
216
+ iface: TypeScriptInterface,
217
+ prefix = "",
218
+ ): string[] {
219
+ const keys: string[] = [];
220
+
221
+ for (const [key, prop] of Object.entries(iface.properties)) {
222
+ // Remove quotes from key for parameter names
223
+ const cleanKey = key.replaceAll('"', "");
224
+ const fullKey = prefix ? `${prefix}.${cleanKey}` : cleanKey;
225
+
226
+ if (prop.nested) {
227
+ keys.push(...flattenInterfaceKeys(prop.nested, fullKey));
228
+ } else {
229
+ keys.push(fullKey);
230
+ }
231
+ }
232
+
233
+ return keys;
234
+ }
235
+
236
+ function capitalizeFirst(str: string): string {
237
+ return str.charAt(0).toUpperCase() + str.slice(1);
238
+ }
package/src/reset.d.ts ADDED
@@ -0,0 +1 @@
1
+ import "@total-typescript/ts-reset";
package/src/schemas.ts ADDED
@@ -0,0 +1,39 @@
1
+ // Zod schemas for runtime validation
2
+ import { z } from "zod";
3
+
4
+ // Individual Zod schemas for type detection
5
+ export const StringSchema = z.string();
6
+ export const ActualNumberSchema = z.number(); // Runtime number
7
+ export const ActualBooleanSchema = z.boolean(); // Runtime boolean
8
+ export const NullSchema = z.null();
9
+ export const UndefinedSchema = z.undefined();
10
+ export const ArraySchema = z.array(z.unknown());
11
+ export const RecordSchema = z.record(z.string(), z.unknown());
12
+ export const ErrorSchema = z.instanceof(Error);
13
+
14
+ // Custom boolean string parser - only matches actual boolean-like strings
15
+ export const StringBooleanSchema = z.string().refine((val) => {
16
+ const lower = val.toLowerCase();
17
+ return (
18
+ lower === "true" || lower === "false" || lower === "yes" || lower === "no"
19
+ );
20
+ }, "Not a boolean string");
21
+
22
+ // Zod schema for validating YAML values - recursive definition with more flexibility
23
+ export const HelmValueSchema: z.ZodType<Record<string, unknown>> = z.lazy(() =>
24
+ z.record(
25
+ z.string(),
26
+ z.union([
27
+ z.string(),
28
+ z.number(),
29
+ z.boolean(),
30
+ z.null(),
31
+ z.undefined(),
32
+ z.array(z.unknown()), // Allow arrays of any type
33
+ HelmValueSchema,
34
+ z.unknown(), // Allow any other values as fallback
35
+ ]),
36
+ ),
37
+ );
38
+
39
+ export type HelmValue = z.infer<typeof HelmValueSchema>;
@@ -0,0 +1,180 @@
1
+ import type {
2
+ JSONSchemaProperty,
3
+ TypeScriptInterface,
4
+ TypeProperty,
5
+ } from "./types.ts";
6
+ import {
7
+ StringSchema,
8
+ ActualNumberSchema,
9
+ ActualBooleanSchema,
10
+ StringBooleanSchema,
11
+ } from "./schemas.ts";
12
+
13
+ export type PropertyConversionContext = {
14
+ value: unknown;
15
+ nestedTypeName: string;
16
+ schema?: JSONSchemaProperty;
17
+ propertyName?: string;
18
+ yamlComment?: string;
19
+ yamlComments?: Map<string, string>;
20
+ fullKey?: string;
21
+ chartName?: string;
22
+ };
23
+
24
+ /**
25
+ * Merge description from schema and YAML comments
26
+ */
27
+ export function mergeDescriptions(
28
+ schemaDescription: string | undefined,
29
+ yamlComment: string | undefined,
30
+ ): string | undefined {
31
+ if (yamlComment == null || yamlComment === "") {
32
+ return schemaDescription;
33
+ }
34
+ return schemaDescription != null && schemaDescription !== ""
35
+ ? `${yamlComment}\n\n${schemaDescription}`
36
+ : yamlComment;
37
+ }
38
+
39
+ /**
40
+ * Infer a primitive TypeProperty from a runtime value (no schema)
41
+ */
42
+ export function inferPrimitiveType(
43
+ value: unknown,
44
+ yamlComment?: string,
45
+ ): TypeProperty {
46
+ if (ActualBooleanSchema.safeParse(value).success) {
47
+ return {
48
+ type: "boolean",
49
+ optional: true,
50
+ description: yamlComment,
51
+ default: value,
52
+ };
53
+ }
54
+
55
+ if (ActualNumberSchema.safeParse(value).success) {
56
+ return {
57
+ type: "number",
58
+ optional: true,
59
+ description: yamlComment,
60
+ default: value,
61
+ };
62
+ }
63
+
64
+ if (StringBooleanSchema.safeParse(value).success) {
65
+ return {
66
+ type: "boolean",
67
+ optional: true,
68
+ description: yamlComment,
69
+ default: value,
70
+ };
71
+ }
72
+
73
+ const stringCheckForNumber = StringSchema.safeParse(value);
74
+ if (stringCheckForNumber.success) {
75
+ const trimmed = stringCheckForNumber.data.trim();
76
+ if (
77
+ trimmed !== "" &&
78
+ !Number.isNaN(Number(trimmed)) &&
79
+ Number.isFinite(Number(trimmed))
80
+ ) {
81
+ return {
82
+ type: "number",
83
+ optional: true,
84
+ description: yamlComment,
85
+ default: value,
86
+ };
87
+ }
88
+ }
89
+
90
+ const stringCheckForPlain = StringSchema.safeParse(value);
91
+ if (stringCheckForPlain.success) {
92
+ if (stringCheckForPlain.data === "default") {
93
+ return {
94
+ type: "string | number | boolean",
95
+ optional: true,
96
+ description: yamlComment,
97
+ default: value,
98
+ };
99
+ }
100
+ return {
101
+ type: "string",
102
+ optional: true,
103
+ description: yamlComment,
104
+ default: value,
105
+ };
106
+ }
107
+
108
+ console.warn(
109
+ `Unrecognized value type for: ${String(value)}, using 'unknown'`,
110
+ );
111
+ return { type: "unknown", optional: true, description: yamlComment };
112
+ }
113
+
114
+ /**
115
+ * Augment a Kubernetes resource spec interface with both requests and limits.
116
+ * If only one is present, copy its type structure to the other.
117
+ */
118
+ export function augmentK8sResourceSpec(iface: TypeScriptInterface): void {
119
+ const hasRequests = "requests" in iface.properties;
120
+ const hasLimits = "limits" in iface.properties;
121
+
122
+ // If we have requests but not limits, add limits with the same structure
123
+ if (hasRequests && !hasLimits) {
124
+ const requestsProp = iface.properties["requests"];
125
+ if (requestsProp) {
126
+ // Create limits property with the same type but different name for the nested interface
127
+ const limitsTypeName = requestsProp.type.replace("Requests", "Limits");
128
+
129
+ // If there's a nested interface, create a copy for limits
130
+ if (requestsProp.nested) {
131
+ const limitsNested: TypeScriptInterface = {
132
+ name: limitsTypeName,
133
+ properties: { ...requestsProp.nested.properties },
134
+ allowArbitraryProps: requestsProp.nested.allowArbitraryProps,
135
+ };
136
+ iface.properties["limits"] = {
137
+ type: limitsTypeName,
138
+ optional: true,
139
+ nested: limitsNested,
140
+ description: "Kubernetes resource limits (memory, cpu, etc.)",
141
+ };
142
+ } else {
143
+ // No nested interface, just copy the type
144
+ iface.properties["limits"] = {
145
+ type: requestsProp.type,
146
+ optional: true,
147
+ description: "Kubernetes resource limits (memory, cpu, etc.)",
148
+ };
149
+ }
150
+ }
151
+ }
152
+
153
+ // If we have limits but not requests, add requests with the same structure
154
+ if (hasLimits && !hasRequests) {
155
+ const limitsProp = iface.properties["limits"];
156
+ if (limitsProp) {
157
+ const requestsTypeName = limitsProp.type.replace("Limits", "Requests");
158
+
159
+ if (limitsProp.nested) {
160
+ const requestsNested: TypeScriptInterface = {
161
+ name: requestsTypeName,
162
+ properties: { ...limitsProp.nested.properties },
163
+ allowArbitraryProps: limitsProp.nested.allowArbitraryProps,
164
+ };
165
+ iface.properties["requests"] = {
166
+ type: requestsTypeName,
167
+ optional: true,
168
+ nested: requestsNested,
169
+ description: "Kubernetes resource requests (memory, cpu, etc.)",
170
+ };
171
+ } else {
172
+ iface.properties["requests"] = {
173
+ type: limitsProp.type,
174
+ optional: true,
175
+ description: "Kubernetes resource requests (memory, cpu, etc.)",
176
+ };
177
+ }
178
+ }
179
+ }
180
+ }