timonel 2.13.0 → 2.14.0-beta.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # [2.14.0-beta.1](https://github.com/KenkoGeek/timonel/compare/v2.13.0...v2.14.0-beta.1) (2025-11-28)
2
+
3
+ ### Features
4
+
5
+ - **cli:** add Helm helpers (helmIf, helmWith) to templates and new release ([#201](https://github.com/KenkoGeek/timonel/issues/201)) ([b5ecb0f](https://github.com/KenkoGeek/timonel/commit/b5ecb0f29b207f7ff225b971c6e698bbc2d26073))
6
+
1
7
  # [2.13.0](https://github.com/KenkoGeek/timonel/compare/v2.12.2...v2.13.0) (2025-11-24)
2
8
 
3
9
  ### Features
package/README.md CHANGED
@@ -18,6 +18,8 @@ directory.
18
18
  ## ✨ Key Features
19
19
 
20
20
  - **🔒 Type-safe API** with strict TypeScript and cdk8s constructs
21
+ - **🎯 Type-Safe Helm Helpers** with 9 composable template helpers (`helmIf`, `helmRange`,
22
+ `helmWith`, `helmInclude`, etc.)
21
23
  - **🔧 Flexible resource creation** with built-in methods and `addManifest()` for custom resources
22
24
  - **🌍 Multi-environment support** with automatic values files generation
23
25
  - **☂️ Umbrella Charts** for managing multiple subcharts as a single unit
@@ -72,33 +74,58 @@ tl umbrella synth
72
74
  ### Simple Web Application
73
75
 
74
76
  ```typescript
75
- import { Chart } from 'cdk8s';
76
- import { Deployment, Service } from 'cdk8s-plus-33';
77
-
78
- export class WebAppChart extends Chart {
79
- constructor(scope: Chart, id: string) {
80
- super(scope, id);
81
-
82
- // Create deployment
83
- new Deployment(this, 'web-deployment', {
84
- containers: [
85
- {
86
- image: 'nginx:latest',
87
- port: 80,
88
- env: {
89
- NGINX_PORT: '80',
90
- },
77
+ import { Rutter, helmInclude, createHelmExpression as helm } from 'timonel';
78
+
79
+ const chart = new Rutter({
80
+ meta: {
81
+ name: 'web-app',
82
+ version: '1.0.0',
83
+ description: 'Simple web application',
84
+ },
85
+ defaultValues: {
86
+ replicas: 3,
87
+ image: {
88
+ repository: 'nginx',
89
+ tag: 'latest',
90
+ },
91
+ },
92
+ });
93
+
94
+ // Add Deployment with type-safe helpers
95
+ chart.addManifest(
96
+ {
97
+ apiVersion: 'apps/v1',
98
+ kind: 'Deployment',
99
+ metadata: {
100
+ name: helmInclude('chart.fullname', '.'),
101
+ labels: helmInclude('chart.labels', '.', { pipe: 'nindent 4' }),
102
+ },
103
+ spec: {
104
+ replicas: helm('{{ .Values.replicas }}'),
105
+ selector: {
106
+ matchLabels: helmInclude('chart.selectorLabels', '.', { pipe: 'nindent 6' }),
107
+ },
108
+ template: {
109
+ metadata: {
110
+ labels: helmInclude('chart.selectorLabels', '.', { pipe: 'nindent 8' }),
91
111
  },
92
- ],
93
- });
94
-
95
- // Create service
96
- new Service(this, 'web-service', {
97
- selector: { app: 'web' },
98
- ports: [{ port: 80, targetPort: 80 }],
99
- });
100
- }
101
- }
112
+ spec: {
113
+ containers: [
114
+ {
115
+ name: 'web',
116
+ image: helm('{{ .Values.image.repository }}:{{ .Values.image.tag }}'),
117
+ ports: [{ containerPort: 80, name: 'http' }],
118
+ },
119
+ ],
120
+ },
121
+ },
122
+ },
123
+ },
124
+ 'deployment',
125
+ );
126
+
127
+ // Generate the chart
128
+ chart.write('./dist');
102
129
  ```
103
130
 
104
131
  ### Umbrella Chart with Multiple Services
@@ -126,6 +153,23 @@ const umbrellaConfig = {
126
153
  export const umbrella = new UmbrellaChartTemplate(umbrellaConfig);
127
154
  ```
128
155
 
156
+ ### Type-Safe Helm Helpers
157
+
158
+ Timonel provides 9 composable, type-safe helpers for Helm template generation: `helmIf`,
159
+ `helmRange`, `helmWith`, `helmInclude`, `helmDefine`, `helmVar`, `helmBlock`, `helmComment`, and
160
+ `helmFragment`.
161
+
162
+ **Benefits:**
163
+
164
+ - ✅ 100% Type-Safe - catch errors at compile time
165
+ - ✅ No Raw Strings - eliminate manual template interpolation
166
+ - ✅ Composable - nest and combine helpers freely
167
+ - ✅ Full IDE Support - autocomplete and type hints
168
+
169
+ **Learn more:** See the
170
+ [Type-Safe Helm Helpers Guide](https://github.com/KenkoGeek/timonel/wiki/Helm-Helpers-System) for
171
+ complete documentation, examples, and best practices.
172
+
129
173
  ## 📚 Documentation
130
174
 
131
175
  - **[API Reference](https://github.com/KenkoGeek/timonel/wiki/API-Reference)** - Complete API
package/dist/index.d.ts CHANGED
@@ -11,4 +11,6 @@ export type { KarpenterDisruption, KarpenterDisruptionBudget, KarpenterEC2NodeCl
11
11
  export { DEFAULT_TERMINATION_GRACE_PERIOD, isValidDisruptionBudget, isValidKubernetesDuration, KarpenterVersionUtils, } from './lib/resources/cloud/aws/karpenterResources.js';
12
12
  export { AWS_HELPERS, createHelper, FILE_ACCESS_HELPERS, formatHelpers, generateHelpersTemplate, getDefaultHelpers, getHelpersByCategory, KUBERNETES_HELPERS, SPRIG_HELPERS, STANDARD_HELPERS, TEMPLATE_FUNCTION_HELPERS, type HelperDefinition, } from './lib/utils/helmHelpers.js';
13
13
  export { createLogger, logger, LogLevel, TimonelLogger, type LogContext, type LoggerConfig, } from './lib/utils/logger.js';
14
- export { createHelmExpression, isHelmExpression, type HelmExpression, } from './lib/utils/helmYamlSerializer.js';
14
+ export { UmbrellaRutter } from './lib/umbrellaRutter.js';
15
+ export { helmIf, helmRange, helmWith, helmInclude, helmDefine, helmVar, helmBlock, helmComment, helmFragment, createHelmExpression, isHelmConstruct, isHelmExpression, type HelmConstruct, type HelmExpression, type HelmContent, type HelmWhitespaceOptions, } from './lib/utils/helmControlStructures.js';
16
+ export { dumpHelmAwareYaml, validateHelmYaml, type HelmValidationError, type HelmValidationResult, } from './lib/utils/helmYamlSerializer.js';
package/dist/index.js CHANGED
@@ -8,4 +8,6 @@ export { UmbrellaChartTemplate as UmbrellaChart } from './lib/templates/umbrella
8
8
  export { DEFAULT_TERMINATION_GRACE_PERIOD, isValidDisruptionBudget, isValidKubernetesDuration, KarpenterVersionUtils, } from './lib/resources/cloud/aws/karpenterResources.js';
9
9
  export { AWS_HELPERS, createHelper, FILE_ACCESS_HELPERS, formatHelpers, generateHelpersTemplate, getDefaultHelpers, getHelpersByCategory, KUBERNETES_HELPERS, SPRIG_HELPERS, STANDARD_HELPERS, TEMPLATE_FUNCTION_HELPERS, } from './lib/utils/helmHelpers.js';
10
10
  export { createLogger, logger, LogLevel, TimonelLogger, } from './lib/utils/logger.js';
11
- export { createHelmExpression, isHelmExpression, } from './lib/utils/helmYamlSerializer.js';
11
+ export { UmbrellaRutter } from './lib/umbrellaRutter.js';
12
+ export { helmIf, helmRange, helmWith, helmInclude, helmDefine, helmVar, helmBlock, helmComment, helmFragment, createHelmExpression, isHelmConstruct, isHelmExpression, } from './lib/utils/helmControlStructures.js';
13
+ export { dumpHelmAwareYaml, validateHelmYaml, } from './lib/utils/helmYamlSerializer.js';
@@ -93,18 +93,6 @@ export declare class Rutter {
93
93
  target: string;
94
94
  }>;
95
95
  private toSynthArray;
96
- private processHelmTemplates;
97
- private fixCharacterMappingIssues;
98
- private processCharacterMapping;
99
- private applyHelmTemplateReplacements;
100
- private fixIncludeStatements;
101
- private fixFunctionCalls;
102
- private fixChartReferences;
103
- private removeHelmExpressionQuotes;
104
- private fixConditionalBlocks;
105
- private fixPipeExpressions;
106
- private fixNestedQuotes;
107
- private cleanupArtifacts;
108
96
  write(outDir: string): void;
109
97
  }
110
98
  export interface ChartMetadata {
@@ -1,11 +1,12 @@
1
1
  import { ApiObject, App, Chart, Testing } from 'cdk8s';
2
- import * as jsYaml from 'js-yaml';
2
+ import { parse } from 'yaml';
3
3
  import { include } from './helm.js';
4
4
  import { HelmChartWriter } from './helmChartWriter.js';
5
5
  import { AWSResources } from './resources/cloud/aws/awsResources.js';
6
6
  import { createLogger } from './utils/logger.js';
7
7
  import { KarpenterResources } from './resources/cloud/aws/karpenterResources.js';
8
- import { dumpHelmAwareYaml, isHelmExpression } from './utils/helmYamlSerializer.js';
8
+ import { isHelmExpression, isHelmConstruct } from './utils/helmControlStructures.js';
9
+ import { dumpHelmAwareYaml } from './utils/helmYamlSerializer.js';
9
10
  import { generateHelpersTemplate } from './utils/helmHelpers.js';
10
11
  export class Rutter {
11
12
  constructor(props) {
@@ -63,7 +64,7 @@ export class Rutter {
63
64
  let manifestObject;
64
65
  if (typeof yamlOrObject === 'string') {
65
66
  try {
66
- manifestObject = jsYaml.load(yamlOrObject);
67
+ manifestObject = parse(yamlOrObject);
67
68
  }
68
69
  catch (error) {
69
70
  throw new Error(`Invalid YAML provided to addManifest(): ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -76,12 +77,13 @@ export class Rutter {
76
77
  throw new Error('addManifest() requires either a YAML string or an object');
77
78
  }
78
79
  this.validateManifestStructure(manifestObject);
79
- return new ApiObject(this.chart, id, {
80
- apiVersion: manifestObject['apiVersion'],
81
- kind: manifestObject['kind'],
82
- metadata: manifestObject['metadata'],
83
- spec: manifestObject['spec'],
84
- });
80
+ const apiObjectProps = {
81
+ apiVersion: manifestObject.apiVersion,
82
+ kind: manifestObject.kind,
83
+ metadata: manifestObject.metadata,
84
+ ...manifestObject,
85
+ };
86
+ return new ApiObject(this.chart, id, apiObjectProps);
85
87
  }
86
88
  addTemplateManifest(yamlTemplate, id) {
87
89
  const templateAsset = {
@@ -169,13 +171,10 @@ ${yamlContent.trim()}
169
171
  });
170
172
  throw new Error('Manifest metadata must have a name');
171
173
  }
172
- if (typeof metadata.name !== 'string' && !isHelmExpression(metadata.name)) {
173
- this.logger.error('Validation failed: metadata.name must be a string or HelmExpression', {
174
- operation: 'manifest_validation',
175
- issue: 'invalid_metadata_name_type',
176
- actual_type: typeof metadata.name,
177
- });
178
- throw new Error('Manifest metadata.name must be a string or HelmExpression');
174
+ const name = manifest['metadata']?.['name'];
175
+ if (typeof name !== 'string' && !isHelmExpression(name) && !isHelmConstruct(name)) {
176
+ const actualType = Array.isArray(name) ? 'array' : name === null ? 'null' : typeof name;
177
+ throw new Error(`Manifest metadata.name must be a string, HelmExpression, or HelmConstruct. Got ${actualType}`);
179
178
  }
180
179
  }
181
180
  getMeta() {
@@ -242,7 +241,7 @@ ${yamlContent.trim()}
242
241
  const synthAssets = [];
243
242
  if (this.props.singleManifestFile) {
244
243
  const combinedYaml = enriched
245
- .map((obj) => this.processHelmTemplates(dumpHelmAwareYaml(obj).trim()))
244
+ .map((obj) => dumpHelmAwareYaml(obj).trim())
246
245
  .filter(Boolean)
247
246
  .join('\n---\n');
248
247
  const manifestId = this.props.manifestPrefix ?? 'manifests';
@@ -252,7 +251,7 @@ ${yamlContent.trim()}
252
251
  enriched.forEach((obj, index) => {
253
252
  const apiObjectId = apiObjectIds[index];
254
253
  const manifestId = apiObjectId || `manifest-${index + 1}`;
255
- const yaml = this.processHelmTemplates(dumpHelmAwareYaml(obj).trim());
254
+ const yaml = dumpHelmAwareYaml(obj).trim();
256
255
  if (yaml) {
257
256
  synthAssets.push({ id: manifestId, yaml });
258
257
  }
@@ -270,173 +269,6 @@ ${yamlContent.trim()}
270
269
  timer();
271
270
  return synthAssets;
272
271
  }
273
- processHelmTemplates(yaml) {
274
- let processed = this.fixCharacterMappingIssues(yaml);
275
- processed = this.applyHelmTemplateReplacements(processed);
276
- return processed;
277
- }
278
- fixCharacterMappingIssues(yaml) {
279
- const lines = yaml.split('\n');
280
- const fixedLines = [];
281
- let i = 0;
282
- while (i < lines.length) {
283
- const line = lines[i];
284
- if (!line) {
285
- i++;
286
- continue;
287
- }
288
- const charKeyMatch = line.match(/^(\s*)"(\d+)":\s*(.+)$/);
289
- if (charKeyMatch && charKeyMatch[2] && parseInt(charKeyMatch[2]) === 0) {
290
- const indent = charKeyMatch[1] || '';
291
- const result = this.processCharacterMapping(lines, i, indent);
292
- if (result.reconstructed) {
293
- fixedLines.push(result.reconstructed);
294
- i = result.nextIndex;
295
- continue;
296
- }
297
- }
298
- if (line) {
299
- fixedLines.push(line);
300
- }
301
- i++;
302
- }
303
- return fixedLines.join('\n');
304
- }
305
- processCharacterMapping(lines, startIndex, indent) {
306
- const charMappings = [];
307
- let j = startIndex;
308
- while (j < lines.length) {
309
- const currentLine = lines[j];
310
- if (!currentLine) {
311
- break;
312
- }
313
- const currentMatch = currentLine.match(/^(\s*)"(\d+)":\s*(.+)$/);
314
- if (currentMatch && currentMatch[1] === indent && currentMatch[2] && currentMatch[3]) {
315
- const index = parseInt(currentMatch[2]);
316
- let char = currentMatch[3];
317
- if (char && char.startsWith('"') && char.endsWith('"')) {
318
- char = char.slice(1, -1);
319
- }
320
- charMappings.push({ index, char });
321
- j++;
322
- }
323
- else {
324
- break;
325
- }
326
- }
327
- if (charMappings.length > 3) {
328
- charMappings.sort((a, b) => a.index - b.index);
329
- const reconstructed = charMappings.map((m) => m.char).join('');
330
- if (reconstructed.includes('{{') && reconstructed.includes('}}')) {
331
- return { reconstructed: `${indent}${reconstructed}`, nextIndex: j };
332
- }
333
- }
334
- return { nextIndex: j };
335
- }
336
- applyHelmTemplateReplacements(processed) {
337
- processed = this.fixConditionalBlocks(processed);
338
- processed = this.fixPipeExpressions(processed);
339
- processed = this.fixNestedQuotes(processed);
340
- processed = this.cleanupArtifacts(processed);
341
- processed = this.removeHelmExpressionQuotes(processed);
342
- return processed;
343
- }
344
- fixIncludeStatements(processed) {
345
- processed = processed.replace(/\{\{ include \\"([^"]+)\\" \. \| nindent (\d+) \}\}/g, '{{ include "$1" . | nindent $2 }}');
346
- processed = processed.replace(/\{\{ include \\"([^"]+)\\" \. \}\}/g, '{{ include "$1" . }}');
347
- return processed;
348
- }
349
- fixFunctionCalls(processed) {
350
- return processed.replace(/\{\{ (\w+) \\"([^"]*)\\" ([^}]*) \}\}/g, '{{ $1 "$2" $3 }}');
351
- }
352
- fixChartReferences(processed) {
353
- return processed.replace(/\{\{ \.Chart\.(\w+) \| replace \\"([^"]*)\\" \\"([^"]*)\\" ([^}]*) \}\}/g, '{{ .Chart.$1 | replace "$2" "$3" $4 }}');
354
- }
355
- removeHelmExpressionQuotes(processed) {
356
- const HELM_EXPRESSION_REPLACEMENT = '$1$2: {{$3}}';
357
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\|\s*(?:int|float|bool|number)[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
358
- processed = processed.replace(/^(\s*)(port):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
359
- processed = processed.replace(/^(\s*)(port\.number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
360
- processed = processed.replace(/^(\s*)(service\.port):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
361
- processed = processed.replace(/^(\s*)(backend\.service\.port\.number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
362
- processed = processed.replace(/^(\s*)(spec\.port):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
363
- processed = processed.replace(/^(\s*)([^:\s]*\.number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
364
- processed = processed.replace(/^(\s*)(number):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
365
- processed = processed.replace(/^(\s*)(replicas):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
366
- processed = processed.replace(/^(\s*)(targetPort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
367
- processed = processed.replace(/^(\s*)(nodePort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
368
- processed = processed.replace(/^(\s*)(containerPort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
369
- processed = processed.replace(/^(\s*)(hostPort):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
370
- processed = processed.replace(/^(\s*)(weight):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
371
- processed = processed.replace(/^(\s*)(priority):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
372
- processed = processed.replace(/^(\s*)(timeoutSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
373
- processed = processed.replace(/^(\s*)(periodSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
374
- processed = processed.replace(/^(\s*)(successThreshold):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
375
- processed = processed.replace(/^(\s*)(failureThreshold):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
376
- processed = processed.replace(/^(\s*)(initialDelaySeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
377
- processed = processed.replace(/^(\s*)(terminationGracePeriodSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
378
- processed = processed.replace(/^(\s*)(activeDeadlineSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
379
- processed = processed.replace(/^(\s*)(backoffLimit):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
380
- processed = processed.replace(/^(\s*)(parallelism):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
381
- processed = processed.replace(/^(\s*)(completions):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
382
- processed = processed.replace(/^(\s*)(revisionHistoryLimit):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
383
- processed = processed.replace(/^(\s*)(progressDeadlineSeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
384
- processed = processed.replace(/^(\s*)(minReadySeconds):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
385
- processed = processed.replace(/^(\s*)(maxUnavailable):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
386
- processed = processed.replace(/^(\s*)(maxSurge):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
387
- processed = processed.replace(/^(\s*)(cpu):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
388
- processed = processed.replace(/^(\s*)(memory):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
389
- processed = processed.replace(/^(\s*)(enabled):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
390
- processed = processed.replace(/^(\s*)(create):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
391
- processed = processed.replace(/^(\s*)(allow):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
392
- processed = processed.replace(/^(\s*)(disable):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
393
- processed = processed.replace(/^(\s*)(force):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
394
- processed = processed.replace(/^(\s*)(required):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
395
- processed = processed.replace(/^(\s*)(optional):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
396
- processed = processed.replace(/^(\s*)(readOnly):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
397
- processed = processed.replace(/^(\s*)(runAsNonRoot):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
398
- processed = processed.replace(/^(\s*)(privileged):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
399
- processed = processed.replace(/^(\s*)(allowPrivilegeEscalation):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
400
- processed = processed.replace(/^(\s*)(readOnlyRootFilesystem):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
401
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*true[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
402
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*false[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
403
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\d+[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
404
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\beq\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
405
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bne\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
406
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\blt\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
407
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\ble\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
408
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bgt\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
409
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bge\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
410
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\band\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
411
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bor\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
412
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*\bnot\b[^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
413
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*[+][^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
414
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*[-][^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
415
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*[*][^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
416
- processed = processed.replace(/:\s*"([^"]*\{\{[^}]+\}\}[^"]*)"/g, ': $1');
417
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"(true|false)"$/gm, '$1$2: $3');
418
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"(-?\d+)"$/gm, '$1$2: $3');
419
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"(-?\d+\.\d+)"$/gm, '$1$2: $3');
420
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"(-?\d+\.?\d*[eE][+-]?\d+)"$/gm, '$1$2: $3');
421
- processed = processed.replace(/^(\s*)([^:\s]+):\s*"\{\{([^}]*)\}\}"/gm, HELM_EXPRESSION_REPLACEMENT);
422
- processed = processed.replace(/"\{\{([^}]*)\}\}"/g, '{{$1}}');
423
- processed = processed.replace(/^(\s*)-\s*"\{\{([^}]*)\}\}"/gm, '$1- {{$2}}');
424
- return processed;
425
- }
426
- fixConditionalBlocks(processed) {
427
- return processed.replace(/"\{\{-\s*(if|with|range)([^}]*)\}\}([^"]*)\{\{-\s*end\s*\}\}"/g, '{{- $1$2}}$3{{- end }}');
428
- }
429
- fixPipeExpressions(processed) {
430
- return processed.replace(/"\{\{([^}]*\|[^}]*)\}\}"/g, '{{$1}}');
431
- }
432
- fixNestedQuotes(processed) {
433
- return processed.replace(/\{\{([^}]*)\\"([^"]*)\\"([^}]*)\}\}/g, '{{$1"$2"$3}}');
434
- }
435
- cleanupArtifacts(processed) {
436
- processed = processed.replace(/": "\.nan"/g, ': .nan');
437
- processed = processed.replace(/: \{\{([^}]+)\}\}$/gm, ': {{$1}}');
438
- return processed;
439
- }
440
272
  write(outDir) {
441
273
  const timer = this.logger.time('chart_write');
442
274
  this.logger.info('Starting chart write operation', {
@@ -107,10 +107,10 @@ export function createFlexibleSubchart(scope, id, config) {
107
107
  }
108
108
  export function generateFlexibleSubchartTemplate(name) {
109
109
  return `import { App } from 'cdk8s';
110
- import { Rutter } from 'timonel';
110
+ import { Rutter, helmInclude, helmIf, helmWith, createHelmExpression as helm } from 'timonel';
111
111
 
112
112
  /**
113
- * Creates a new chart with simple Rutter implementation
113
+ * Creates a new chart with type-safe Helm helpers
114
114
  * @returns Rutter instance for Helm chart generation
115
115
  * @since 2.11.0
116
116
  */
@@ -126,45 +126,43 @@ export default function createChart() {
126
126
  scope: app,
127
127
  defaultValues: {
128
128
  appName: '${name}',
129
- image: 'nginx:latest',
129
+ image: {
130
+ repository: 'nginx',
131
+ tag: 'latest'
132
+ },
130
133
  port: 80,
131
134
  replicas: 1,
132
135
  },
133
136
  });
134
137
 
135
- // Add simple deployment
138
+ // Add Deployment with type-safe helpers
136
139
  rutter.addManifest({
137
140
  apiVersion: 'apps/v1',
138
141
  kind: 'Deployment',
139
142
  metadata: {
140
- name: '${name}',
141
- labels: {
142
- 'app.kubernetes.io/name': '${name}'
143
- }
143
+ name: helmInclude('chart.fullname', '.'),
144
+ labels: helmInclude('chart.labels', '.', { pipe: 'nindent 4' })
144
145
  },
145
146
  spec: {
146
- replicas: 1,
147
+ replicas: helmIf('.Values.autoscaling.enabled', '{{ .Values.replicas }}', '1'),
147
148
  selector: {
148
- matchLabels: {
149
- 'app.kubernetes.io/name': '${name}'
150
- }
149
+ matchLabels: helmInclude('chart.selectorLabels', '.', { pipe: 'nindent 6' })
151
150
  },
152
151
  template: {
153
152
  metadata: {
154
- labels: {
155
- 'app.kubernetes.io/name': '${name}'
156
- }
153
+ labels: helmInclude('chart.selectorLabels', '.', { pipe: 'nindent 8' }),
154
+ annotations: helmWith('.Values.podAnnotations', helm('{{- toYaml . | nindent 8 }}'))
157
155
  },
158
156
  spec: {
159
157
  containers: [{
160
158
  name: '${name}',
161
- image: 'nginx:latest',
159
+ image: helm('{{ .Values.image.repository }}:{{ .Values.image.tag }}'),
162
160
  ports: [{
163
- containerPort: 80
161
+ containerPort: helm('{{ .Values.port }}')
164
162
  }],
165
163
  env: [
166
- { name: 'APP_NAME', value: '${name}' },
167
- { name: 'PORT', value: '80' }
164
+ { name: 'APP_NAME', value: helm('{{ .Values.appName }}') },
165
+ { name: 'PORT', value: helm('{{ .Values.port | toString }}') }
168
166
  ]
169
167
  }]
170
168
  }
@@ -172,27 +170,23 @@ export default function createChart() {
172
170
  }
173
171
  }, 'deployment');
174
172
 
175
- // Add Service
173
+ // Add Service with type-safe helpers
176
174
  rutter.addManifest({
177
175
  apiVersion: 'v1',
178
176
  kind: 'Service',
179
177
  metadata: {
180
- name: '${name}',
181
- labels: {
182
- 'app.kubernetes.io/name': '${name}'
183
- }
178
+ name: helmInclude('chart.fullname', '.'),
179
+ labels: helmInclude('chart.labels', '.', { pipe: 'nindent 4' })
184
180
  },
185
181
  spec: {
186
182
  type: 'ClusterIP',
187
183
  ports: [{
188
- port: 80,
189
- targetPort: 80,
184
+ port: helm('{{ .Values.port }}'),
185
+ targetPort: helm('{{ .Values.port }}'),
190
186
  protocol: 'TCP',
191
187
  name: 'http'
192
188
  }],
193
- selector: {
194
- 'app.kubernetes.io/name': '${name}'
195
- }
189
+ selector: helmInclude('chart.selectorLabels', '.', { pipe: 'nindent 4' })
196
190
  }
197
191
  }, 'service');
198
192
 
@@ -1,16 +1,16 @@
1
1
  import { writeFileSync, mkdirSync, existsSync, copyFileSync, readdirSync, rmSync } from 'fs';
2
2
  import { join } from 'path';
3
- import * as jsYaml from 'js-yaml';
3
+ import { parse } from 'yaml';
4
4
  import { App, Chart, ApiObject } from 'cdk8s';
5
5
  import { dumpHelmAwareYaml } from '../utils/helmYamlSerializer.js';
6
6
  import { generateHelpersTemplate } from '../utils/helmHelpers.js';
7
7
  import { createFlexibleSubchart } from './flexible-subchart.js';
8
8
  export function generateUmbrellaChart(name) {
9
9
  return `import { App } from 'cdk8s';
10
- import { Rutter } from 'timonel';
10
+ import { Rutter, helmInclude, helmIf, helmWith, createHelmExpression as helm } from 'timonel';
11
11
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
12
12
  import { join } from 'path';
13
- import * as jsYaml from 'js-yaml';
13
+ import { parse, stringify } from 'yaml';
14
14
  // Import subcharts - add your subchart imports here
15
15
 
16
16
  type SynthMode = 'dependencies' | 'inline';
@@ -42,7 +42,7 @@ function readYamlFile(filePath: string): Record<string, unknown> {
42
42
  if (!existsSync(filePath)) {
43
43
  return {};
44
44
  }
45
- const content = jsYaml.load(readFileSync(filePath, 'utf8'));
45
+ const content = parse(readFileSync(filePath, 'utf8'));
46
46
  return content && typeof content === 'object' ? (content as Record<string, unknown>) : {};
47
47
  }
48
48
 
@@ -147,8 +147,8 @@ export function synth(outDir: string, options?: SynthOptions) {
147
147
  });
148
148
  }
149
149
 
150
- writeFileSync(chartPath, jsYaml.dump(chartDoc));
151
- writeFileSync(valuesPath, jsYaml.dump(valuesDoc));
150
+ writeFileSync(chartPath, stringify(chartDoc));
151
+ writeFileSync(valuesPath, stringify(valuesDoc));
152
152
 
153
153
  app.synth();
154
154
 
@@ -327,7 +327,7 @@ export class UmbrellaChartTemplate extends Chart {
327
327
  const assets = getAssets?.() || [];
328
328
  assets.forEach((asset, assetIndex) => {
329
329
  try {
330
- const manifest = jsYaml.load(asset.yaml);
330
+ const manifest = parse(asset.yaml);
331
331
  if (manifest?.apiVersion && manifest?.kind) {
332
332
  new ApiObject(flexibleSubchart, `${String(manifest.kind).toLowerCase()}-${assetIndex}`, {
333
333
  apiVersion: String(manifest.apiVersion),
@@ -0,0 +1,3 @@
1
+ import type { HelmConstruct, HelmContent } from './helmControlStructures.js';
2
+ export declare function serializeHelmConstruct(construct: HelmConstruct, indent?: number): string;
3
+ export declare function serializeHelmContent(content: HelmContent, indent?: number): string;
@@ -0,0 +1,61 @@
1
+ import { isHelmConstruct } from './helmControlStructures.js';
2
+ export function serializeHelmConstruct(construct, indent = 0) {
3
+ const trimLeft = construct.options?.trimLeft ?? true;
4
+ const trimRight = construct.options?.trimRight ?? true;
5
+ const openTag = `{{${trimLeft ? '-' : ''} `;
6
+ const closeTag = ` ${trimRight ? '-' : ''}}}`;
7
+ switch (construct.type) {
8
+ case 'if': {
9
+ const data = construct.data;
10
+ let result = `${openTag}if ${data.condition}${closeTag}\n`;
11
+ result += serializeHelmContent(data.then, indent);
12
+ if (data.else !== undefined) {
13
+ result += `\n${openTag}else${closeTag}\n`;
14
+ result += serializeHelmContent(data.else, indent);
15
+ }
16
+ result += `\n${openTag}end${closeTag}`;
17
+ return result;
18
+ }
19
+ default:
20
+ throw new Error(`Unknown Helm construct type: ${construct.type}`);
21
+ }
22
+ }
23
+ export function serializeHelmContent(content, indent = 0) {
24
+ const indentStr = ' '.repeat(indent);
25
+ if (isHelmConstruct(content)) {
26
+ return serializeHelmConstruct(content, indent);
27
+ }
28
+ if (content === null || content === undefined) {
29
+ return '';
30
+ }
31
+ if (typeof content === 'string' || typeof content === 'number' || typeof content === 'boolean') {
32
+ return String(content);
33
+ }
34
+ if (Array.isArray(content)) {
35
+ return content
36
+ .map((item) => {
37
+ if (isHelmConstruct(item)) {
38
+ return serializeHelmConstruct(item, indent);
39
+ }
40
+ return serializeHelmContent(item, indent);
41
+ })
42
+ .join('\n');
43
+ }
44
+ if (typeof content === 'object') {
45
+ const lines = [];
46
+ for (const [key, value] of Object.entries(content)) {
47
+ if (isHelmConstruct(value)) {
48
+ lines.push(`${indentStr}${key}: ${serializeHelmConstruct(value, indent + 1)}`);
49
+ }
50
+ else if (typeof value === 'object' && value !== null) {
51
+ lines.push(`${indentStr}${key}:`);
52
+ lines.push(serializeHelmContent(value, indent + 1));
53
+ }
54
+ else {
55
+ lines.push(`${indentStr}${key}: ${value}`);
56
+ }
57
+ }
58
+ return lines.join('\n');
59
+ }
60
+ return '';
61
+ }
@@ -0,0 +1,34 @@
1
+ export interface HelmConstruct {
2
+ __helmConstruct: true;
3
+ type: 'if' | 'range' | 'with' | 'include' | 'define' | 'var' | 'block' | 'comment' | 'fragment';
4
+ data: unknown;
5
+ options?: {
6
+ trimLeft?: boolean;
7
+ trimRight?: boolean;
8
+ };
9
+ }
10
+ export declare function helmFragment(...contents: HelmContent[]): HelmConstruct;
11
+ export interface HelmExpression {
12
+ __helmExpression: true;
13
+ value: string;
14
+ }
15
+ export declare function createHelmExpression(value: string): HelmExpression;
16
+ export declare function isHelmExpression(value: unknown): value is HelmExpression;
17
+ export declare function helmRange(vars: string, collection: string, content: HelmContent, options?: HelmWhitespaceOptions): HelmConstruct;
18
+ export declare function helmWith(scope: string, content: HelmContent, options?: HelmWhitespaceOptions): HelmConstruct;
19
+ export declare function helmInclude(templateName: string, scope?: string, options?: {
20
+ pipe?: string;
21
+ } & HelmWhitespaceOptions): HelmConstruct;
22
+ export declare function helmDefine(name: string, content: HelmContent, options?: HelmWhitespaceOptions): HelmConstruct;
23
+ export declare function helmVar(name: string, value: string, options?: HelmWhitespaceOptions): HelmConstruct;
24
+ export declare function helmBlock(name: string, content: HelmContent, options?: HelmWhitespaceOptions): HelmConstruct;
25
+ export declare function helmComment(text: string): HelmConstruct;
26
+ export type HelmContent = string | number | boolean | null | undefined | HelmConstruct | HelmExpression | {
27
+ [key: string]: HelmContent;
28
+ } | HelmContent[];
29
+ export interface HelmWhitespaceOptions {
30
+ trimLeft?: boolean;
31
+ trimRight?: boolean;
32
+ }
33
+ export declare function helmIf(condition: string, thenContent: HelmContent, elseContent?: HelmContent, options?: HelmWhitespaceOptions): HelmConstruct;
34
+ export declare function isHelmConstruct(value: unknown): value is HelmConstruct;
@@ -0,0 +1,112 @@
1
+ export function helmFragment(...contents) {
2
+ return {
3
+ __helmConstruct: true,
4
+ type: 'fragment',
5
+ data: contents,
6
+ };
7
+ }
8
+ export function createHelmExpression(value) {
9
+ return {
10
+ __helmExpression: true,
11
+ value,
12
+ };
13
+ }
14
+ export function isHelmExpression(value) {
15
+ return (typeof value === 'object' &&
16
+ value !== null &&
17
+ '__helmExpression' in value &&
18
+ value.__helmExpression === true);
19
+ }
20
+ export function helmRange(vars, collection, content, options) {
21
+ return {
22
+ __helmConstruct: true,
23
+ type: 'range',
24
+ data: {
25
+ vars,
26
+ collection,
27
+ content,
28
+ },
29
+ ...(options ? { options } : {}),
30
+ };
31
+ }
32
+ export function helmWith(scope, content, options) {
33
+ return {
34
+ __helmConstruct: true,
35
+ type: 'with',
36
+ data: {
37
+ scope,
38
+ content,
39
+ },
40
+ ...(options ? { options } : {}),
41
+ };
42
+ }
43
+ export function helmInclude(templateName, scope = '.', options) {
44
+ return {
45
+ __helmConstruct: true,
46
+ type: 'include',
47
+ data: {
48
+ templateName,
49
+ scope,
50
+ pipe: options?.pipe,
51
+ },
52
+ ...(options ? { options } : {}),
53
+ };
54
+ }
55
+ export function helmDefine(name, content, options) {
56
+ return {
57
+ __helmConstruct: true,
58
+ type: 'define',
59
+ data: {
60
+ name,
61
+ content,
62
+ },
63
+ ...(options ? { options } : {}),
64
+ };
65
+ }
66
+ export function helmVar(name, value, options) {
67
+ return {
68
+ __helmConstruct: true,
69
+ type: 'var',
70
+ data: {
71
+ name,
72
+ value,
73
+ },
74
+ ...(options ? { options } : {}),
75
+ };
76
+ }
77
+ export function helmBlock(name, content, options) {
78
+ return {
79
+ __helmConstruct: true,
80
+ type: 'block',
81
+ data: {
82
+ name,
83
+ content,
84
+ },
85
+ ...(options ? { options } : {}),
86
+ };
87
+ }
88
+ export function helmComment(text) {
89
+ return {
90
+ __helmConstruct: true,
91
+ type: 'comment',
92
+ data: { text },
93
+ };
94
+ }
95
+ export function helmIf(condition, thenContent, elseContent, options) {
96
+ return {
97
+ __helmConstruct: true,
98
+ type: 'if',
99
+ data: {
100
+ condition,
101
+ then: thenContent,
102
+ else: elseContent,
103
+ },
104
+ ...(options ? { options } : {}),
105
+ };
106
+ }
107
+ export function isHelmConstruct(value) {
108
+ return (typeof value === 'object' &&
109
+ value !== null &&
110
+ '__helmConstruct' in value &&
111
+ value.__helmConstruct === true);
112
+ }
@@ -1,26 +1,12 @@
1
- import * as jsYaml from 'js-yaml';
2
- type HelmExpressionType = 'block' | 'nested' | 'comment' | 'action-trimmed' | 'raw' | 'include-context' | 'generic';
3
- interface HelmExpressionMatch {
4
- type: HelmExpressionType;
5
- expression: string;
6
- start: number;
7
- end: number;
8
- }
9
- export interface HelmExpression {
10
- __helmExpression: true;
11
- value: string;
12
- }
13
- declare function createHelmExpression(value: string): HelmExpression;
14
- declare function isHelmExpression(value: unknown): value is HelmExpression;
15
- declare function detectHelmExpressions(str: string): HelmExpressionMatch[];
16
- declare function preprocessHelmExpressions(obj: unknown, depth?: number): unknown;
17
- declare function postProcessHelmExpressions(yaml: string): string;
18
- export declare function dumpHelmAwareYaml(obj: unknown, options?: jsYaml.DumpOptions): string;
1
+ export declare function dumpHelmAwareYaml(obj: unknown, options?: {
2
+ lineWidth?: number;
3
+ }): string;
19
4
  export declare function stringify(obj: unknown, options?: {
20
5
  lineWidth?: number;
21
6
  doubleQuotedAsJSON?: boolean;
22
7
  simpleKeys?: boolean;
23
8
  }): string;
9
+ type HelmExpressionType = 'block' | 'nested' | 'comment' | 'action-trimmed' | 'raw' | 'include-context' | 'generic';
24
10
  export interface HelmValidationError {
25
11
  type: 'syntax' | 'semantic' | 'reference' | 'type';
26
12
  message: string;
@@ -48,13 +34,7 @@ export declare function parseHelmExpressions(content: string): Array<{
48
34
  endLine: number;
49
35
  endCol: number;
50
36
  }>;
51
- export interface PrettifyOptions {
52
- indentSize?: number;
53
- alignExpressions?: boolean;
54
- preserveComments?: boolean;
55
- maxLineWidth?: number;
56
- sortKeys?: boolean;
57
- groupHelpers?: boolean;
58
- }
59
- export declare function prettifyHelmTemplate(yaml: string, options?: PrettifyOptions): string;
60
- export { createHelmExpression, isHelmExpression, detectHelmExpressions, preprocessHelmExpressions, postProcessHelmExpressions, };
37
+ export declare function detectHelmExpressions(str: string): unknown[];
38
+ export declare function preprocessHelmExpressions(obj: unknown): unknown;
39
+ export declare function postProcessHelmExpressions(yaml: string): string;
40
+ export {};
@@ -1,114 +1,184 @@
1
- import * as jsYaml from 'js-yaml';
2
- const _HELM_EXPRESSION_PATTERNS = [
3
- /\{\{-?\s*define\s+[^}]+\s*-?\}\}[\s\S]*?\{\{-?\s*end\s*-?\}\}/g,
4
- /\{\{[^}]*\{\{[^}]*\}\}[^}]*\}\}/g,
5
- /\{\{\/\*[\s\S]*?\*\/\}\}/g,
6
- /\{\{-?[\s\S]*?-?\}\}/g,
7
- ];
8
- const COMPILED_PATTERNS = [
9
- { regex: /\{\{-?\s*define\s+[^}]+\s*-?\}\}[\s\S]*?\{\{-?\s*end\s*-?\}\}/g, type: 'block' },
10
- { regex: /\{\{[^}]*\{\{[^}]*\}\}[^}]*\}\}/g, type: 'nested' },
11
- { regex: /\{\{\/\*[\s\S]*?\*\/\}\}/g, type: 'comment' },
12
- { regex: /\{\{-?[\s\S]*?-?\}\}/g, type: 'action-trimmed' },
13
- { regex: /\{\{`[\s\S]*?`\}\}/g, type: 'raw' },
14
- { regex: /\{\{\s*include\s+"[^"]+"\s+[^}]+\s*\}\}/g, type: 'include-context' },
15
- ];
16
- function createHelmExpression(value) {
17
- return {
18
- __helmExpression: true,
19
- value,
20
- };
21
- }
22
- function isHelmExpression(value) {
23
- return (typeof value === 'object' &&
24
- value !== null &&
25
- '__helmExpression' in value &&
26
- value.__helmExpression === true);
1
+ import { Document, Scalar, isMap, isScalar, visit } from 'yaml';
2
+ import { isHelmConstruct, isHelmExpression, createHelmExpression, } from './helmControlStructures.js';
3
+ function serializeElseIfChain(elseContent, openTag, closeTag) {
4
+ let result = '';
5
+ let currentElse = elseContent;
6
+ if (isHelmConstruct(currentElse) && currentElse.type === 'if') {
7
+ while (isHelmConstruct(currentElse) && currentElse.type === 'if') {
8
+ const elseIfData = currentElse.data;
9
+ result += `\n${openTag}else if ${elseIfData.condition}${closeTag}\n`;
10
+ result += serializeHelmContent(elseIfData.then);
11
+ currentElse = elseIfData.else;
12
+ }
13
+ }
14
+ return { result, remainingElse: currentElse };
27
15
  }
28
- function detectHelmExpressions(str) {
29
- const matches = [];
30
- for (const { regex, type } of COMPILED_PATTERNS) {
31
- regex.lastIndex = 0;
32
- let result;
33
- while ((result = regex.exec(str)) !== null) {
34
- matches.push({
35
- type,
36
- expression: result[0],
37
- start: result.index,
38
- end: result.index + result[0].length,
39
- });
40
- if (result.index === regex.lastIndex)
41
- regex.lastIndex++;
16
+ function serializeHelmConstruct(construct) {
17
+ const trimLeft = construct.options?.trimLeft ?? true;
18
+ const trimRight = construct.options?.trimRight ?? true;
19
+ const openTag = `{{${trimLeft ? '-' : ''} `;
20
+ const closeTag = ` ${trimRight ? '-' : ''}}}`;
21
+ switch (construct.type) {
22
+ case 'if': {
23
+ const data = construct.data;
24
+ let result = `${openTag}if ${data.condition}${closeTag}\n`;
25
+ result += serializeHelmContent(data.then);
26
+ if (data.else !== undefined) {
27
+ const { result: elseIfResult, remainingElse } = serializeElseIfChain(data.else, openTag, closeTag);
28
+ result += elseIfResult;
29
+ if (remainingElse !== undefined) {
30
+ result += `\n${openTag}else${closeTag}\n`;
31
+ result += serializeHelmContent(remainingElse);
32
+ }
33
+ }
34
+ result += `\n${openTag}end${closeTag}`;
35
+ return result;
36
+ }
37
+ case 'fragment': {
38
+ const data = construct.data;
39
+ return data.map((item) => serializeHelmContent(item)).join('\n');
40
+ }
41
+ case 'range': {
42
+ const data = construct.data;
43
+ let result = `${openTag}range ${data.vars} := ${data.collection}${closeTag}\n`;
44
+ result += serializeHelmContent(data.content);
45
+ result += `\n${openTag}end${closeTag}`;
46
+ return result;
47
+ }
48
+ case 'with': {
49
+ const data = construct.data;
50
+ let result = `${openTag}with ${data.scope}${closeTag}\n`;
51
+ result += serializeHelmContent(data.content);
52
+ result += `\n${openTag}end${closeTag}`;
53
+ return result;
42
54
  }
55
+ case 'include': {
56
+ const data = construct.data;
57
+ let result = `${openTag}include "${data.templateName}" ${data.scope}`;
58
+ if (data.pipe) {
59
+ result += ` | ${data.pipe}`;
60
+ }
61
+ result += `${closeTag}`;
62
+ return result;
63
+ }
64
+ case 'define': {
65
+ const data = construct.data;
66
+ let result = `${openTag}define "${data.name}"${closeTag}\n`;
67
+ result += serializeHelmContent(data.content);
68
+ result += `\n${openTag}end${closeTag}`;
69
+ return result;
70
+ }
71
+ case 'var': {
72
+ const data = construct.data;
73
+ return `${openTag}${data.name} := ${data.value}${closeTag}`;
74
+ }
75
+ case 'block': {
76
+ const data = construct.data;
77
+ let result = `${openTag}block "${data.name}" .${closeTag}\n`;
78
+ result += serializeHelmContent(data.content);
79
+ result += `\n${openTag}end${closeTag}`;
80
+ return result;
81
+ }
82
+ case 'comment': {
83
+ const data = construct.data;
84
+ return `{{/* ${data.text} */}}`;
85
+ }
86
+ default:
87
+ throw new Error(`Unknown Helm construct type: ${construct.type}`);
43
88
  }
44
- return matches.sort((a, b) => a.start - b.start);
45
89
  }
46
- function preprocessHelmExpressions(obj, depth = 0) {
47
- if (depth > 100) {
48
- throw new Error('Maximum recursion depth exceeded during Helm preprocessing');
90
+ function serializeHelmContent(content) {
91
+ if (isHelmConstruct(content)) {
92
+ return serializeHelmConstruct(content);
49
93
  }
50
- if (isHelmExpression(obj)) {
51
- return obj;
94
+ if (isHelmExpression(content)) {
95
+ return content.value;
52
96
  }
53
- if (typeof obj === 'string') {
54
- if (detectHelmExpressions(obj).length > 0) {
55
- return createHelmExpression(obj);
56
- }
57
- return obj;
97
+ if (content === null || content === undefined) {
98
+ return '';
99
+ }
100
+ if (typeof content === 'string') {
101
+ return content;
102
+ }
103
+ if (typeof content === 'number' || typeof content === 'boolean') {
104
+ return String(content);
105
+ }
106
+ if (typeof content === 'object') {
107
+ const doc = new Document(content);
108
+ let yaml = doc.toString({ lineWidth: 0 });
109
+ yaml = yaml.trim();
110
+ return yaml;
111
+ }
112
+ return '';
113
+ }
114
+ function preprocessHelmConstructs(obj) {
115
+ if (isHelmConstruct(obj)) {
116
+ const helmTemplate = serializeHelmConstruct(obj);
117
+ return createHelmExpression(helmTemplate);
58
118
  }
59
119
  if (Array.isArray(obj)) {
60
- return obj.map((item) => preprocessHelmExpressions(item, depth + 1));
120
+ return obj.map((item) => preprocessHelmConstructs(item));
61
121
  }
62
- if (typeof obj === 'object' && obj !== null) {
122
+ if (obj !== null && typeof obj === 'object') {
123
+ if (isHelmExpression(obj)) {
124
+ return obj;
125
+ }
63
126
  const result = {};
64
127
  for (const [key, value] of Object.entries(obj)) {
65
- Object.defineProperty(result, key, {
66
- value: preprocessHelmExpressions(value, depth + 1),
67
- writable: true,
68
- enumerable: true,
69
- configurable: true,
70
- });
128
+ result[key] = preprocessHelmConstructs(value);
71
129
  }
72
130
  return result;
73
131
  }
74
132
  return obj;
75
133
  }
76
- function helmAwareReplacer(key, value) {
77
- if (isHelmExpression(value)) {
78
- return value.value;
79
- }
80
- return value;
81
- }
82
- function postProcessHelmExpressions(yaml) {
83
- let processed = yaml;
84
- processed = processed.replace(/'(\{\{[^}]*\}\})'/g, '$1');
85
- processed = processed.replace(/"(\{\{[^}]*\}\})"/g, '$1');
86
- processed = processed.replace(/'(\{\{[^}]*\\"[^}]*\}\})'/g, '$1');
87
- processed = processed.replace(/"(\{\{[^}]*\\"[^}]*\}\})"/g, '$1');
88
- processed = processed.replace(/\\(\{\{[^}]+\}\})/g, '$1');
89
- return processed;
90
- }
91
134
  export function dumpHelmAwareYaml(obj, options = {}) {
92
- const preprocessed = preprocessHelmExpressions(obj);
93
- const dumpOptions = {
94
- forceQuotes: false,
95
- lineWidth: options.lineWidth ?? 0,
96
- flowLevel: options.flowLevel ?? -1,
97
- replacer: helmAwareReplacer,
98
- ...options,
99
- };
100
- let yamlOutput = jsYaml.dump(preprocessed, dumpOptions);
101
- yamlOutput = postProcessHelmExpressions(yamlOutput);
102
- return yamlOutput;
135
+ const preprocessed = preprocessHelmConstructs(obj);
136
+ const doc = new Document(preprocessed);
137
+ visit(doc, (_key, node) => {
138
+ if (isMap(node)) {
139
+ const isHelmExpr = node.items.some((pair) => isScalar(pair.key) &&
140
+ pair.key.value === '__helmExpression' &&
141
+ isScalar(pair.value) &&
142
+ pair.value.value === true);
143
+ if (isHelmExpr) {
144
+ const valuePair = node.items.find((pair) => isScalar(pair.key) && pair.key.value === 'value');
145
+ if (valuePair && isScalar(valuePair.value)) {
146
+ const value = String(valuePair.value.value);
147
+ const scalar = new Scalar(value);
148
+ if (value.trim().startsWith('{{') && value.includes('\n')) {
149
+ scalar.type = 'BLOCK_LITERAL';
150
+ }
151
+ else {
152
+ scalar.type = 'QUOTE_DOUBLE';
153
+ }
154
+ return scalar;
155
+ }
156
+ }
157
+ }
158
+ return undefined;
159
+ });
160
+ const toStringOptions = {};
161
+ if (options.lineWidth !== undefined) {
162
+ toStringOptions.lineWidth = options.lineWidth;
163
+ }
164
+ let result = doc.toString(toStringOptions);
165
+ result = result.replace(/\\"/g, '"');
166
+ result = result.replace(/\\\\/g, '');
167
+ result = result.replace(/:\s*\|[-+]?\s*\n(\s*\{\{)/g, ':\n$1');
168
+ result = result.replace(/"(\{\{[\s\S]*?\}\})"/g, '$1');
169
+ return result;
103
170
  }
104
171
  export function stringify(obj, options = {}) {
105
- const jsYamlOptions = {
106
- lineWidth: options.lineWidth ?? 0,
107
- quotingType: options.doubleQuotedAsJSON ? '"' : "'",
108
- forceQuotes: options.doubleQuotedAsJSON,
109
- };
110
- return dumpHelmAwareYaml(obj, jsYamlOptions);
172
+ return dumpHelmAwareYaml(obj, options.lineWidth !== undefined ? { lineWidth: options.lineWidth } : {});
111
173
  }
174
+ const COMPILED_PATTERNS = [
175
+ { regex: /\{\{-?\s*define\s+[^}]+\s*-?\}\}[\s\S]*?\{\{-?\s*end\s*-?\}\}/g, type: 'block' },
176
+ { regex: /\{\{[^}]*\{\{[^}]*\}\}[^}]*\}\}/g, type: 'nested' },
177
+ { regex: /\{\{\/\*[\s\S]*?\*\/\}\}/g, type: 'comment' },
178
+ { regex: /\{\{-?[\s\S]*?-?\}\}/g, type: 'action-trimmed' },
179
+ { regex: /\{\{`[\s\S]*?`\}\}/g, type: 'raw' },
180
+ { regex: /\{\{\s*include\s+"[^"]+"\s+[^}]+\s*\}\}/g, type: 'include-context' },
181
+ ];
112
182
  export function validateHelmYaml(yaml) {
113
183
  const errors = [];
114
184
  const warnings = [];
@@ -280,120 +350,12 @@ function calculateComplexity(expressions) {
280
350
  }
281
351
  return score;
282
352
  }
283
- export function prettifyHelmTemplate(yaml, options = {}) {
284
- const { indentSize = 2, alignExpressions = true, preserveComments: _preserveComments = true, maxLineWidth = 120, sortKeys = false, groupHelpers = true, } = options;
285
- let formatted = yaml;
286
- if (groupHelpers) {
287
- formatted = groupHelmHelpers(formatted);
288
- }
289
- if (alignExpressions) {
290
- formatted = alignTemplateExpressions(formatted, indentSize);
291
- }
292
- formatted = formatConditionalBlocks(formatted, indentSize);
293
- formatted = formatLoopBlocks(formatted, indentSize);
294
- formatted = wrapLongLines(formatted, maxLineWidth);
295
- if (sortKeys) {
296
- formatted = sortYamlKeys(formatted);
297
- }
298
- return formatted;
299
- }
300
- function alignTemplateExpressions(yaml, _indentSize) {
301
- const lines = yaml.split('\n');
302
- const aligned = [];
303
- for (const line of lines) {
304
- const expressions = line.match(/\{\{[^}]*\}\}/g);
305
- if (expressions && expressions.length > 1) {
306
- const baseIndent = (line.match(/^\s*/) || [''])[0];
307
- const content = line.trim();
308
- const parts = content.split(/(\{\{[^}]*\}\})/);
309
- let alignedLine = baseIndent;
310
- for (let i = 0; i < parts.length; i++) {
311
- alignedLine += parts[i] || '';
312
- const nextPart = parts[i + 1];
313
- if (i < parts.length - 1 && nextPart && nextPart.startsWith('{{')) {
314
- alignedLine += ' ';
315
- }
316
- }
317
- aligned.push(alignedLine);
318
- }
319
- else {
320
- aligned.push(line);
321
- }
322
- }
323
- return aligned.join('\n');
324
- }
325
- function groupHelmHelpers(yaml) {
326
- const lines = yaml.split('\n');
327
- const groups = {
328
- comments: [],
329
- definitions: [],
330
- conditionals: [],
331
- loops: [],
332
- other: [],
333
- };
334
- for (const line of lines) {
335
- if (line.includes('{{/*')) {
336
- groups.comments.push(line);
337
- }
338
- else if (line.includes('{{- define')) {
339
- groups.definitions.push(line);
340
- }
341
- else if (/\{\{\s*(if|else|end)\s/.test(line)) {
342
- groups.conditionals.push(line);
343
- }
344
- else if (/\{\{\s*(range|with)\s/.test(line)) {
345
- groups.loops.push(line);
346
- }
347
- else {
348
- groups.other.push(line);
349
- }
350
- }
351
- return [
352
- ...groups.comments,
353
- ...groups.definitions,
354
- ...groups.conditionals,
355
- ...groups.loops,
356
- ...groups.other,
357
- ].join('\n');
358
- }
359
- function formatConditionalBlocks(yaml, indentSize) {
360
- const indent = ' '.repeat(indentSize);
361
- return yaml
362
- .replace(/(\{\{\s*if\s+[^}]+\}\})/g, '$1\n' + indent)
363
- .replace(/(\{\{\s*else\s*\}\})/g, '$1\n' + indent)
364
- .replace(/(\{\{\s*end\s*\}\})/g, '\n$1');
365
- }
366
- function formatLoopBlocks(yaml, indentSize) {
367
- const indent = ' '.repeat(indentSize);
368
- return yaml
369
- .replace(/(\{\{\s*range\s+[^}]+\}\})/g, '$1\n' + indent)
370
- .replace(/(\{\{\s*with\s+[^}]+\}\})/g, '$1\n' + indent);
353
+ export function detectHelmExpressions(str) {
354
+ return parseHelmExpressions(str);
371
355
  }
372
- function wrapLongLines(yaml, maxLineWidth) {
373
- if (maxLineWidth <= 0)
374
- return yaml;
375
- const lines = yaml.split('\n');
376
- const wrapped = [];
377
- for (const line of lines) {
378
- if (line.length <= maxLineWidth) {
379
- wrapped.push(line);
380
- continue;
381
- }
382
- let remaining = line;
383
- while (remaining.length > maxLineWidth) {
384
- let wrapIndex = remaining.lastIndexOf(' ', maxLineWidth);
385
- if (wrapIndex === -1)
386
- wrapIndex = maxLineWidth;
387
- wrapped.push(remaining.slice(0, wrapIndex));
388
- remaining = remaining.slice(wrapIndex).trim();
389
- }
390
- if (remaining.length > 0) {
391
- wrapped.push(remaining);
392
- }
393
- }
394
- return wrapped.join('\n');
356
+ export function preprocessHelmExpressions(obj) {
357
+ return preprocessHelmConstructs(obj);
395
358
  }
396
- function sortYamlKeys(yaml) {
359
+ export function postProcessHelmExpressions(yaml) {
397
360
  return yaml;
398
361
  }
399
- export { createHelmExpression, isHelmExpression, detectHelmExpressions, preprocessHelmExpressions, postProcessHelmExpressions, };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "timonel",
3
3
  "type": "module",
4
- "version": "2.13.0",
4
+ "version": "2.14.0-beta.1",
5
5
  "description": "Timonel: programmatic Helm chart generator using cdk8s (TypeScript)",
6
6
  "bin": {
7
7
  "timonel": "dist/cli.js",
@@ -12,18 +12,27 @@
12
12
  "exports": {
13
13
  ".": {
14
14
  "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
15
16
  "default": "./dist/index.js"
16
17
  },
17
18
  "./lib/helm": {
18
19
  "types": "./dist/lib/helm.d.ts",
20
+ "import": "./dist/lib/helm.js",
19
21
  "default": "./dist/lib/helm.js"
20
22
  },
23
+ "./lib/utils/helmControlStructures": {
24
+ "types": "./dist/lib/utils/helmControlStructures.d.ts",
25
+ "import": "./dist/lib/utils/helmControlStructures.js",
26
+ "default": "./dist/lib/utils/helmControlStructures.js"
27
+ },
21
28
  "./lib/utils/logger": {
22
29
  "types": "./dist/lib/utils/logger.d.ts",
30
+ "import": "./dist/lib/utils/logger.js",
23
31
  "default": "./dist/lib/utils/logger.js"
24
32
  },
25
33
  "./lib/utils/helmYamlSerializer": {
26
34
  "types": "./dist/lib/utils/helmYamlSerializer.d.ts",
35
+ "import": "./dist/lib/utils/helmYamlSerializer.js",
27
36
  "default": "./dist/lib/utils/helmYamlSerializer.js"
28
37
  }
29
38
  },
@@ -95,7 +104,6 @@
95
104
  "cdk8s-plus-33": "^2.4.6",
96
105
  "constructs": "^10.4.3",
97
106
  "handlebars": "^4.7.8",
98
- "js-yaml": "^4.1.1",
99
107
  "pino": "^10.1.0",
100
108
  "pino-pretty": "^13.1.2",
101
109
  "ts-node": "^10.9.2",
@@ -108,7 +116,6 @@
108
116
  "@semantic-release/changelog": "^6.0.3",
109
117
  "@semantic-release/exec": "^7.1.0",
110
118
  "@semantic-release/git": "^10.0.1",
111
- "@types/js-yaml": "^4.0.9",
112
119
  "@types/node": "^24.10.1",
113
120
  "@typescript-eslint/eslint-plugin": "^8.47.0",
114
121
  "@typescript-eslint/parser": "^8.47.0",