@shepherdjerred/helm-types 1.1.0 → 1.2.0-dev.891

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.
@@ -1,72 +1,83 @@
1
+ /**
2
+ * Check if a line looks like an example/code block rather than documentation
3
+ */
4
+ function isExampleLine(line: string): boolean {
5
+ return (
6
+ /^-{3,}/.test(line) ||
7
+ /^BEGIN .*(?:KEY|CERTIFICATE)/.test(line) ||
8
+ /^END .*(?:KEY|CERTIFICATE)/.test(line) ||
9
+ (line.startsWith("-") && (line.includes(":") || /^-\s+\|/.test(line))) ||
10
+ /^\w+:$/.test(line) ||
11
+ /^[\w-]+:\s*$/.test(line) ||
12
+ /^[\w.-]+:\s*\|/.test(line) || // YAML multiline indicator (e.g., "policy.csv: |")
13
+ line.startsWith("|") ||
14
+ line.includes("$ARGOCD_") ||
15
+ line.includes("$KUBE_") ||
16
+ /^\s{2,}/.test(line) ||
17
+ /^echo\s+/.test(line) ||
18
+ /^[pg],\s*/.test(line) // Policy rules like "p, role:..." or "g, subject, ..."
19
+ );
20
+ }
21
+
22
+ /**
23
+ * Check if a line looks like normal prose (sentence case, punctuation)
24
+ */
25
+ function looksLikeProse(line: string): boolean {
26
+ return (
27
+ /^[A-Z][\w\s]+[.!?]$/.test(line) ||
28
+ /^[A-Z][\w\s,'"-]+(?::\s*)?$/.test(line) ||
29
+ line.startsWith("Ref:") ||
30
+ line.startsWith("See:") ||
31
+ line.startsWith("http://") ||
32
+ line.startsWith("https://")
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Strip YAML comment markers from a single line
38
+ */
39
+ function stripCommentMarkers(line: string): string {
40
+ let result = line.replace(/^#+\s*/, "");
41
+ result = result.replace(/^--\s*/, "");
42
+ result = result.replace(/^##\s*/, "");
43
+ return result.trim();
44
+ }
45
+
1
46
  /**
2
47
  * Clean up YAML comment text for use in JSDoc
3
48
  */
4
49
  export function cleanYAMLComment(comment: string): string {
5
- const lines = comment.split("\n").map((line) => {
6
- // Remove leading # symbols
7
- line = line.replace(/^#+\s*/, "");
8
- // Remove common Helm chart comment markers
9
- line = line.replace(/^--\s*/, "");
10
- line = line.replace(/^##\s*/, "");
11
- return line.trim();
12
- });
50
+ const lines = comment.split("\n").map((line) => stripCommentMarkers(line));
13
51
 
14
52
  // Filter and clean lines
15
53
  const cleaned: string[] = [];
16
54
  let inCodeBlock = false;
17
55
 
18
- for (const line of lines) {
19
- const currentLine = line;
20
-
21
- // Skip empty lines
56
+ for (const currentLine of lines) {
22
57
  if (currentLine.length === 0) {
23
- if (inCodeBlock) inCodeBlock = false;
58
+ if (inCodeBlock) {
59
+ inCodeBlock = false;
60
+ }
24
61
  continue;
25
62
  }
26
63
 
27
- // Skip @default lines (we'll generate our own)
28
- if (currentLine.startsWith("@default")) continue;
29
-
30
- // Detect various patterns that indicate this is an example/code block, not documentation
31
- const isExample =
32
- /^-{3,}/.test(currentLine) ||
33
- /^BEGIN .*(KEY|CERTIFICATE)/.test(currentLine) ||
34
- /^END .*(KEY|CERTIFICATE)/.test(currentLine) ||
35
- (currentLine.startsWith("-") && (currentLine.includes(":") || /^-\s+\|/.test(currentLine))) ||
36
- /^\w+:$/.test(currentLine) ||
37
- /^[\w-]+:\s*$/.test(currentLine) ||
38
- /^[\w.-]+:\s*\|/.test(currentLine) || // YAML multiline indicator (e.g., "policy.csv: |")
39
- currentLine.startsWith("|") ||
40
- currentLine.includes("$ARGOCD_") ||
41
- currentLine.includes("$KUBE_") ||
42
- /^\s{2,}/.test(currentLine) ||
43
- /^echo\s+/.test(currentLine) ||
44
- /^[pg],\s*/.test(currentLine); // Policy rules like "p, role:..." or "g, subject, ..."
45
-
46
- if (isExample) {
64
+ if (currentLine.startsWith("@default")) {
65
+ continue;
66
+ }
67
+
68
+ if (isExampleLine(currentLine)) {
47
69
  inCodeBlock = true;
48
70
  continue;
49
71
  }
50
72
 
51
- // If we're in a code block, skip until we hit normal prose
52
73
  if (inCodeBlock) {
53
- // Check if this line looks like normal prose (sentence case, punctuation)
54
- const looksLikeProse =
55
- /^[A-Z][\w\s]+[.!?]$/.test(currentLine) ||
56
- /^[A-Z][\w\s,'"-]+:?\s*$/.test(currentLine) ||
57
- currentLine.startsWith("Ref:") ||
58
- currentLine.startsWith("See:") ||
59
- currentLine.startsWith("http://") ||
60
- currentLine.startsWith("https://");
61
-
62
- if (looksLikeProse) {
74
+ if (looksLikeProse(currentLine)) {
63
75
  inCodeBlock = false;
64
76
  } else {
65
77
  continue;
66
78
  }
67
79
  }
68
80
 
69
- // Keep the line
70
81
  cleaned.push(currentLine);
71
82
  }
72
83
 
@@ -96,7 +107,7 @@ export function parseYAMLComments(yamlContent: string): Map<string, string> {
96
107
 
97
108
  // Check if line is a comment
98
109
  if (trimmed.startsWith("#")) {
99
- const comment = trimmed.substring(1).trim();
110
+ const comment = trimmed.slice(1).trim();
100
111
  if (comment) {
101
112
  // Track the indentation level of the comment
102
113
  const commentIndent = currentLine.search(/\S/);
@@ -106,35 +117,48 @@ export function parseYAMLComments(yamlContent: string): Map<string, string> {
106
117
  }
107
118
 
108
119
  // Check if line has a key
109
- const keyMatch = /^(\s*)([a-zA-Z0-9_-]+)\s*:/.exec(currentLine);
120
+ const keyMatch = /^(\s*)([\w-]+)\s*:/.exec(currentLine);
110
121
  if (keyMatch) {
111
122
  const indent = keyMatch[1]?.length ?? 0;
112
123
  const key = keyMatch[2];
113
- if (!key) continue;
124
+ if (key == null || key === "") {
125
+ continue;
126
+ }
114
127
 
115
128
  // Update indent stack
116
- const lastIndent = indentStack[indentStack.length - 1];
117
- while (indentStack.length > 0 && lastIndent && lastIndent.indent >= indent) {
129
+ const lastIndent = indentStack.at(-1);
130
+ while (
131
+ indentStack.length > 0 &&
132
+ lastIndent &&
133
+ lastIndent.indent >= indent
134
+ ) {
118
135
  indentStack.pop();
119
136
  }
120
137
 
121
138
  // Build full key path
122
- const keyPath = indentStack.length > 0 ? `${indentStack.map((s) => s.key).join(".")}.${key}` : key;
139
+ const keyPath =
140
+ indentStack.length > 0
141
+ ? `${indentStack.map((s) => s.key).join(".")}.${key}`
142
+ : key;
123
143
 
124
144
  // Check for inline comment
125
- const inlineCommentMatch = /#\s*(.+)$/.exec(currentLine);
126
- if (inlineCommentMatch?.[1]) {
145
+ const inlineCommentMatch = /#\s*(\S.*)$/.exec(currentLine);
146
+ if (inlineCommentMatch?.[1] != null && inlineCommentMatch[1] !== "") {
127
147
  pendingComments.push({ text: inlineCommentMatch[1].trim(), indent });
128
148
  }
129
149
 
130
150
  // Filter pending comments to only those at the same or shallower indent level as this key
131
151
  // This prevents comments from deeper nested properties being associated with a shallower property
132
- const relevantComments = pendingComments.filter((c) => c.indent <= indent);
152
+ const relevantComments = pendingComments.filter(
153
+ (c) => c.indent <= indent,
154
+ );
133
155
 
134
156
  // Associate relevant comments with this key
135
157
  if (relevantComments.length > 0) {
136
158
  // Join and clean comments
137
- const commentText = cleanYAMLComment(relevantComments.map((c) => c.text).join("\n"));
159
+ const commentText = cleanYAMLComment(
160
+ relevantComments.map((c) => c.text).join("\n"),
161
+ );
138
162
  if (commentText) {
139
163
  comments.set(keyPath, commentText);
140
164
  }
package/src/config.ts CHANGED
@@ -17,7 +17,9 @@ export const K8S_RESOURCE_SPEC_PATTERN = {
17
17
  * Check if a property name indicates a Kubernetes resource spec.
18
18
  */
19
19
  export function isK8sResourceSpec(propertyName: string): boolean {
20
- return K8S_RESOURCE_SPEC_PATTERN.resourceSpecNames.includes(propertyName.toLowerCase());
20
+ return K8S_RESOURCE_SPEC_PATTERN.resourceSpecNames.includes(
21
+ propertyName.toLowerCase(),
22
+ );
21
23
  }
22
24
 
23
25
  /**
@@ -86,7 +88,10 @@ export function shouldAllowArbitraryProps(
86
88
  const patterns = EXTENSIBLE_TYPE_PATTERNS[chartName];
87
89
  if (patterns) {
88
90
  for (const pattern of patterns) {
89
- if (pattern === keyPath || (pattern === "" && keyPath.split(".").length === 1)) {
91
+ if (
92
+ pattern === keyPath ||
93
+ (pattern === "" && keyPath.split(".").length === 1)
94
+ ) {
90
95
  return true;
91
96
  }
92
97
  // Also match if keyPath starts with pattern
@@ -126,11 +131,13 @@ export function shouldAllowArbitraryProps(
126
131
  }
127
132
 
128
133
  // Check YAML comments for hints
129
- if (yamlComment) {
134
+ if (yamlComment != null && yamlComment !== "") {
130
135
  const commentLower = yamlComment.toLowerCase();
131
136
  if (
132
- /\b(arbitrary|custom|additional|extra|any)\s+(keys?|properties?|fields?|values?)\b/i.exec(commentLower) ||
133
- /\bkey[\s-]?value\s+pairs?\b/i.exec(commentLower)
137
+ /\b(?:arbitrary|custom|additional|extra|any)\s+(?:keys?|properties?|fields?|values?)\b/i.test(
138
+ commentLower,
139
+ ) ||
140
+ /\bkey[\s-]?value\s+pairs?\b/i.test(commentLower)
134
141
  ) {
135
142
  return true;
136
143
  }
package/src/helm-types.ts CHANGED
@@ -1,46 +1,16 @@
1
- // Main entry point for Helm type generation
2
- // This file re-exports all functionality from the modular components
3
-
4
- // Core types
5
- export type { ChartInfo, JSONSchemaProperty, TypeScriptInterface, TypeProperty } from "./types.js";
6
-
7
- // Schemas
8
- export type { HelmValue } from "./schemas.js";
9
- export {
10
- StringSchema,
11
- ActualNumberSchema,
12
- ActualBooleanSchema,
13
- NullSchema,
14
- UndefinedSchema,
15
- ArraySchema,
16
- RecordSchema,
17
- ErrorSchema,
18
- StringBooleanSchema,
19
- HelmValueSchema,
20
- } from "./schemas.js";
21
-
22
- // Configuration
23
- export { EXTENSIBLE_TYPE_PATTERNS, shouldAllowArbitraryProps } from "./config.js";
24
-
25
- // Chart info parsing
26
- export { parseChartInfoFromVersions } from "./chart-info-parser.js";
27
-
28
- // YAML comments
29
- export { cleanYAMLComment, parseYAMLComments } from "./yaml-comments.js";
30
-
31
- // Chart fetching
32
- export { fetchHelmChart } from "./chart-fetcher.js";
33
-
34
- // Type conversion
35
- export {
36
- jsonSchemaToTypeScript,
37
- inferTypeFromValue,
38
- typesAreCompatible,
39
- convertToTypeScriptInterface,
40
- } from "./type-converter.js";
41
-
42
- // Code generation
43
- export { generateTypeScriptCode } from "./interface-generator.js";
44
-
45
- // Utilities
46
- export { sanitizePropertyName, sanitizeTypeName, capitalizeFirst } from "./utils.js";
1
+ /**
2
+ * This file previously served as a barrel/re-export module.
3
+ * Import directly from submodules instead:
4
+ * - ./types.ts - Core types
5
+ * - ./schemas.ts - Zod schemas
6
+ * - ./config.ts - Configuration
7
+ * - ./chart-info-parser.ts - Chart info parsing
8
+ * - ./yaml-comments.ts - YAML comments
9
+ * - ./chart-fetcher.ts - Chart fetching
10
+ * - ./type-converter.ts - Type conversion
11
+ * - ./interface-generator.ts - Code generation
12
+ * - ./utils.ts - Utilities
13
+ */
14
+
15
+ /** Version marker for the helm-types module. */
16
+ export const HELM_TYPES_VERSION = "1.1.0";
package/src/index.ts CHANGED
@@ -11,5 +11,18 @@
11
11
  *
12
12
  * This is a general-purpose library that can be used with any Helm chart.
13
13
  * Application-specific logic should be kept in your application code.
14
+ *
15
+ * Import directly from submodules:
16
+ * - ./types.ts - Core types (ChartInfo, JSONSchemaProperty, TypeScriptInterface, TypeProperty)
17
+ * - ./schemas.ts - Zod schemas (HelmValueSchema, StringSchema, etc.)
18
+ * - ./config.ts - Configuration (EXTENSIBLE_TYPE_PATTERNS, shouldAllowArbitraryProps)
19
+ * - ./chart-info-parser.ts - Chart info parsing (parseChartInfoFromVersions)
20
+ * - ./yaml-comments.ts - YAML comments (cleanYAMLComment, parseYAMLComments)
21
+ * - ./chart-fetcher.ts - Chart fetching (fetchHelmChart)
22
+ * - ./type-converter.ts - Type conversion (jsonSchemaToTypeScript, inferTypeFromValue, etc.)
23
+ * - ./interface-generator.ts - Code generation (generateTypeScriptCode)
24
+ * - ./utils.ts - Utilities (sanitizePropertyName, sanitizeTypeName, capitalizeFirst)
14
25
  */
15
- export * from "./helm-types.ts";
26
+
27
+ /** Version marker for the helm-types package. */
28
+ export const HELM_TYPES_PACKAGE_VERSION = "1.1.0";
@@ -1,10 +1,19 @@
1
- import type { TypeScriptInterface } from "./types.js";
2
- import { ArraySchema, RecordSchema, StringSchema, ActualNumberSchema, ActualBooleanSchema } from "./schemas.js";
1
+ import type { TypeScriptInterface } from "./types.ts";
2
+ import {
3
+ ArraySchema,
4
+ RecordSchema,
5
+ StringSchema,
6
+ ActualNumberSchema,
7
+ ActualBooleanSchema,
8
+ } from "./schemas.ts";
3
9
 
4
10
  /**
5
11
  * Generate TypeScript code from interface definition
6
12
  */
7
- export function generateTypeScriptCode(mainInterface: TypeScriptInterface, chartName: string): string {
13
+ export function generateTypeScriptCode(
14
+ mainInterface: TypeScriptInterface,
15
+ chartName: string,
16
+ ): string {
8
17
  const interfaces: TypeScriptInterface[] = [];
9
18
 
10
19
  // Collect all nested interfaces
@@ -21,18 +30,20 @@ export function generateTypeScriptCode(mainInterface: TypeScriptInterface, chart
21
30
  // Generate parameter type (flattened dot notation)
22
31
  code += generateParameterType(mainInterface, chartName);
23
32
 
24
- // Check if any 'any' types were generated and add ESLint disable if needed
33
+ // Add header comment for generated files
25
34
  if (code.includes(": any")) {
26
35
  code = `// Generated TypeScript types for ${chartName} Helm chart
27
- /* eslint-disable @typescript-eslint/no-explicit-any */
28
36
 
29
- ${code.substring(code.indexOf("\n\n") + 2)}`;
37
+ ${code.slice(Math.max(0, code.indexOf("\n\n") + 2))}`;
30
38
  }
31
39
 
32
40
  return code;
33
41
  }
34
42
 
35
- function collectNestedInterfaces(iface: TypeScriptInterface, collected: TypeScriptInterface[]): void {
43
+ function collectNestedInterfaces(
44
+ iface: TypeScriptInterface,
45
+ collected: TypeScriptInterface[],
46
+ ): void {
36
47
  for (const prop of Object.values(iface.properties)) {
37
48
  if (prop.nested) {
38
49
  collected.push(prop.nested);
@@ -49,7 +60,7 @@ function collectNestedInterfaces(iface: TypeScriptInterface, collected: TypeScri
49
60
  function generateInterfaceCode(iface: TypeScriptInterface): string {
50
61
  const hasProperties = Object.keys(iface.properties).length > 0;
51
62
 
52
- if (!hasProperties && !iface.allowArbitraryProps) {
63
+ if (!hasProperties && iface.allowArbitraryProps !== true) {
53
64
  // Use 'object' for empty interfaces instead of '{}'
54
65
  return `export type ${iface.name} = object;\n`;
55
66
  }
@@ -57,7 +68,7 @@ function generateInterfaceCode(iface: TypeScriptInterface): string {
57
68
  let code = `export type ${iface.name} = {\n`;
58
69
 
59
70
  // Add index signature if this type allows arbitrary properties
60
- if (iface.allowArbitraryProps) {
71
+ if (iface.allowArbitraryProps === true) {
61
72
  code += ` /**\n`;
62
73
  code += ` * This type allows arbitrary additional properties beyond those defined below.\n`;
63
74
  code += ` * This is common for config maps, custom settings, and extensible configurations.\n`;
@@ -69,13 +80,19 @@ function generateInterfaceCode(iface: TypeScriptInterface): string {
69
80
  const optional = prop.optional ? "?" : "";
70
81
 
71
82
  // Generate JSDoc comment if we have description or default
72
- if (prop.description || prop.default !== undefined) {
83
+ if (
84
+ (prop.description != null && prop.description !== "") ||
85
+ prop.default !== undefined
86
+ ) {
73
87
  code += ` /**\n`;
74
88
 
75
- if (prop.description) {
89
+ if (prop.description != null && prop.description !== "") {
76
90
  // Format multi-line descriptions properly with " * " prefix
77
91
  // Escape */ sequences to prevent premature comment closure
78
- const escapedDescription = prop.description.replace(/\*\//g, "*\\/");
92
+ const escapedDescription = prop.description.replaceAll(
93
+ "*/",
94
+ String.raw`*\/`,
95
+ );
79
96
  const descLines = escapedDescription.split("\n");
80
97
  for (const line of descLines) {
81
98
  code += ` * ${line}\n`;
@@ -84,8 +101,12 @@ function generateInterfaceCode(iface: TypeScriptInterface): string {
84
101
 
85
102
  if (prop.default !== undefined) {
86
103
  const defaultStr = formatDefaultValue(prop.default);
87
- if (defaultStr) {
88
- if (prop.description) code += ` *\n`;
104
+ const hasDescription =
105
+ prop.description != null && prop.description !== "";
106
+ if (defaultStr != null && defaultStr !== "" && hasDescription) {
107
+ code += ` *\n`;
108
+ }
109
+ if (defaultStr != null && defaultStr !== "") {
89
110
  code += ` * @default ${defaultStr}\n`;
90
111
  }
91
112
  }
@@ -104,13 +125,19 @@ function generateInterfaceCode(iface: TypeScriptInterface): string {
104
125
  * Format a default value for display in JSDoc
105
126
  */
106
127
  function formatDefaultValue(value: unknown): string | null {
107
- if (value === null) return "null";
108
- if (value === undefined) return null;
128
+ if (value === null) {
129
+ return "null";
130
+ }
131
+ if (value === undefined) {
132
+ return null;
133
+ }
109
134
 
110
135
  // Handle arrays
111
136
  const arrayCheck = ArraySchema.safeParse(value);
112
137
  if (arrayCheck.success) {
113
- if (arrayCheck.data.length === 0) return "[]";
138
+ if (arrayCheck.data.length === 0) {
139
+ return "[]";
140
+ }
114
141
  if (arrayCheck.data.length <= 3) {
115
142
  try {
116
143
  return JSON.stringify(arrayCheck.data);
@@ -125,7 +152,9 @@ function formatDefaultValue(value: unknown): string | null {
125
152
  const recordCheck = RecordSchema.safeParse(value);
126
153
  if (recordCheck.success) {
127
154
  const keys = Object.keys(recordCheck.data);
128
- if (keys.length === 0) return "{}";
155
+ if (keys.length === 0) {
156
+ return "{}";
157
+ }
129
158
  if (keys.length <= 3) {
130
159
  try {
131
160
  return JSON.stringify(recordCheck.data);
@@ -141,7 +170,7 @@ function formatDefaultValue(value: unknown): string | null {
141
170
  if (stringCheck.success) {
142
171
  // Truncate long strings
143
172
  if (stringCheck.data.length > 50) {
144
- return `"${stringCheck.data.substring(0, 47)}..."`;
173
+ return `"${stringCheck.data.slice(0, 47)}..."`;
145
174
  }
146
175
  return `"${stringCheck.data}"`;
147
176
  }
@@ -165,10 +194,13 @@ function formatDefaultValue(value: unknown): string | null {
165
194
  }
166
195
  }
167
196
 
168
- function generateParameterType(iface: TypeScriptInterface, chartName: string): string {
197
+ function generateParameterType(
198
+ iface: TypeScriptInterface,
199
+ chartName: string,
200
+ ): string {
169
201
  const parameterKeys = flattenInterfaceKeys(iface);
170
202
 
171
- const normalizedChartName = capitalizeFirst(chartName).replace(/-/g, "");
203
+ const normalizedChartName = capitalizeFirst(chartName).replaceAll("-", "");
172
204
  let code = `export type ${normalizedChartName}HelmParameters = {\n`;
173
205
 
174
206
  for (const key of parameterKeys) {
@@ -180,12 +212,15 @@ function generateParameterType(iface: TypeScriptInterface, chartName: string): s
180
212
  return code;
181
213
  }
182
214
 
183
- function flattenInterfaceKeys(iface: TypeScriptInterface, prefix = ""): string[] {
215
+ function flattenInterfaceKeys(
216
+ iface: TypeScriptInterface,
217
+ prefix = "",
218
+ ): string[] {
184
219
  const keys: string[] = [];
185
220
 
186
221
  for (const [key, prop] of Object.entries(iface.properties)) {
187
222
  // Remove quotes from key for parameter names
188
- const cleanKey = key.replace(/"/g, "");
223
+ const cleanKey = key.replaceAll('"', "");
189
224
  const fullKey = prefix ? `${prefix}.${cleanKey}` : cleanKey;
190
225
 
191
226
  if (prop.nested) {
package/src/schemas.ts CHANGED
@@ -14,7 +14,9 @@ export const ErrorSchema = z.instanceof(Error);
14
14
  // Custom boolean string parser - only matches actual boolean-like strings
15
15
  export const StringBooleanSchema = z.string().refine((val) => {
16
16
  const lower = val.toLowerCase();
17
- return lower === "true" || lower === "false" || lower === "yes" || lower === "no";
17
+ return (
18
+ lower === "true" || lower === "false" || lower === "yes" || lower === "no"
19
+ );
18
20
  }, "Not a boolean string");
19
21
 
20
22
  // Zod schema for validating YAML values - recursive definition with more flexibility