helm-env-delta 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,6 +38,8 @@ HelmEnvDelta (`hed`) automates environment synchronization for GitOps workflows
38
38
 
39
39
  🎯 **Path Filtering** - Preserve environment-specific values (namespaces, replicas, secrets) that should never sync.
40
40
 
41
+ 📌 **Fixed Values** - Set specific fields to constant values regardless of source/destination. Enforce production settings like `debug: false` or `replicas: 3` after every sync.
42
+
41
43
  🔄 **Powerful Transforms** - Regex find/replace for both file content and paths. Load transforms from external YAML files for reusability. Change `uat-db.internal` → `prod-db.internal` automatically.
42
44
 
43
45
  🛡️ **Safety Rules** - Block major version upgrades, scaling violations, and forbidden patterns. Load validation rules from external files. Scan globally or target specific fields.
@@ -252,6 +254,21 @@ helm-env-delta --config example/5-external-files/config.yaml --dry-run --diff
252
254
  - Pattern files (`regexFile`, `regexFileKey`)
253
255
  - Global vs targeted regex validation
254
256
 
257
+ ### 📌 Example 6: Fixed Values
258
+
259
+ Set specific fields to constant values regardless of source/destination. Perfect for enforcing production settings.
260
+
261
+ ```bash
262
+ helm-env-delta --config example/6-fixed-values/config.yaml --dry-run --diff
263
+ ```
264
+
265
+ **Features shown:**
266
+
267
+ - Simple path fixed values (`debug: false`, `logLevel: warn`)
268
+ - Nested paths (`spec.replicas`)
269
+ - Array filter operators (`env[name=LOG_LEVEL].value`)
270
+ - Combining with skipPath and transforms
271
+
255
272
  ---
256
273
 
257
274
  ## 💡 Smart Configuration Suggestions (Heuristic)
@@ -421,6 +438,50 @@ skipPath:
421
438
 
422
439
  ---
423
440
 
441
+ ### 📌 Fixed Values (fixedValues)
442
+
443
+ Set specific JSONPath locations to constant values, regardless of source/destination values. Applied after merge, before formatting.
444
+
445
+ ```yaml
446
+ fixedValues:
447
+ # Apply to all YAML files
448
+ '**/*.yaml':
449
+ - path: 'debug'
450
+ value: false
451
+ - path: 'logLevel'
452
+ value: 'warn'
453
+
454
+ # Specific file patterns
455
+ 'deployment.yaml':
456
+ - path: 'spec.replicas'
457
+ value: 3
458
+ - path: 'spec.template.spec.containers[name=app].resources.limits.memory'
459
+ value: '512Mi'
460
+
461
+ # Array filter operators supported
462
+ 'configmap.yaml':
463
+ - path: 'data.env[name=LOG_LEVEL].value'
464
+ value: 'info'
465
+ - path: 'data.env[name^=FEATURE_].value' # startsWith
466
+ value: 'stable'
467
+ ```
468
+
469
+ **Supported filter operators:** `=` (equals), `^=` (startsWith), `$=` (endsWith), `*=` (contains) - updates ALL matching items
470
+
471
+ **Value types:** String, number, boolean, null, object, array
472
+
473
+ **Behavior:**
474
+
475
+ - **Filter operators update ALL matching items** (e.g., `env[name^=LOG_]` updates every item starting with `LOG_`)
476
+ - Applied during diff computation, so changes are visible in all reports (HTML, console, JSON)
477
+ - Non-existent paths are silently skipped
478
+ - Multiple rules for same path: last one wins
479
+ - Works with skipPath (fixedValues applied after skipPath restoration)
480
+
481
+ **Use cases:** Enforce production settings (`debug: false`), standardize resource limits, set mandatory environment variables, ensure consistent configuration across syncs.
482
+
483
+ ---
484
+
424
485
  ### 🔄 Transformations
425
486
 
426
487
  Regex find/replace for content and file paths. Load transforms from external files or define inline.
@@ -780,7 +841,7 @@ git push origin main
780
841
 
781
842
  ✅ **Flexibility** - Per-file patterns. Config inheritance. Regex transforms.
782
843
 
783
- ✅ **Reliability** - 920 tests, 84% coverage. Battle-tested.
844
+ ✅ **Reliability** - 1150+ tests, 84% coverage. Battle-tested.
784
845
 
785
846
  ---
786
847
 
@@ -77,6 +77,10 @@ declare const arraySortRuleSchema: z.ZodObject<{
77
77
  desc: "desc";
78
78
  }>>;
79
79
  }, z.core.$strip>;
80
+ declare const fixedValueRuleSchema: z.ZodObject<{
81
+ path: z.ZodString;
82
+ value: z.ZodUnknown;
83
+ }, z.core.$strip>;
80
84
  declare const transformRuleSchema: z.ZodObject<{
81
85
  find: z.ZodString;
82
86
  replace: z.ZodString;
@@ -159,6 +163,10 @@ declare const baseConfigSchema: z.ZodObject<{
159
163
  forbidden: "forbidden";
160
164
  }>>;
161
165
  }, z.core.$strict>], "type">>>>;
166
+ fixedValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
167
+ path: z.ZodString;
168
+ value: z.ZodUnknown;
169
+ }, z.core.$strip>>>>;
162
170
  }, z.core.$strip>;
163
171
  declare const finalConfigSchema: z.ZodObject<{
164
172
  transforms: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -208,6 +216,10 @@ declare const finalConfigSchema: z.ZodObject<{
208
216
  forbidden: "forbidden";
209
217
  }>>;
210
218
  }, z.core.$strict>], "type">>>>;
219
+ fixedValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
220
+ path: z.ZodString;
221
+ value: z.ZodUnknown;
222
+ }, z.core.$strip>>>>;
211
223
  include: z.ZodDefault<z.ZodArray<z.ZodString>>;
212
224
  exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
213
225
  prune: z.ZodDefault<z.ZodBoolean>;
@@ -242,6 +254,8 @@ export type TransformRule = z.infer<typeof transformRuleSchema>;
242
254
  export type TransformRules = z.infer<typeof transformRulesSchema>;
243
255
  export type TransformConfig = Record<string, TransformRules>;
244
256
  export type OutputFormat = BaseConfig['outputFormat'];
257
+ export type FixedValueRule = z.infer<typeof fixedValueRuleSchema>;
258
+ export type FixedValueConfig = Record<string, FixedValueRule[]>;
245
259
  export declare const parseBaseConfig: (data: unknown, configPath?: string) => BaseConfig;
246
260
  export declare const parseFinalConfig: (data: unknown, configPath?: string) => FinalConfig;
247
261
  export declare const parseConfig: (data: unknown, configPath?: string) => FinalConfig;
@@ -75,6 +75,10 @@ const arraySortRuleSchema = zod_1.z.object({
75
75
  sortBy: zod_1.z.string().min(1),
76
76
  order: zod_1.z.enum(['asc', 'desc']).default('asc')
77
77
  });
78
+ const fixedValueRuleSchema = zod_1.z.object({
79
+ path: zod_1.z.string().min(1).describe('JSONPath to the value to set'),
80
+ value: zod_1.z.unknown().describe('The constant value to set (any type: string, number, boolean, null, object, array)')
81
+ });
78
82
  const transformRuleSchema = zod_1.z
79
83
  .object({
80
84
  find: zod_1.z.string().min(1).describe('Regex pattern to find'),
@@ -123,7 +127,8 @@ const baseConfigSchema = zod_1.z.object({
123
127
  })
124
128
  .optional(),
125
129
  transforms: zod_1.z.record(zod_1.z.string(), transformRulesSchema).optional(),
126
- stopRules: zod_1.z.record(zod_1.z.string(), zod_1.z.array(stopRuleSchema)).optional()
130
+ stopRules: zod_1.z.record(zod_1.z.string(), zod_1.z.array(stopRuleSchema)).optional(),
131
+ fixedValues: zod_1.z.record(zod_1.z.string(), zod_1.z.array(fixedValueRuleSchema)).optional()
127
132
  });
128
133
  const finalConfigSchema = baseConfigSchema
129
134
  .omit({ extends: true })
@@ -24,6 +24,18 @@ const validateConfigWarnings = (config) => {
24
24
  for (const [pattern, rules] of Object.entries(config.transforms))
25
25
  if ((rules.content?.length ?? 0) === 0 && (rules.filename?.length ?? 0) === 0)
26
26
  warnings.push(`Transform pattern '${pattern}' has empty content and filename arrays (will have no effect)`);
27
+ if (config.fixedValues)
28
+ for (const [pattern, rules] of Object.entries(config.fixedValues))
29
+ if (rules.length === 0)
30
+ warnings.push(`fixedValues pattern '${pattern}' has empty array (will have no effect)`);
31
+ if (config.fixedValues && config.skipPath)
32
+ for (const [fixedPattern, fixedRules] of Object.entries(config.fixedValues))
33
+ for (const [skipPattern, skipPaths] of Object.entries(config.skipPath))
34
+ if (fixedPattern === skipPattern || fixedPattern === '**/*' || skipPattern === '**/*')
35
+ for (const rule of fixedRules)
36
+ for (const skipPath of skipPaths)
37
+ if (rule.path === skipPath || rule.path.startsWith(skipPath + '.'))
38
+ warnings.push(`fixedValues path '${rule.path}' overlaps with skipPath '${skipPath}' (fixedValues wins after skipPath restored)`);
27
39
  return {
28
40
  warnings,
29
41
  hasWarnings: warnings.length > 0
@@ -1,4 +1,4 @@
1
- import { Config, TransformConfig } from './configFile';
1
+ import { Config, FixedValueConfig, TransformConfig } from './configFile';
2
2
  import { FileMap } from './fileLoader';
3
3
  export interface FileDiffResult {
4
4
  addedFiles: string[];
@@ -27,6 +27,7 @@ export interface ProcessYamlOptions {
27
27
  destinationContent: string;
28
28
  skipPath?: Record<string, string[]>;
29
29
  transforms?: TransformConfig;
30
+ fixedValues?: FixedValueConfig;
30
31
  }
31
32
  declare const FileDiffErrorClass: {
32
33
  new (message: string, options?: import("./utils/errors").ErrorOptions): {
package/dist/fileDiff.js CHANGED
@@ -8,6 +8,7 @@ const yaml_1 = __importDefault(require("yaml"));
8
8
  const deepEqual_1 = require("./utils/deepEqual");
9
9
  const errors_1 = require("./utils/errors");
10
10
  const fileType_1 = require("./utils/fileType");
11
+ const fixedValues_1 = require("./utils/fixedValues");
11
12
  const jsonPath_1 = require("./utils/jsonPath");
12
13
  const patternMatcher_1 = require("./utils/patternMatcher");
13
14
  const serialization_1 = require("./utils/serialization");
@@ -105,7 +106,7 @@ const getSkipPathsForFile = (filePath, skipPath) => {
105
106
  };
106
107
  exports.getSkipPathsForFile = getSkipPathsForFile;
107
108
  const processYamlFile = (options) => {
108
- const { filePath, sourceContent, destinationContent, skipPath, transforms } = options;
109
+ const { filePath, sourceContent, destinationContent, skipPath, transforms, fixedValues } = options;
109
110
  let sourceParsed;
110
111
  let destinationParsed;
111
112
  try {
@@ -143,6 +144,9 @@ const processYamlFile = (options) => {
143
144
  throw parseError;
144
145
  }
145
146
  const sourceTransformed = (0, transformer_1.applyTransforms)(sourceParsed, filePath, transforms);
147
+ const fixedValueRules = (0, fixedValues_1.getFixedValuesForFile)(filePath, fixedValues);
148
+ if (fixedValueRules.length > 0)
149
+ (0, fixedValues_1.applyFixedValues)(sourceTransformed, fixedValueRules);
146
150
  const pathsToSkip = (0, exports.getSkipPathsForFile)(filePath, skipPath);
147
151
  const sourceFiltered = pathsToSkip.length > 0 ? applySkipPaths(sourceTransformed, pathsToSkip) : sourceTransformed;
148
152
  const destinationFiltered = pathsToSkip.length > 0 ? applySkipPaths(destinationParsed, pathsToSkip) : destinationParsed;
@@ -166,7 +170,7 @@ const processYamlFile = (options) => {
166
170
  parsedDest: destinationParsed
167
171
  };
168
172
  };
169
- const processChangedFiles = (sourceFiles, destinationFiles, skipPath, transforms, originalPaths) => {
173
+ const processChangedFiles = (sourceFiles, destinationFiles, skipPath, transforms, fixedValues, originalPaths) => {
170
174
  const changedFiles = [];
171
175
  const unchangedFiles = [];
172
176
  for (const [path, sourceContent] of sourceFiles.entries()) {
@@ -181,7 +185,8 @@ const processChangedFiles = (sourceFiles, destinationFiles, skipPath, transforms
181
185
  sourceContent,
182
186
  destinationContent,
183
187
  skipPath,
184
- transforms
188
+ transforms,
189
+ fixedValues
185
190
  });
186
191
  if (changed) {
187
192
  if (originalPath)
@@ -222,7 +227,7 @@ const computeFileDiff = (sourceFiles, destinationFiles, config, logger, original
222
227
  }
223
228
  const addedFiles = detectAddedFiles(sourceFiles, destinationFiles);
224
229
  const deletedFiles = config.prune ? detectDeletedFiles(sourceFiles, destinationFiles) : [];
225
- const { changedFiles, unchangedFiles } = processChangedFiles(sourceFiles, destinationFiles, config.skipPath, config.transforms, originalPaths);
230
+ const { changedFiles, unchangedFiles } = processChangedFiles(sourceFiles, destinationFiles, config.skipPath, config.transforms, config.fixedValues, originalPaths);
226
231
  return { addedFiles, deletedFiles, changedFiles, unchangedFiles };
227
232
  };
228
233
  exports.computeFileDiff = computeFileDiff;
@@ -8,8 +8,10 @@ const promises_1 = require("node:fs/promises");
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const yaml_1 = __importDefault(require("yaml"));
10
10
  const consoleFormatter_1 = require("./consoleFormatter");
11
+ const arrayMerger_1 = require("./utils/arrayMerger");
11
12
  const errors_1 = require("./utils/errors");
12
13
  const fileType_1 = require("./utils/fileType");
14
+ const fixedValues_1 = require("./utils/fixedValues");
13
15
  const transformer_1 = require("./utils/transformer");
14
16
  const yamlFormatter_1 = require("./yamlFormatter");
15
17
  const FileUpdaterErrorClass = (0, errors_1.createErrorClass)('File Updater Error', {
@@ -61,15 +63,40 @@ const ensureParentDirectory = async (filePath) => {
61
63
  });
62
64
  }
63
65
  };
64
- const deepMerge = (fullTarget, filteredSource, filteredTarget) => {
66
+ const deepMerge = (fullTarget, filteredSource, filteredTarget, currentPath = [], skipPaths = []) => {
65
67
  if (filteredSource === null || filteredSource === undefined)
66
68
  return fullTarget;
67
69
  if (fullTarget === null || fullTarget === undefined)
68
70
  return filteredSource;
69
71
  if (typeof fullTarget !== typeof filteredSource)
70
72
  return filteredSource;
71
- if (Array.isArray(filteredSource))
72
- return filteredSource;
73
+ if (Array.isArray(filteredSource)) {
74
+ const fullTargetArray = Array.isArray(fullTarget) ? fullTarget : [];
75
+ const filteredTargetArray = Array.isArray(filteredTarget) ? filteredTarget : [];
76
+ const applicableFilters = (0, arrayMerger_1.getApplicableArrayFilters)(currentPath, skipPaths);
77
+ if (applicableFilters.length === 0)
78
+ return filteredSource;
79
+ const hasNestedFilters = applicableFilters.some((f) => f.remainingPath.length > 0);
80
+ const result = [];
81
+ for (const sourceItem of filteredSource) {
82
+ if (hasNestedFilters && sourceItem && typeof sourceItem === 'object') {
83
+ const { matches, matchedFilter } = (0, arrayMerger_1.itemMatchesAnyFilter)(sourceItem, applicableFilters);
84
+ if (matches && matchedFilter && matchedFilter.remainingPath.length > 0) {
85
+ const matchingTargetItem = (0, arrayMerger_1.findMatchingTargetItem)(sourceItem, fullTargetArray, applicableFilters);
86
+ const matchingFilteredTargetItem = (0, arrayMerger_1.findMatchingTargetItem)(sourceItem, filteredTargetArray, applicableFilters);
87
+ if (matchingTargetItem) {
88
+ result.push(deepMerge(matchingTargetItem, sourceItem, matchingFilteredTargetItem, currentPath, skipPaths));
89
+ continue;
90
+ }
91
+ }
92
+ }
93
+ result.push(sourceItem);
94
+ }
95
+ for (const item of fullTargetArray)
96
+ if ((0, arrayMerger_1.shouldPreserveItem)(item, applicableFilters, result))
97
+ result.push(item);
98
+ return result;
99
+ }
73
100
  if (typeof filteredSource === 'object' && typeof fullTarget === 'object') {
74
101
  const sourceObject = filteredSource;
75
102
  const fullTargetObject = fullTarget;
@@ -80,12 +107,12 @@ const deepMerge = (fullTarget, filteredSource, filteredTarget) => {
80
107
  result[key] = value;
81
108
  for (const [key, value] of Object.entries(sourceObject))
82
109
  if (key in fullTargetObject)
83
- result[key] = deepMerge(fullTargetObject[key], value, filteredTargetObject[key]);
110
+ result[key] = deepMerge(fullTargetObject[key], value, filteredTargetObject[key], [...currentPath, key], skipPaths);
84
111
  return result;
85
112
  }
86
113
  return filteredSource;
87
114
  };
88
- const mergeYamlContent = (destinationContent, processedSourceContent, filteredDestinationContent, filePath) => {
115
+ const mergeYamlContent = (destinationContent, processedSourceContent, filteredDestinationContent, filePath, skipPaths = []) => {
89
116
  let destinationParsed;
90
117
  try {
91
118
  destinationParsed = yaml_1.default.parse(destinationContent);
@@ -104,7 +131,7 @@ const mergeYamlContent = (destinationContent, processedSourceContent, filteredDe
104
131
  }
105
132
  let merged;
106
133
  try {
107
- merged = deepMerge(destinationParsed, processedSourceContent, filteredDestinationContent);
134
+ merged = deepMerge(destinationParsed, processedSourceContent, filteredDestinationContent, [], skipPaths);
108
135
  }
109
136
  catch (error) {
110
137
  throw new FileUpdaterError('Failed to merge YAML content', {
@@ -136,6 +163,9 @@ const addFile = async (options) => {
136
163
  try {
137
164
  const parsed = yaml_1.default.parse(content);
138
165
  const transformed = (0, transformer_1.applyTransforms)(parsed, relativePath, config.transforms);
166
+ const fixedValueRules = (0, fixedValues_1.getFixedValuesForFile)(relativePath, config.fixedValues);
167
+ if (fixedValueRules.length > 0)
168
+ (0, fixedValues_1.applyFixedValues)(transformed, fixedValueRules);
139
169
  contentToWrite = yaml_1.default.stringify(transformed);
140
170
  const effectiveOutputFormat = skipFormat ? undefined : config.outputFormat;
141
171
  contentToWrite = (0, yamlFormatter_1.formatYaml)(contentToWrite, relativePath, effectiveOutputFormat);
@@ -168,9 +198,15 @@ const updateFile = async (options) => {
168
198
  return;
169
199
  }
170
200
  let contentToWrite = (0, fileType_1.isYamlFile)(changedFile.path)
171
- ? mergeYamlContent(changedFile.destinationContent, changedFile.rawParsedSource, changedFile.rawParsedDest, changedFile.path)
201
+ ? mergeYamlContent(changedFile.destinationContent, changedFile.rawParsedSource, changedFile.rawParsedDest, changedFile.path, changedFile.skipPaths)
172
202
  : changedFile.sourceContent;
173
203
  if ((0, fileType_1.isYamlFile)(changedFile.path)) {
204
+ const fixedValueRules = (0, fixedValues_1.getFixedValuesForFile)(changedFile.path, config.fixedValues);
205
+ if (fixedValueRules.length > 0) {
206
+ const parsed = yaml_1.default.parse(contentToWrite);
207
+ (0, fixedValues_1.applyFixedValues)(parsed, fixedValueRules);
208
+ contentToWrite = yaml_1.default.stringify(parsed);
209
+ }
174
210
  const effectiveOutputFormat = skipFormat ? undefined : config.outputFormat;
175
211
  contentToWrite = (0, yamlFormatter_1.formatYaml)(contentToWrite, changedFile.path, effectiveOutputFormat);
176
212
  }
@@ -9,10 +9,8 @@ const promises_1 = require("node:fs/promises");
9
9
  const node_os_1 = require("node:os");
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
11
  const diff2html_1 = require("diff2html");
12
- const yaml_1 = __importDefault(require("yaml"));
13
12
  const browserLauncher_1 = require("./reporters/browserLauncher");
14
13
  const htmlTemplate_1 = require("./reporters/htmlTemplate");
15
- const arrayDiffProcessor_1 = require("./utils/arrayDiffProcessor");
16
14
  const diffGenerator_1 = require("./utils/diffGenerator");
17
15
  const errors_1 = require("./utils/errors");
18
16
  const fileType_1 = require("./utils/fileType");
@@ -33,103 +31,29 @@ const generateTemporaryFilePath = () => {
33
31
  const filename = `helm-env-delta-${timestamp}-${randomName}.html`;
34
32
  return node_path_1.default.join((0, node_os_1.tmpdir)(), filename);
35
33
  };
36
- const escapeHtml = (text) => text
37
- .replaceAll('&', '&amp;')
38
- .replaceAll('<', '&lt;')
39
- .replaceAll('>', '&gt;')
40
- .replaceAll('"', '&quot;')
41
- .replaceAll("'", '&#039;');
42
34
  const DIFF2HTML_OPTIONS = {
43
35
  drawFileList: false,
44
36
  matching: 'lines',
45
37
  outputFormat: 'side-by-side'
46
38
  };
47
39
  const generateDiffHtml = (unifiedDiff) => (0, diff2html_1.html)(unifiedDiff, DIFF2HTML_OPTIONS);
48
- const generateArrayDiffHtml = (change) => {
49
- let html = '<div class="array-diff">';
50
- if (change.removed.length > 0) {
51
- html += '<div class="removed-items">';
52
- html += `<h4>Removed (${change.removed.length})</h4>`;
53
- html += '<ul>';
54
- for (const item of change.removed) {
55
- const yaml = yaml_1.default.stringify(item, { indent: 2 });
56
- html += `<li class="removed"><pre>${escapeHtml(yaml)}</pre></li>`;
57
- }
58
- html += '</ul></div>';
59
- }
60
- if (change.added.length > 0) {
61
- html += '<div class="added-items">';
62
- html += `<h4>Added (${change.added.length})</h4>`;
63
- html += '<ul>';
64
- for (const item of change.added) {
65
- const yaml = yaml_1.default.stringify(item, { indent: 2 });
66
- html += `<li class="added"><pre>${escapeHtml(yaml)}</pre></li>`;
67
- }
68
- html += '</ul></div>';
69
- }
70
- if (change.unchanged.length > 0)
71
- html += `<div class="unchanged-count">Unchanged: ${change.unchanged.length} items</div>`;
72
- html += '</div>';
73
- return html;
74
- };
75
40
  const generateFileSummary = (file) => {
76
41
  if (!file.originalPath)
77
42
  return file.path;
78
43
  return `<span class="filename-transform">${file.originalPath} → ${file.path}</span>`;
79
44
  };
80
- const generateChangedFileSection = (file) => {
45
+ const generateChangedFileSection = (file, fileId) => {
81
46
  const isYaml = (0, fileType_1.isYamlFile)(file.path);
82
47
  const summary = generateFileSummary(file);
83
- if (!isYaml) {
84
- const destinationContent = (0, serialization_1.serializeForDiff)(file.processedDestContent, false);
85
- const sourceContent = (0, serialization_1.serializeForDiff)(file.processedSourceContent, false);
86
- const unifiedDiff = (0, diffGenerator_1.generateUnifiedDiff)(file.path, destinationContent, sourceContent);
87
- const diffHtml = generateDiffHtml(unifiedDiff);
88
- return `
89
- <details class="file-section" open>
90
- <summary>${summary}</summary>
91
- <div class="diff-container">
92
- ${diffHtml}
93
- </div>
94
- </details>
95
- `;
96
- }
97
- const arrayInfo = (0, arrayDiffProcessor_1.detectArrayChanges)(file);
98
- if (!arrayInfo.hasArrays) {
99
- const destinationContent = (0, serialization_1.serializeForDiff)(file.processedDestContent, true);
100
- const sourceContent = (0, serialization_1.serializeForDiff)(file.processedSourceContent, true);
101
- const unifiedDiff = (0, diffGenerator_1.generateUnifiedDiff)(file.path, destinationContent, sourceContent);
102
- const diffHtml = generateDiffHtml(unifiedDiff);
103
- return `
104
- <details class="file-section" open>
105
- <summary>${summary}</summary>
106
- <div class="diff-container">
107
- ${diffHtml}
108
- </div>
109
- </details>
110
- `;
111
- }
112
- const destinationContent = (0, serialization_1.serializeForDiff)(file.processedDestContent, true);
113
- const sourceContent = (0, serialization_1.serializeForDiff)(file.processedSourceContent, true);
48
+ const destinationContent = (0, serialization_1.serializeForDiff)(file.processedDestContent, isYaml);
49
+ const sourceContent = (0, serialization_1.serializeForDiff)(file.processedSourceContent, isYaml);
114
50
  const unifiedDiff = (0, diffGenerator_1.generateUnifiedDiff)(file.path, destinationContent, sourceContent);
115
51
  const diffHtml = generateDiffHtml(unifiedDiff);
116
- let arrayDiffsHtml = '';
117
- if (arrayInfo.hasChanges) {
118
- arrayDiffsHtml = '<details class="array-details"><summary>Array-specific details:</summary>';
119
- for (const change of arrayInfo.changes) {
120
- const pathString = change.path.join('.');
121
- arrayDiffsHtml += `<div class="array-section"><h4>${pathString}:</h4>`;
122
- arrayDiffsHtml += generateArrayDiffHtml(change);
123
- arrayDiffsHtml += '</div>';
124
- }
125
- arrayDiffsHtml += '</details>';
126
- }
127
52
  return `
128
- <details class="file-section" open>
53
+ <details class="file-section" id="${fileId}" data-file-id="${fileId}" open>
129
54
  <summary>${summary}</summary>
130
55
  <div class="diff-container">
131
56
  ${diffHtml}
132
- ${arrayDiffsHtml}
133
57
  </div>
134
58
  </details>
135
59
  `;
@@ -163,8 +87,11 @@ const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun, lo
163
87
  };
164
88
  const formattedSet = new Set(formattedFiles);
165
89
  const trulyUnchangedFiles = diffResult.unchangedFiles.filter((file) => !formattedSet.has(file));
166
- const changedSections = diffResult.changedFiles.map((file) => generateChangedFileSection(file));
167
- const htmlContent = (0, htmlTemplate_1.generateHtmlTemplate)(diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections);
90
+ const changedFileIds = new Map();
91
+ for (const [index, file] of diffResult.changedFiles.entries())
92
+ changedFileIds.set(file.path, `file-${index}`);
93
+ const changedSections = diffResult.changedFiles.map((file, index) => generateChangedFileSection(file, `file-${index}`));
94
+ const htmlContent = (0, htmlTemplate_1.generateHtmlTemplate)(diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds);
168
95
  await writeHtmlFile(htmlContent, reportPath);
169
96
  logger?.log(`✓ HTML report generated: ${reportPath}, opening in browser...`);
170
97
  try {
@@ -5,7 +5,7 @@ export interface PatternUsageResult {
5
5
  hasWarnings: boolean;
6
6
  }
7
7
  export interface PatternUsageWarning {
8
- type: 'unused-exclude' | 'unused-skipPath' | 'unused-skipPath-jsonpath' | 'unused-stopRule-glob' | 'unused-stopRule-path';
8
+ type: 'unused-exclude' | 'unused-skipPath' | 'unused-skipPath-jsonpath' | 'unused-stopRule-glob' | 'unused-stopRule-path' | 'unused-fixedValues' | 'unused-fixedValues-jsonpath';
9
9
  pattern: string;
10
10
  message: string;
11
11
  context?: string;
@@ -11,7 +11,8 @@ const validatePatternUsage = (config, sourceFiles, destinationFiles) => {
11
11
  const warnings = [
12
12
  ...validateExcludePatterns(config, sourceFiles, destinationFiles),
13
13
  ...validateSkipPathPatterns(config, sourceFiles, destinationFiles),
14
- ...validateStopRulePatterns(config, sourceFiles, destinationFiles)
14
+ ...validateStopRulePatterns(config, sourceFiles, destinationFiles),
15
+ ...validateFixedValuesPatterns(config, sourceFiles, destinationFiles)
15
16
  ];
16
17
  return {
17
18
  warnings,
@@ -99,6 +100,38 @@ const validateStopRulePatterns = (config, sourceFiles, destinationFiles) => {
99
100
  return warnings;
100
101
  };
101
102
  const hasPathField = (rule) => 'path' in rule && typeof rule.path === 'string';
103
+ const validateFixedValuesPatterns = (config, sourceFiles, destinationFiles) => {
104
+ const warnings = [];
105
+ if (!config.fixedValues)
106
+ return warnings;
107
+ const allFiles = new Set([...sourceFiles.keys(), ...destinationFiles.keys()]);
108
+ for (const [pattern, rules] of Object.entries(config.fixedValues)) {
109
+ const matchedFiles = [...allFiles].filter((filePath) => patternMatcher_1.globalMatcher.match(filePath, pattern));
110
+ if (matchedFiles.length === 0) {
111
+ warnings.push({
112
+ type: 'unused-fixedValues',
113
+ pattern,
114
+ message: `fixedValues pattern '${pattern}' matches no files`,
115
+ context: `${rules.length} rule(s) defined`
116
+ });
117
+ continue;
118
+ }
119
+ const yamlFiles = matchedFiles.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
120
+ if (yamlFiles.length === 0)
121
+ continue;
122
+ for (const rule of rules) {
123
+ const pathExistsInAny = validateJsonPathInFiles(rule.path, yamlFiles, sourceFiles, destinationFiles);
124
+ if (!pathExistsInAny)
125
+ warnings.push({
126
+ type: 'unused-fixedValues-jsonpath',
127
+ pattern,
128
+ message: `fixedValues JSONPath '${rule.path}' not found in any matched files`,
129
+ context: `Pattern: ${pattern}, matches ${yamlFiles.length} file(s)`
130
+ });
131
+ }
132
+ }
133
+ return warnings;
134
+ };
102
135
  const pathCouldMatch = (object, pathParts) => {
103
136
  let current = object;
104
137
  for (const part of pathParts) {
@@ -1,2 +1,2 @@
1
- export declare const HTML_STYLES = "\n /* Custom styles */\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n margin: 0;\n padding: 20px;\n background: #f6f8fa;\n }\n\n header {\n background: white;\n padding: 20px;\n border-radius: 6px;\n margin-bottom: 20px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n h1 {\n margin: 0 0 10px 0;\n color: #24292e;\n }\n\n .metadata {\n display: flex;\n gap: 20px;\n margin: 10px 0;\n color: #586069;\n font-size: 14px;\n }\n\n .dry-run-badge {\n display: inline-block;\n padding: 4px 8px;\n background: #cfe2ff;\n color: #084298;\n border-radius: 4px;\n font-weight: bold;\n font-size: 12px;\n }\n\n .summary {\n display: flex;\n gap: 12px;\n margin: 15px 0;\n }\n\n .stat {\n padding: 8px 16px;\n border-radius: 6px;\n font-weight: 600;\n font-size: 14px;\n }\n\n .stat.added { background: #d4edda; color: #155724; }\n .stat.changed { background: #fff3cd; color: #856404; }\n .stat.deleted { background: #f8d7da; color: #721c24; }\n .stat.formatted { background: #d1ecf1; color: #0c5460; }\n .stat.unchanged { background: #e2e3e5; color: #383d41; }\n\n .tabs {\n display: flex;\n background: white;\n border-radius: 6px 6px 0 0;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab {\n padding: 12px 24px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 14px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .tab:hover {\n color: #24292e;\n }\n\n .tab.active {\n border-bottom: 2px solid #0969da;\n color: #0969da;\n font-weight: 600;\n }\n\n main {\n background: white;\n padding: 20px;\n border-radius: 0 0 6px 6px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab-content {\n display: none;\n }\n\n .tab-content.active {\n display: block;\n }\n\n .file-section {\n margin: 12px 0;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n }\n\n .file-section summary {\n padding: 12px 16px;\n background: #f6f8fa;\n cursor: pointer;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #24292e;\n }\n\n .file-section summary:hover {\n background: #eaeef2;\n }\n\n .filename-transform {\n color: #0969da;\n }\n\n .diff-container {\n padding: 0;\n }\n\n /* Hide diff2html file header with rename badge */\n .d2h-file-header {\n display: none;\n }\n\n .file-list {\n margin: 20px 0;\n }\n\n .file-list ul {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n }\n\n .file-list li {\n padding: 8px 16px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #586069;\n border-bottom: 1px solid #f6f8fa;\n }\n\n .file-list li:hover {\n background: #f6f8fa;\n }\n\n .array-details {\n margin: 20px 0;\n background: #f6f8fa;\n border-radius: 6px;\n border-left: 3px solid #0969da;\n }\n\n .array-details summary {\n padding: 12px 15px;\n cursor: pointer;\n color: #0969da;\n font-size: 13px;\n font-weight: 600;\n }\n\n .array-details summary:hover {\n background: #eaeef2;\n }\n\n .array-details > .array-section {\n margin: 0 15px 15px 15px;\n }\n\n .array-section {\n margin: 15px 0;\n padding: 10px;\n background: white;\n border-radius: 4px;\n border: 1px solid #d0d7de;\n }\n\n .array-section h4 {\n margin: 0 0 10px 0;\n color: #24292e;\n font-size: 14px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n }\n\n .array-unchanged {\n padding: 8px;\n color: #586069;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n }\n\n .added-items, .removed-items {\n margin: 10px 0;\n }\n\n .added-items h4 {\n color: #1a7f37;\n margin: 0 0 8px 0;\n font-size: 13px;\n }\n\n .removed-items h4 {\n color: #cf222e;\n margin: 0 0 8px 0;\n font-size: 13px;\n }\n\n .added-items ul, .removed-items ul {\n list-style: none;\n padding: 0;\n margin: 0;\n }\n\n .added-items li {\n padding: 6px 10px;\n background: #dafbe1;\n border-left: 3px solid #1a7f37;\n margin: 4px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n }\n\n .removed-items li {\n padding: 6px 10px;\n background: #ffebe9;\n border-left: 3px solid #cf222e;\n margin: 4px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n }\n\n .added-items pre, .removed-items pre {\n margin: 0;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n\n .unchanged-count {\n padding: 8px 10px;\n color: #586069;\n font-size: 12px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n background: #f6f8fa;\n border-radius: 4px;\n margin: 10px 0;\n }\n";
2
- export declare const TAB_SCRIPT = "\n // Tab switching\n document.querySelectorAll('.tab').forEach(tab => {\n tab.addEventListener('click', () => {\n const tabName = tab.getAttribute('data-tab');\n\n // Update tabs\n document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\n tab.classList.add('active');\n\n // Update content\n document.querySelectorAll('.tab-content').forEach(content => {\n content.classList.remove('active');\n });\n document.getElementById(tabName).classList.add('active');\n });\n });\n";
1
+ export declare const HTML_STYLES = "\n /* Custom styles */\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n margin: 0;\n padding: 20px;\n background: #f6f8fa;\n }\n\n header {\n background: white;\n padding: 20px;\n border-radius: 6px;\n margin-bottom: 20px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n h1 {\n margin: 0 0 10px 0;\n color: #24292e;\n }\n\n .metadata {\n display: flex;\n gap: 20px;\n margin: 10px 0;\n color: #586069;\n font-size: 14px;\n }\n\n .dry-run-badge {\n display: inline-block;\n padding: 4px 8px;\n background: #cfe2ff;\n color: #084298;\n border-radius: 4px;\n font-weight: bold;\n font-size: 12px;\n }\n\n .summary {\n display: flex;\n gap: 12px;\n margin: 15px 0;\n }\n\n .stat {\n padding: 8px 16px;\n border-radius: 6px;\n font-weight: 600;\n font-size: 14px;\n }\n\n .stat.added { background: #d4edda; color: #155724; }\n .stat.changed { background: #fff3cd; color: #856404; }\n .stat.deleted { background: #f8d7da; color: #721c24; }\n .stat.formatted { background: #d1ecf1; color: #0c5460; }\n .stat.unchanged { background: #e2e3e5; color: #383d41; }\n\n .tabs {\n display: flex;\n background: white;\n border-radius: 6px 6px 0 0;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab {\n padding: 12px 24px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 14px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .tab:hover {\n color: #24292e;\n }\n\n .tab.active {\n border-bottom: 2px solid #0969da;\n color: #0969da;\n font-weight: 600;\n }\n\n main {\n background: white;\n padding: 20px;\n border-radius: 0 0 6px 6px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab-content {\n display: none;\n }\n\n .tab-content.active {\n display: block;\n }\n\n .file-section {\n margin: 12px 0;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n }\n\n .file-section summary {\n padding: 12px 16px;\n background: #f6f8fa;\n cursor: pointer;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #24292e;\n }\n\n .file-section summary:hover {\n background: #eaeef2;\n }\n\n .filename-transform {\n color: #0969da;\n }\n\n .diff-container {\n padding: 0;\n }\n\n /* Hide diff2html file header with rename badge */\n .d2h-file-header {\n display: none;\n }\n\n .file-list {\n margin: 20px 0;\n }\n\n .file-list ul {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n }\n\n .file-list li {\n padding: 8px 16px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #586069;\n border-bottom: 1px solid #f6f8fa;\n }\n\n .file-list li:hover {\n background: #f6f8fa;\n }\n\n /* Treeview styles */\n .tree-root {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n }\n\n .tree-root ul {\n list-style: none;\n padding-left: 20px;\n margin: 0;\n }\n\n .tree-folder,\n .tree-file {\n padding: 4px 8px;\n border-radius: 4px;\n cursor: default;\n }\n\n .tree-folder:hover,\n .tree-file:hover {\n background: #f6f8fa;\n }\n\n .tree-toggle {\n display: inline-block;\n width: 16px;\n cursor: pointer;\n color: #586069;\n font-size: 10px;\n user-select: none;\n }\n\n .tree-folder.collapsed > .tree-toggle {\n transform: rotate(-90deg);\n }\n\n .tree-folder.collapsed > .tree-children {\n display: none;\n }\n\n .tree-folder-name {\n color: #0969da;\n font-weight: 500;\n }\n\n .tree-file-name {\n color: #586069;\n padding-left: 16px;\n }\n\n /* Sidebar styles */\n .sidebar-container {\n display: flex;\n gap: 0;\n }\n\n .sidebar {\n width: 280px;\n min-width: 280px;\n border-right: 1px solid #d0d7de;\n background: #f6f8fa;\n transition: width 0.2s, min-width 0.2s, padding 0.2s, opacity 0.2s;\n }\n\n .sidebar.collapsed {\n width: 0;\n min-width: 0;\n padding: 0;\n overflow: hidden;\n border-right: none;\n }\n\n .sidebar-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n border-bottom: 1px solid #d0d7de;\n background: #fff;\n font-weight: 600;\n font-size: 14px;\n color: #24292e;\n position: sticky;\n top: 0;\n z-index: 1;\n }\n\n .sidebar-toggle {\n background: none;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n cursor: pointer;\n padding: 4px 8px;\n color: #586069;\n font-size: 12px;\n }\n\n .sidebar-toggle:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n .sidebar-content {\n padding: 8px;\n }\n\n .sidebar-tree .tree-file-link {\n color: #586069;\n text-decoration: none;\n padding-left: 16px;\n display: block;\n }\n\n .sidebar-tree .tree-file-link:hover {\n color: #0969da;\n }\n\n .sidebar-tree .tree-file.active .tree-file-link {\n color: #0969da;\n font-weight: 600;\n }\n\n .changed-content {\n flex: 1;\n min-width: 0;\n padding-left: 20px;\n }\n\n .sidebar-expand-btn {\n display: none;\n position: fixed;\n left: 0;\n top: 50%;\n transform: translateY(-50%);\n background: #f6f8fa;\n border: 1px solid #d0d7de;\n border-left: none;\n border-radius: 0 4px 4px 0;\n padding: 8px 4px;\n cursor: pointer;\n color: #586069;\n z-index: 100;\n }\n\n .sidebar-expand-btn:hover {\n background: #eaeef2;\n color: #24292e;\n }\n\n .sidebar.collapsed ~ .sidebar-expand-btn {\n display: block;\n }\n";
2
+ export declare const TAB_SCRIPT: string;