klasik 1.0.23 → 1.0.25

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.
@@ -14,7 +14,9 @@
14
14
  "Bash(node test-disc-code.js:*)",
15
15
  "Bash(node test-new-validators.js:*)",
16
16
  "Bash(node dist/cli.js generate-crd:*)",
17
- "Bash(node test-programmatic-api.js:*)"
17
+ "Bash(node test-programmatic-api.js:*)",
18
+ "WebFetch(domain:www.schemastore.org)",
19
+ "WebFetch(domain:json.schemastore.org)"
18
20
  ]
19
21
  }
20
22
  }
package/README.md CHANGED
@@ -101,6 +101,12 @@ npx klasik generate-crd \
101
101
  --url https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/crds/appproject-crd.yaml \
102
102
  --output ./generated
103
103
 
104
+ # From multiple CRD URLs (merged into single output)
105
+ npx klasik generate-crd \
106
+ -u https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/crds/application-crd.yaml \
107
+ -u https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/crds/appproject-crd.yaml \
108
+ --output ./generated
109
+
104
110
  # From a local CRD file
105
111
  npx klasik generate-crd \
106
112
  --url ./my-crd.yaml \
@@ -123,6 +129,7 @@ npx klasik generate-crd \
123
129
  ### CRD Features
124
130
 
125
131
  - ✅ **Automatic schema extraction** from `openAPIV3Schema`
132
+ - ✅ **Multiple URL support** - Generate from multiple CRD sources in one command
126
133
  - ✅ **Version-based organization** - Each CRD version gets its own folder (e.g., `v1alpha1/`, `v1beta1/`)
127
134
  - ✅ **Multi-document YAML support** - Process multiple CRDs from a single file
128
135
  - ✅ **Nested type extraction** - Complex nested objects become separate TypeScript interfaces
@@ -181,13 +188,26 @@ import { CRDGenerator } from 'klasik';
181
188
 
182
189
  const generator = new CRDGenerator();
183
190
 
191
+ // Single URL
184
192
  await generator.generate({
185
- url: './crds/my-resource.yaml',
193
+ urls: './crds/my-resource.yaml',
186
194
  outputDir: './generated',
187
195
  includeStatus: true,
188
196
  nestJsSwagger: true,
189
197
  classValidator: true
190
198
  });
199
+
200
+ // Multiple URLs (merged into single output)
201
+ await generator.generate({
202
+ urls: [
203
+ 'https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/crds/application-crd.yaml',
204
+ 'https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/crds/appproject-crd.yaml',
205
+ './local/my-crd.yaml'
206
+ ],
207
+ outputDir: './generated',
208
+ includeStatus: true,
209
+ nestJsSwagger: true
210
+ });
191
211
  ```
192
212
 
193
213
  ### Multi-CRD YAML Files
@@ -344,7 +364,9 @@ klasik download \
344
364
  Generate TypeScript models from Kubernetes CustomResourceDefinition (CRD) YAML files.
345
365
 
346
366
  **Options:**
347
- - `-u, --url <url>` - URL or file path to CRD YAML (required)
367
+ - `-u, --url <url...>` - URL(s) or file path(s) to CRD YAML files (required)
368
+ - Can be specified multiple times: `-u url1 -u url2 -u url3`
369
+ - All CRDs from all URLs are merged into a single output directory
348
370
  - Supports: `https://...`, `http://...`, `file://...`, `/absolute/path`, `./relative/path`
349
371
  - `-o, --output <dir>` - Output directory for generated models (required)
350
372
  - `--include-status` - Generate status schemas in addition to spec schemas (default: false)
@@ -366,6 +388,19 @@ klasik generate-crd \
366
388
  --url https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/crds/appproject-crd.yaml \
367
389
  --output ./generated/crds
368
390
 
391
+ # Generate from multiple CRD URLs (merged into single output)
392
+ klasik generate-crd \
393
+ -u https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/crds/application-crd.yaml \
394
+ -u https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/crds/appproject-crd.yaml \
395
+ --output ./generated/argocd
396
+
397
+ # Mix remote and local CRDs
398
+ klasik generate-crd \
399
+ -u https://example.com/crd1.yaml \
400
+ -u ./crds/crd2.yaml \
401
+ -u ./crds/crd3.yaml \
402
+ --output ./src/generated
403
+
369
404
  # Generate with status models
370
405
  klasik generate-crd \
371
406
  --url ./my-crd.yaml \
package/dist/cli.js CHANGED
@@ -137,7 +137,7 @@ program
137
137
  program
138
138
  .command('generate-crd')
139
139
  .description('Generate TypeScript models from Kubernetes CRD YAML files')
140
- .requiredOption('-u, --url <url>', 'URL or file path to CRD YAML')
140
+ .requiredOption('-u, --url <url...>', 'URL(s) or file path(s) to CRD YAML files (can be specified multiple times)')
141
141
  .requiredOption('-o, --output <dir>', 'Output directory for generated models')
142
142
  .option('--include-status', 'Generate status schemas in addition to spec schemas', false)
143
143
  .option('--nestjs-swagger', 'Include NestJS Swagger @ApiProperty decorators in generated models', false)
@@ -164,8 +164,10 @@ program
164
164
  const outputDir = path.resolve(options.output);
165
165
  const { CRDGenerator } = await Promise.resolve().then(() => __importStar(require('./crd/crd-generator')));
166
166
  const generator = new CRDGenerator();
167
+ // Handle both single URL (backwards compat) and multiple URLs
168
+ const urls = Array.isArray(options.url) ? options.url : [options.url];
167
169
  await generator.generate({
168
- url: options.url,
170
+ urls,
169
171
  outputDir,
170
172
  includeStatus: options.includeStatus,
171
173
  headers: Object.keys(headers).length > 0 ? headers : undefined,
@@ -3,9 +3,10 @@
3
3
  */
4
4
  export interface CRDGeneratorOptions {
5
5
  /**
6
- * URL or file path to CRD YAML file(s)
6
+ * URL(s) or file path(s) to CRD YAML file(s)
7
+ * Can be a single URL string or array of URLs
7
8
  */
8
- url: string;
9
+ urls: string | string[];
9
10
  /**
10
11
  * Output directory for generated models
11
12
  */
@@ -57,36 +57,65 @@ class CRDGenerator {
57
57
  * @param options Generation options
58
58
  */
59
59
  async generate(options) {
60
- const { url, outputDir, includeStatus = false, headers, timeout, fixEsmImports = false, nestJsSwagger = false, classValidator = false, skipJsExtensions = false, templateDir, keepSpec = false, } = options;
60
+ const { urls: urlsInput, outputDir, includeStatus = false, headers, timeout, fixEsmImports = false, nestJsSwagger = false, classValidator = false, skipJsExtensions = false, templateDir, keepSpec = false, } = options;
61
+ // Normalize to array
62
+ const urls = Array.isArray(urlsInput) ? urlsInput : [urlsInput];
61
63
  try {
62
- // Step 1: Download/load CRD file
63
- console.log('Step 1: Loading CRD file...');
64
- const crdContent = await this.loadCRDFile(url, headers, timeout);
65
- // Step 2: Parse CRDs
66
- console.log('Step 2: Parsing CRD definitions...');
67
- const parseResult = this.parser.parseContent(crdContent, {
68
- includeStatus,
69
- });
70
- // Check for errors
71
- if (parseResult.errors.length > 0) {
72
- console.error('Errors encountered during parsing:');
73
- parseResult.errors.forEach(err => console.error(` ${err}`));
74
- throw new Error('CRD parsing failed');
64
+ const allCRDs = [];
65
+ const errors = [];
66
+ // Process each URL
67
+ for (let i = 0; i < urls.length; i++) {
68
+ const url = urls[i];
69
+ console.log(`\n[${i + 1}/${urls.length}] Processing URL: ${url}`);
70
+ try {
71
+ // Step 1: Download/load CRD file
72
+ console.log(' Loading CRD file...');
73
+ const crdContent = await this.loadCRDFile(url, headers, timeout);
74
+ // Step 2: Parse CRDs
75
+ console.log(' Parsing CRD definitions...');
76
+ const parseResult = this.parser.parseContent(crdContent, {
77
+ includeStatus,
78
+ });
79
+ // Check for errors
80
+ if (parseResult.errors.length > 0) {
81
+ const errorMsg = parseResult.errors.join(', ');
82
+ errors.push({ url, error: errorMsg });
83
+ console.error(` ❌ Errors: ${errorMsg}`);
84
+ continue; // Skip to next URL
85
+ }
86
+ // Show warnings
87
+ if (parseResult.warnings.length > 0) {
88
+ parseResult.warnings.forEach(warn => console.warn(` ⚠️ ${warn}`));
89
+ }
90
+ console.log(` Found ${parseResult.crds.length} CRD(s)`);
91
+ allCRDs.push(...parseResult.crds);
92
+ }
93
+ catch (error) {
94
+ const errorMsg = error instanceof Error ? error.message : String(error);
95
+ errors.push({ url, error: errorMsg });
96
+ console.error(` ❌ Failed to process URL: ${errorMsg}`);
97
+ // Continue with next URL
98
+ }
75
99
  }
76
- // Show warnings
77
- if (parseResult.warnings.length > 0) {
78
- console.warn('Warnings:');
79
- parseResult.warnings.forEach(warn => console.warn(` ⚠️ ${warn}`));
100
+ // Check if any CRDs were successfully loaded
101
+ if (allCRDs.length === 0) {
102
+ if (errors.length > 0) {
103
+ throw new Error(`Failed to load CRDs from all URLs:\n${errors.map(e => ` - ${e.url}: ${e.error}`).join('\n')}`);
104
+ }
105
+ throw new Error('No valid CRDs found in any of the provided URLs');
80
106
  }
81
- if (parseResult.crds.length === 0) {
82
- throw new Error('No valid CRDs found in the file');
107
+ // Show summary
108
+ console.log(`\n✅ Successfully loaded ${allCRDs.length} CRD(s) from ${urls.length - errors.length}/${urls.length} URL(s)`);
109
+ if (errors.length > 0) {
110
+ console.warn(`\n⚠️ Failed to load from ${errors.length} URL(s):`);
111
+ errors.forEach(e => console.warn(` - ${e.url}: ${e.error}`));
83
112
  }
84
- console.log(`Found ${parseResult.crds.length} CRD(s)`);
85
- // Step 3: Process each CRD
86
- for (const crd of parseResult.crds) {
113
+ // Step 3: Process all CRDs
114
+ console.log('\nGenerating TypeScript models...');
115
+ for (const crd of allCRDs) {
87
116
  console.log(`\nProcessing CRD: ${crd.spec.names.kind}`);
88
117
  // Determine output directory structure
89
- const crdOutputDir = parseResult.crds.length > 1
118
+ const crdOutputDir = allCRDs.length > 1
90
119
  ? path.join(outputDir, crd.spec.names.kind)
91
120
  : outputDir;
92
121
  const versionDirs = await this.processCRD(crd, crdOutputDir, {
@@ -101,18 +130,18 @@ class CRDGenerator {
101
130
  keepSpec,
102
131
  });
103
132
  // Generate index file for this CRD if multiple CRDs
104
- if (parseResult.crds.length > 1) {
133
+ if (allCRDs.length > 1) {
105
134
  this.generateCRDIndex(crdOutputDir, versionDirs);
106
135
  }
107
136
  }
108
137
  // Step 4: Generate main index file
109
- console.log('\nStep 4: Generating index files...');
110
- if (parseResult.crds.length > 1) {
111
- this.generateMainIndex(outputDir, parseResult.crds.map(c => c.spec.names.kind));
138
+ console.log('\nGenerating index files...');
139
+ if (allCRDs.length > 1) {
140
+ this.generateMainIndex(outputDir, allCRDs.map(c => c.spec.names.kind));
112
141
  }
113
142
  else {
114
143
  // For single CRD, generate version index
115
- const versionNames = parseResult.crds[0].spec.versions.map(v => v.name);
144
+ const versionNames = allCRDs[0].spec.versions.map(v => v.name);
116
145
  this.generateVersionIndex(outputDir, versionNames);
117
146
  }
118
147
  console.log('\n✅ CRD client generation completed successfully!');
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Fixes enum declaration order in TypeScript files generated with NestJS decorators
3
+ *
4
+ * When openapi-class-transformer generates files with @ApiProperty decorators,
5
+ * enum constants are defined at the end of the file but referenced in decorators
6
+ * at the beginning, causing "used before initialization" errors.
7
+ *
8
+ * This utility moves enum declarations to after imports, before class definitions.
9
+ */
10
+ export declare class EnumOrderFixer {
11
+ /**
12
+ * Fix all TypeScript files in a directory recursively
13
+ */
14
+ fixDirectory(directory: string): Promise<void>;
15
+ /**
16
+ * Fix enum declarations in a single file
17
+ * @returns true if file was modified, false otherwise
18
+ */
19
+ private fixFile;
20
+ /**
21
+ * Move enum declarations from end of file to after imports
22
+ */
23
+ private moveEnumsToTop;
24
+ /**
25
+ * Get all TypeScript files in a directory recursively
26
+ */
27
+ private getAllTsFiles;
28
+ }
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.EnumOrderFixer = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ /**
40
+ * Fixes enum declaration order in TypeScript files generated with NestJS decorators
41
+ *
42
+ * When openapi-class-transformer generates files with @ApiProperty decorators,
43
+ * enum constants are defined at the end of the file but referenced in decorators
44
+ * at the beginning, causing "used before initialization" errors.
45
+ *
46
+ * This utility moves enum declarations to after imports, before class definitions.
47
+ */
48
+ class EnumOrderFixer {
49
+ /**
50
+ * Fix all TypeScript files in a directory recursively
51
+ */
52
+ async fixDirectory(directory) {
53
+ console.log('Fixing enum declaration order...');
54
+ const files = this.getAllTsFiles(directory);
55
+ let fixedCount = 0;
56
+ for (const file of files) {
57
+ const fixed = await this.fixFile(file);
58
+ if (fixed) {
59
+ fixedCount++;
60
+ }
61
+ }
62
+ console.log(`✅ Fixed enum order in ${fixedCount} file(s)`);
63
+ }
64
+ /**
65
+ * Fix enum declarations in a single file
66
+ * @returns true if file was modified, false otherwise
67
+ */
68
+ async fixFile(filePath) {
69
+ let content = fs.readFileSync(filePath, 'utf8');
70
+ const originalContent = content;
71
+ // Move enums to after imports
72
+ content = this.moveEnumsToTop(content);
73
+ // Only write if content changed
74
+ if (content !== originalContent) {
75
+ fs.writeFileSync(filePath, content, 'utf8');
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+ /**
81
+ * Move enum declarations from end of file to after imports
82
+ */
83
+ moveEnumsToTop(content) {
84
+ // Pattern to match enum declarations at end of file
85
+ // Matches pairs like:
86
+ // export const FooEnum = { ... } as const;
87
+ // export type FooEnum = typeof FooEnum[keyof typeof FooEnum];
88
+ const enumPattern = /\n\nexport const (\w+Enum) = \{[^}]+\} as const;\n\nexport type \1 = typeof \1\[keyof typeof \1\];/g;
89
+ const enumBlocks = [];
90
+ let match;
91
+ // Extract all enum blocks
92
+ while ((match = enumPattern.exec(content)) !== null) {
93
+ enumBlocks.push(match[0]);
94
+ }
95
+ // If no enums found, return unchanged
96
+ if (enumBlocks.length === 0) {
97
+ return content;
98
+ }
99
+ // Remove enum blocks from original location
100
+ let newContent = content.replace(enumPattern, '');
101
+ // Find last import statement
102
+ const importMatches = newContent.match(/^import\s+.*$/gm);
103
+ if (!importMatches || importMatches.length === 0) {
104
+ // No imports found, don't modify
105
+ return content;
106
+ }
107
+ const lastImport = importMatches[importMatches.length - 1];
108
+ const lastImportIndex = newContent.indexOf(lastImport);
109
+ const insertPoint = lastImportIndex + lastImport.length;
110
+ // Build the new content with enums after imports
111
+ const beforeInsert = newContent.substring(0, insertPoint);
112
+ const afterInsert = newContent.substring(insertPoint);
113
+ // Create enum block with proper spacing
114
+ const enumsBlock = '\n\n' + enumBlocks.map(block => block.trim()).join('\n\n') + '\n';
115
+ return beforeInsert + enumsBlock + afterInsert;
116
+ }
117
+ /**
118
+ * Get all TypeScript files in a directory recursively
119
+ */
120
+ getAllTsFiles(directory) {
121
+ const files = [];
122
+ const walk = (dir) => {
123
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
124
+ for (const entry of entries) {
125
+ const fullPath = path.join(dir, entry.name);
126
+ if (entry.isDirectory()) {
127
+ // Skip node_modules and hidden directories
128
+ if (entry.name !== 'node_modules' && !entry.name.startsWith('.')) {
129
+ walk(fullPath);
130
+ }
131
+ }
132
+ else if (entry.isFile() && entry.name.endsWith('.ts')) {
133
+ files.push(fullPath);
134
+ }
135
+ }
136
+ };
137
+ walk(directory);
138
+ return files;
139
+ }
140
+ }
141
+ exports.EnumOrderFixer = EnumOrderFixer;
@@ -88,9 +88,16 @@ class K8sClientGenerator {
88
88
  console.log('Step 3.5: Adding decorator dependencies to package.json...');
89
89
  this.addDecoratorDependencies(outputDir, nestJsSwagger, classValidator);
90
90
  }
91
- // Step 4: Fix ESM imports if requested
91
+ // Step 4: Fix enum declaration order if NestJS decorators enabled
92
+ if (nestJsSwagger) {
93
+ console.log('Step 4: Fixing enum declaration order...');
94
+ const { EnumOrderFixer } = await Promise.resolve().then(() => __importStar(require('./enum-order-fixer')));
95
+ const enumFixer = new EnumOrderFixer();
96
+ await enumFixer.fixDirectory(outputDir);
97
+ }
98
+ // Step 5: Fix ESM imports if requested
92
99
  if (fixEsmImports) {
93
- console.log('Step 4: Fixing ESM imports...');
100
+ console.log('Step 5: Fixing ESM imports...');
94
101
  const esmFixer = new esm_fixer_1.EsmFixer();
95
102
  await esmFixer.fixDirectory(outputDir);
96
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klasik",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "Download OpenAPI specs from remote URLs and generate TypeScript clients with class-transformer support - support yaml, json, CustomResourceDefinition formats",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",