@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/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
+ }