@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
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shepherdjerred/helm-types",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Generate TypeScript types from Helm chart values.yaml and values.schema.json",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"helm-types": "dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"!src/**/*.test.ts",
|
|
21
|
+
"!src/__snapshots__"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "bun build src/index.ts src/cli.ts --outdir dist --target node",
|
|
25
|
+
"generate": "bun run src/cli.ts",
|
|
26
|
+
"test": "bun test",
|
|
27
|
+
"test:helm-types": "bun test src/helm-types.test.ts",
|
|
28
|
+
"test:cli": "bun test src/cli.test.ts",
|
|
29
|
+
"lint": "bunx eslint .",
|
|
30
|
+
"lint:fix": "bunx eslint . --fix",
|
|
31
|
+
"typecheck": "bunx tsc --noEmit",
|
|
32
|
+
"prepublishOnly": "bun run build"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"helm",
|
|
36
|
+
"kubernetes",
|
|
37
|
+
"typescript",
|
|
38
|
+
"types",
|
|
39
|
+
"codegen",
|
|
40
|
+
"k8s"
|
|
41
|
+
],
|
|
42
|
+
"author": "Jerred Shepherd",
|
|
43
|
+
"license": "GPL-3.0",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/shepherdjerred/homelab.git",
|
|
47
|
+
"directory": "src/helm-types"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/shepherdjerred/homelab/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/shepherdjerred/homelab/tree/main/src/helm-types#readme",
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=18"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"yaml": "^2.8.1",
|
|
61
|
+
"zod": "^4.1.11"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Using Bun.$ for path operations instead of node:path
|
|
2
|
+
import { parse as yamlParse } from "yaml";
|
|
3
|
+
import type { ChartInfo, JSONSchemaProperty } from "./types.js";
|
|
4
|
+
import { HelmValueSchema, RecordSchema, ErrorSchema } from "./schemas.js";
|
|
5
|
+
import type { HelmValue } from "./schemas.js";
|
|
6
|
+
import { parseYAMLComments } from "./yaml-comments.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load JSON schema if it exists in the chart
|
|
10
|
+
*/
|
|
11
|
+
async function loadJSONSchema(chartPath: string): Promise<JSONSchemaProperty | null> {
|
|
12
|
+
try {
|
|
13
|
+
const schemaPath = `${chartPath}/values.schema.json`;
|
|
14
|
+
const schemaContent = await Bun.file(schemaPath).text();
|
|
15
|
+
const parsed: unknown = JSON.parse(schemaContent);
|
|
16
|
+
// Validate that parsed is an object
|
|
17
|
+
const recordCheck = RecordSchema.safeParse(parsed);
|
|
18
|
+
if (!recordCheck.success) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
// Note: JSONSchemaProperty is a structural type
|
|
22
|
+
const schema: JSONSchemaProperty = recordCheck.data;
|
|
23
|
+
console.log(` 📋 Loaded values.schema.json`);
|
|
24
|
+
return schema;
|
|
25
|
+
} catch {
|
|
26
|
+
// Schema doesn't exist or couldn't be parsed - that's okay
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run a command and return its output using Bun
|
|
33
|
+
*/
|
|
34
|
+
async function runCommand(command: string, args: string[]): Promise<string> {
|
|
35
|
+
try {
|
|
36
|
+
const proc = Bun.spawn([command, ...args], {
|
|
37
|
+
stdout: "pipe",
|
|
38
|
+
stderr: "inherit",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const output = await new Response(proc.stdout).text();
|
|
42
|
+
const exitCode = await proc.exited;
|
|
43
|
+
|
|
44
|
+
if (exitCode === 0) {
|
|
45
|
+
return output;
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error(`Command "${command} ${args.join(" ")}" failed with code ${exitCode.toString()}`);
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const parseResult = ErrorSchema.safeParse(error);
|
|
51
|
+
const errorMessage = parseResult.success ? parseResult.data.message : String(error);
|
|
52
|
+
throw new Error(`Failed to spawn command "${command} ${args.join(" ")}": ${errorMessage}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Fetch a Helm chart and extract its values.yaml and optional schema
|
|
58
|
+
*/
|
|
59
|
+
export async function fetchHelmChart(
|
|
60
|
+
chart: ChartInfo,
|
|
61
|
+
): Promise<{ values: HelmValue; schema: JSONSchemaProperty | null; yamlComments: Map<string, string> }> {
|
|
62
|
+
const pwd = Bun.env["PWD"] ?? process.cwd();
|
|
63
|
+
const tempDir = `${pwd}/temp/helm-${chart.name}`;
|
|
64
|
+
const repoName = `temp-repo-${chart.name}-${String(Date.now())}`;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Ensure temp directory exists
|
|
68
|
+
await Bun.$`mkdir -p ${tempDir}`.quiet();
|
|
69
|
+
|
|
70
|
+
console.log(` 📦 Adding Helm repo: ${chart.repoUrl}`);
|
|
71
|
+
// Add the helm repo
|
|
72
|
+
await runCommand("helm", ["repo", "add", repoName, chart.repoUrl]);
|
|
73
|
+
|
|
74
|
+
console.log(` 🔄 Updating Helm repos...`);
|
|
75
|
+
// Update repo
|
|
76
|
+
await runCommand("helm", ["repo", "update"]);
|
|
77
|
+
|
|
78
|
+
console.log(` ⬇️ Pulling chart ${chart.chartName}:${chart.version}...`);
|
|
79
|
+
// Pull the chart
|
|
80
|
+
await runCommand("helm", [
|
|
81
|
+
"pull",
|
|
82
|
+
`${repoName}/${chart.chartName}`,
|
|
83
|
+
"--version",
|
|
84
|
+
chart.version,
|
|
85
|
+
"--destination",
|
|
86
|
+
tempDir,
|
|
87
|
+
"--untar",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// Read values.yaml
|
|
91
|
+
const valuesPath = `${tempDir}/${chart.chartName}/values.yaml`;
|
|
92
|
+
console.log(` 📖 Reading values.yaml from ${valuesPath}`);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const valuesContent = await Bun.file(valuesPath).text();
|
|
96
|
+
|
|
97
|
+
// Parse YAML comments
|
|
98
|
+
const yamlComments = parseYAMLComments(valuesContent);
|
|
99
|
+
console.log(` 💬 Extracted ${String(yamlComments.size)} comments from values.yaml`);
|
|
100
|
+
|
|
101
|
+
// Parse YAML using yaml package
|
|
102
|
+
const parsedValues = yamlParse(valuesContent) as unknown;
|
|
103
|
+
console.log(` ✅ Successfully parsed values.yaml`);
|
|
104
|
+
const recordParseResult = RecordSchema.safeParse(parsedValues);
|
|
105
|
+
if (recordParseResult.success) {
|
|
106
|
+
console.log(
|
|
107
|
+
` 🔍 Parsed values keys: ${Object.keys(recordParseResult.data)
|
|
108
|
+
.slice(0, 10)
|
|
109
|
+
.join(", ")}${Object.keys(recordParseResult.data).length > 10 ? "..." : ""}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check if parsedValues is a valid object using Zod before validation
|
|
114
|
+
if (!recordParseResult.success) {
|
|
115
|
+
console.warn(` ⚠️ Parsed values is not a valid record object: ${String(parsedValues)}`);
|
|
116
|
+
return { values: {}, schema: null, yamlComments: new Map() };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Validate and parse with Zod for runtime type safety
|
|
120
|
+
const parseResult = HelmValueSchema.safeParse(recordParseResult.data);
|
|
121
|
+
|
|
122
|
+
// Try to load JSON schema
|
|
123
|
+
const chartPath = `${tempDir}/${chart.chartName}`;
|
|
124
|
+
const schema = await loadJSONSchema(chartPath);
|
|
125
|
+
|
|
126
|
+
if (parseResult.success) {
|
|
127
|
+
console.log(` ✅ Zod validation successful`);
|
|
128
|
+
return { values: parseResult.data, schema, yamlComments };
|
|
129
|
+
} else {
|
|
130
|
+
console.warn(` ⚠️ Zod validation failed for ${chart.name}:`);
|
|
131
|
+
console.warn(` First few errors:`, parseResult.error.issues.slice(0, 3));
|
|
132
|
+
console.warn(` ⚠️ Falling back to unvalidated object for type generation`);
|
|
133
|
+
// Return the validated record data from the successful parse result
|
|
134
|
+
return { values: recordParseResult.data, schema, yamlComments };
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.warn(` ⚠️ Failed to read/parse values.yaml: ${String(error)}`);
|
|
138
|
+
return { values: {}, schema: null, yamlComments: new Map() };
|
|
139
|
+
}
|
|
140
|
+
} finally {
|
|
141
|
+
// Cleanup
|
|
142
|
+
try {
|
|
143
|
+
console.log(` 🧹 Cleaning up...`);
|
|
144
|
+
await runCommand("helm", ["repo", "remove", repoName]);
|
|
145
|
+
await Bun.$`rm -rf ${tempDir}`.quiet();
|
|
146
|
+
} catch (cleanupError) {
|
|
147
|
+
console.warn(`Cleanup failed for ${chart.name}:`, String(cleanupError));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ChartInfo } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse chart information from versions.ts comments and values
|
|
5
|
+
*/
|
|
6
|
+
export async function parseChartInfoFromVersions(versionsPath = "src/versions.ts"): Promise<ChartInfo[]> {
|
|
7
|
+
const content = await Bun.file(versionsPath).text();
|
|
8
|
+
const lines = content.split("\n");
|
|
9
|
+
const charts: ChartInfo[] = [];
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12
|
+
const line = lines[i];
|
|
13
|
+
const nextLine = lines[i + 1];
|
|
14
|
+
|
|
15
|
+
// Look for renovate comments that indicate Helm charts
|
|
16
|
+
if (line?.includes("renovate: datasource=helm") && nextLine) {
|
|
17
|
+
const repoUrlMatch = /registryUrl=([^\s]+)/.exec(line);
|
|
18
|
+
const versionKeyMatch = /^\s*"?([^":]+)"?:/.exec(nextLine);
|
|
19
|
+
|
|
20
|
+
if (repoUrlMatch && versionKeyMatch) {
|
|
21
|
+
const repoUrl = repoUrlMatch[1];
|
|
22
|
+
const versionKey = versionKeyMatch[1];
|
|
23
|
+
|
|
24
|
+
if (!repoUrl || !versionKey) continue;
|
|
25
|
+
|
|
26
|
+
// Extract version value
|
|
27
|
+
const versionMatch = /:\s*"([^"]+)"/.exec(nextLine);
|
|
28
|
+
if (versionMatch) {
|
|
29
|
+
const version = versionMatch[1];
|
|
30
|
+
if (!version) continue;
|
|
31
|
+
|
|
32
|
+
// Try to determine chart name from the version key or URL
|
|
33
|
+
let chartName = versionKey;
|
|
34
|
+
|
|
35
|
+
// Handle special cases like "argo-cd" vs "argocd"
|
|
36
|
+
if (versionKey === "argo-cd") {
|
|
37
|
+
chartName = "argo-cd";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
charts.push({
|
|
41
|
+
name: versionKey,
|
|
42
|
+
repoUrl: repoUrl.replace(/\/$/, ""), // Remove trailing slash
|
|
43
|
+
version,
|
|
44
|
+
chartName,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return charts;
|
|
52
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI for @homelab/helm-types
|
|
4
|
+
*
|
|
5
|
+
* Generate TypeScript types from Helm charts
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { fetchHelmChart, convertToTypeScriptInterface, generateTypeScriptCode } from "./helm-types.ts";
|
|
9
|
+
import type { ChartInfo } from "./helm-types.ts";
|
|
10
|
+
|
|
11
|
+
const ErrorSchema = z.object({
|
|
12
|
+
message: z.string(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const HELP_TEXT = `
|
|
16
|
+
helm-types - Generate TypeScript types from Helm charts
|
|
17
|
+
|
|
18
|
+
USAGE:
|
|
19
|
+
bunx @homelab/helm-types [options]
|
|
20
|
+
|
|
21
|
+
OPTIONS:
|
|
22
|
+
--name, -n Unique identifier for the chart (required)
|
|
23
|
+
--chart, -c Chart name in the repository (defaults to --name)
|
|
24
|
+
--repo, -r Helm repository URL (required)
|
|
25
|
+
--version, -v Chart version (required)
|
|
26
|
+
--output, -o Output file path (defaults to stdout)
|
|
27
|
+
--interface, -i Interface name (auto-generated from chart name if not provided)
|
|
28
|
+
--help, -h Show this help message
|
|
29
|
+
|
|
30
|
+
EXAMPLES:
|
|
31
|
+
# Generate types for ArgoCD and print to stdout
|
|
32
|
+
bunx @homelab/helm-types \\
|
|
33
|
+
--name argo-cd \\
|
|
34
|
+
--repo https://argoproj.github.io/argo-helm \\
|
|
35
|
+
--version 8.3.1
|
|
36
|
+
|
|
37
|
+
# Generate types with custom output file
|
|
38
|
+
bunx @homelab/helm-types \\
|
|
39
|
+
--name argo-cd \\
|
|
40
|
+
--repo https://argoproj.github.io/argo-helm \\
|
|
41
|
+
--version 8.3.1 \\
|
|
42
|
+
--output argo-cd.types.ts
|
|
43
|
+
|
|
44
|
+
# Generate types with custom chart name and interface name
|
|
45
|
+
bunx @homelab/helm-types \\
|
|
46
|
+
--name argocd \\
|
|
47
|
+
--chart argo-cd \\
|
|
48
|
+
--repo https://argoproj.github.io/argo-helm \\
|
|
49
|
+
--version 8.3.1 \\
|
|
50
|
+
--interface ArgocdHelmValues \\
|
|
51
|
+
--output argocd.types.ts
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
type CliArgs = {
|
|
55
|
+
name?: string;
|
|
56
|
+
chart?: string;
|
|
57
|
+
repo?: string;
|
|
58
|
+
version?: string;
|
|
59
|
+
output?: string;
|
|
60
|
+
interface?: string;
|
|
61
|
+
help?: boolean;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Simple argument parser for Bun CLI
|
|
66
|
+
*/
|
|
67
|
+
function parseCliArgs(args: string[]): CliArgs {
|
|
68
|
+
const result: CliArgs = {};
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < args.length; i++) {
|
|
71
|
+
const arg = args[i];
|
|
72
|
+
if (!arg) continue;
|
|
73
|
+
|
|
74
|
+
if (arg === "--help" || arg === "-h") {
|
|
75
|
+
result.help = true;
|
|
76
|
+
} else if (arg === "--name" || arg === "-n") {
|
|
77
|
+
const value = args[i + 1];
|
|
78
|
+
if (value) {
|
|
79
|
+
result.name = value;
|
|
80
|
+
i += 1;
|
|
81
|
+
}
|
|
82
|
+
} else if (arg === "--chart" || arg === "-c") {
|
|
83
|
+
const value = args[i + 1];
|
|
84
|
+
if (value) {
|
|
85
|
+
result.chart = value;
|
|
86
|
+
i += 1;
|
|
87
|
+
}
|
|
88
|
+
} else if (arg === "--repo" || arg === "-r") {
|
|
89
|
+
const value = args[i + 1];
|
|
90
|
+
if (value) {
|
|
91
|
+
result.repo = value;
|
|
92
|
+
i += 1;
|
|
93
|
+
}
|
|
94
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
95
|
+
const value = args[i + 1];
|
|
96
|
+
if (value) {
|
|
97
|
+
result.version = value;
|
|
98
|
+
i += 1;
|
|
99
|
+
}
|
|
100
|
+
} else if (arg === "--output" || arg === "-o") {
|
|
101
|
+
const value = args[i + 1];
|
|
102
|
+
if (value) {
|
|
103
|
+
result.output = value;
|
|
104
|
+
i += 1;
|
|
105
|
+
}
|
|
106
|
+
} else if (arg === "--interface" || arg === "-i") {
|
|
107
|
+
const value = args[i + 1];
|
|
108
|
+
if (value) {
|
|
109
|
+
result.interface = value;
|
|
110
|
+
i += 1;
|
|
111
|
+
}
|
|
112
|
+
} else if (arg.startsWith("-")) {
|
|
113
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function main() {
|
|
121
|
+
try {
|
|
122
|
+
const args = parseCliArgs(Bun.argv.slice(2));
|
|
123
|
+
|
|
124
|
+
// Show help
|
|
125
|
+
if (args.help) {
|
|
126
|
+
console.log(HELP_TEXT);
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Validate required arguments
|
|
131
|
+
if (!args.name || !args.repo || !args.version) {
|
|
132
|
+
console.error("Error: Missing required arguments");
|
|
133
|
+
console.error("Required: --name, --repo, --version");
|
|
134
|
+
console.error("\nRun with --help for usage information");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Build chart info
|
|
139
|
+
const chartInfo: ChartInfo = {
|
|
140
|
+
name: args.name,
|
|
141
|
+
chartName: args.chart ?? args.name,
|
|
142
|
+
repoUrl: args.repo,
|
|
143
|
+
version: args.version,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Generate interface name from chart name if not provided
|
|
147
|
+
const interfaceName = args.interface ?? `${toPascalCase(args.name)}HelmValues`;
|
|
148
|
+
|
|
149
|
+
console.error(`Fetching chart: ${chartInfo.chartName}@${chartInfo.version}`);
|
|
150
|
+
console.error(`Repository: ${chartInfo.repoUrl}`);
|
|
151
|
+
console.error("");
|
|
152
|
+
|
|
153
|
+
// Fetch chart
|
|
154
|
+
const { values, schema, yamlComments } = await fetchHelmChart(chartInfo);
|
|
155
|
+
|
|
156
|
+
console.error("");
|
|
157
|
+
console.error(`Converting to TypeScript interface: ${interfaceName}`);
|
|
158
|
+
|
|
159
|
+
// Convert to TypeScript interface
|
|
160
|
+
const tsInterface = convertToTypeScriptInterface(values, interfaceName, schema, yamlComments, "", args.name);
|
|
161
|
+
|
|
162
|
+
// Generate TypeScript code
|
|
163
|
+
const code = generateTypeScriptCode(tsInterface, args.name);
|
|
164
|
+
|
|
165
|
+
// Write to file or stdout
|
|
166
|
+
if (args.output) {
|
|
167
|
+
await Bun.write(args.output, code);
|
|
168
|
+
console.error("");
|
|
169
|
+
console.error(`✅ Types written to: ${args.output}`);
|
|
170
|
+
} else {
|
|
171
|
+
// Write to stdout (so it can be piped)
|
|
172
|
+
console.log(code);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
process.exit(0);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const parseResult = ErrorSchema.safeParse(error);
|
|
178
|
+
if (parseResult.success) {
|
|
179
|
+
console.error(`Error: ${parseResult.data.message}`);
|
|
180
|
+
} else {
|
|
181
|
+
console.error(`Error: ${String(error)}`);
|
|
182
|
+
}
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Convert a string to PascalCase
|
|
189
|
+
*/
|
|
190
|
+
function toPascalCase(str: string): string {
|
|
191
|
+
return str
|
|
192
|
+
.split(/[-_\s]+/)
|
|
193
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
194
|
+
.join("");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
void main();
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { TypeScriptInterface } from "./types.js";
|
|
2
|
+
import { StringSchema, ArraySchema, RecordSchema, ActualNumberSchema, ActualBooleanSchema } from "./schemas.js";
|
|
3
|
+
import { capitalizeFirst } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate TypeScript code from interface definition
|
|
7
|
+
*/
|
|
8
|
+
export function generateTypeScriptCode(mainInterface: TypeScriptInterface, chartName: string): string {
|
|
9
|
+
const interfaces: TypeScriptInterface[] = [];
|
|
10
|
+
|
|
11
|
+
// Collect all nested interfaces
|
|
12
|
+
collectNestedInterfaces(mainInterface, interfaces);
|
|
13
|
+
|
|
14
|
+
let code = `// Generated TypeScript types for ${chartName} Helm chart\n\n`;
|
|
15
|
+
|
|
16
|
+
// Generate all interfaces
|
|
17
|
+
for (const iface of interfaces) {
|
|
18
|
+
code += generateInterfaceCode(iface);
|
|
19
|
+
code += "\n";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Generate parameter type (flattened dot notation)
|
|
23
|
+
code += generateParameterType(mainInterface, chartName);
|
|
24
|
+
|
|
25
|
+
// Check if any 'any' types were generated and add ESLint disable if needed
|
|
26
|
+
if (code.includes(": any")) {
|
|
27
|
+
code = `// Generated TypeScript types for ${chartName} Helm chart
|
|
28
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
29
|
+
|
|
30
|
+
${code.substring(code.indexOf("\n\n") + 2)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return code;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function collectNestedInterfaces(iface: TypeScriptInterface, collected: TypeScriptInterface[]): void {
|
|
37
|
+
for (const prop of Object.values(iface.properties)) {
|
|
38
|
+
if (prop.nested) {
|
|
39
|
+
collected.push(prop.nested);
|
|
40
|
+
collectNestedInterfaces(prop.nested, collected);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add main interface last so dependencies come first
|
|
45
|
+
if (!collected.some((i) => i.name === iface.name)) {
|
|
46
|
+
collected.push(iface);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function generateInterfaceCode(iface: TypeScriptInterface): string {
|
|
51
|
+
const hasProperties = Object.keys(iface.properties).length > 0;
|
|
52
|
+
|
|
53
|
+
if (!hasProperties) {
|
|
54
|
+
// Use 'object' for empty interfaces instead of '{}'
|
|
55
|
+
return `export type ${iface.name} = object;\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let code = `export type ${iface.name} = {\n`;
|
|
59
|
+
|
|
60
|
+
for (const [key, prop] of Object.entries(iface.properties)) {
|
|
61
|
+
const optional = prop.optional ? "?" : "";
|
|
62
|
+
|
|
63
|
+
// Generate JSDoc comment if we have description or default
|
|
64
|
+
if (prop.description || prop.default !== undefined) {
|
|
65
|
+
code += ` /**\n`;
|
|
66
|
+
|
|
67
|
+
if (prop.description) {
|
|
68
|
+
// Format multi-line descriptions properly with " * " prefix
|
|
69
|
+
// Escape */ sequences to prevent premature comment closure
|
|
70
|
+
const escapedDescription = prop.description.replace(/\*\//g, "*\\/");
|
|
71
|
+
const descLines = escapedDescription.split("\n");
|
|
72
|
+
for (const line of descLines) {
|
|
73
|
+
code += ` * ${line}\n`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (prop.default !== undefined) {
|
|
78
|
+
const defaultStr = formatDefaultValue(prop.default);
|
|
79
|
+
if (defaultStr) {
|
|
80
|
+
if (prop.description) code += ` *\n`;
|
|
81
|
+
code += ` * @default ${defaultStr}\n`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
code += ` */\n`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
code += ` ${key}${optional}: ${prop.type};\n`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
code += "};\n";
|
|
92
|
+
return code;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format a default value for display in JSDoc
|
|
97
|
+
*/
|
|
98
|
+
function formatDefaultValue(value: unknown): string | null {
|
|
99
|
+
if (value === null) return "null";
|
|
100
|
+
if (value === undefined) return null;
|
|
101
|
+
|
|
102
|
+
// Handle arrays
|
|
103
|
+
const arrayCheck = ArraySchema.safeParse(value);
|
|
104
|
+
if (arrayCheck.success) {
|
|
105
|
+
if (arrayCheck.data.length === 0) return "[]";
|
|
106
|
+
if (arrayCheck.data.length <= 3) {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.stringify(arrayCheck.data);
|
|
109
|
+
} catch {
|
|
110
|
+
return "[...]";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return `[...] (${String(arrayCheck.data.length)} items)`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Handle objects
|
|
117
|
+
const recordCheck = RecordSchema.safeParse(value);
|
|
118
|
+
if (recordCheck.success) {
|
|
119
|
+
const keys = Object.keys(recordCheck.data);
|
|
120
|
+
if (keys.length === 0) return "{}";
|
|
121
|
+
if (keys.length <= 3) {
|
|
122
|
+
try {
|
|
123
|
+
return JSON.stringify(recordCheck.data);
|
|
124
|
+
} catch {
|
|
125
|
+
return "{...}";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return `{...} (${String(keys.length)} keys)`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Primitives
|
|
132
|
+
const stringCheck = StringSchema.safeParse(value);
|
|
133
|
+
if (stringCheck.success) {
|
|
134
|
+
// Truncate long strings
|
|
135
|
+
if (stringCheck.data.length > 50) {
|
|
136
|
+
return `"${stringCheck.data.substring(0, 47)}..."`;
|
|
137
|
+
}
|
|
138
|
+
return `"${stringCheck.data}"`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Handle other primitives (numbers, booleans, etc.)
|
|
142
|
+
const numberCheck = ActualNumberSchema.safeParse(value);
|
|
143
|
+
if (numberCheck.success) {
|
|
144
|
+
return String(numberCheck.data);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const booleanCheck = ActualBooleanSchema.safeParse(value);
|
|
148
|
+
if (booleanCheck.success) {
|
|
149
|
+
return String(booleanCheck.data);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fallback for unknown types - try JSON.stringify
|
|
153
|
+
try {
|
|
154
|
+
return JSON.stringify(value);
|
|
155
|
+
} catch {
|
|
156
|
+
return "unknown";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function generateParameterType(iface: TypeScriptInterface, chartName: string): string {
|
|
161
|
+
const parameterKeys = flattenInterfaceKeys(iface);
|
|
162
|
+
|
|
163
|
+
const normalizedChartName = capitalizeFirst(chartName).replace(/-/g, "");
|
|
164
|
+
let code = `export type ${normalizedChartName}HelmParameters = {\n`;
|
|
165
|
+
|
|
166
|
+
for (const key of parameterKeys) {
|
|
167
|
+
code += ` "${key}"?: string;\n`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
code += "};\n";
|
|
171
|
+
|
|
172
|
+
return code;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function flattenInterfaceKeys(iface: TypeScriptInterface, prefix = ""): string[] {
|
|
176
|
+
const keys: string[] = [];
|
|
177
|
+
|
|
178
|
+
for (const [key, prop] of Object.entries(iface.properties)) {
|
|
179
|
+
// Remove quotes from key for parameter names
|
|
180
|
+
const cleanKey = key.replace(/"/g, "");
|
|
181
|
+
const fullKey = prefix ? `${prefix}.${cleanKey}` : cleanKey;
|
|
182
|
+
|
|
183
|
+
if (prop.nested) {
|
|
184
|
+
keys.push(...flattenInterfaceKeys(prop.nested, fullKey));
|
|
185
|
+
} else {
|
|
186
|
+
keys.push(fullKey);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return keys;
|
|
191
|
+
}
|