helm-env-delta 1.14.0 → 1.14.2
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 +8 -4
- package/dist/config/configFile.d.ts +4 -4
- package/dist/config/configFile.js +26 -1
- package/dist/pipeline/fileDiff.d.ts +2 -1
- package/dist/pipeline/fileDiff.js +29 -4
- package/dist/pipeline/fileUpdater.js +28 -10
- package/dist/pipeline/yamlFormatter.js +12 -1
- package/dist/reporters/htmlReporter.js +12 -8
- package/dist/reporters/htmlStyles.d.ts +1 -1
- package/dist/reporters/htmlStyles.js +66 -2
- package/dist/reporters/htmlTemplate.js +4 -0
- package/dist/utils/regexPatternFileLoader.js +3 -0
- package/dist/utils/regexSafety.d.ts +1 -0
- package/dist/utils/regexSafety.js +9 -0
- package/dist/utils/regexTransform.js +6 -1
- package/dist/utils/serialization.js +14 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ HelmEnvDelta (`hed`) automates environment synchronization for GitOps workflows
|
|
|
56
56
|
|
|
57
57
|
📦 **Config Inheritance** - Reuse base configurations with environment-specific overrides.
|
|
58
58
|
|
|
59
|
-
📊 **Multiple Reports** - Console, HTML (visual, self-contained), and JSON (CI/CD) output formats. HTML reports include collapsible diff stats dashboard, stop rule violations table (dry-run only), synchronized side-by-side scrolling, copy diff buttons, file search,
|
|
59
|
+
📊 **Multiple Reports** - Console, HTML (visual, self-contained), and JSON (CI/CD) output formats. HTML reports include collapsible diff stats dashboard, stop rule violations table (dry-run only), synchronized side-by-side scrolling, copy diff buttons, file search, collapse/expand controls, jump-to-sidebar navigation, and auto-collapse for large file sets. Empty categories are automatically hidden.
|
|
60
60
|
|
|
61
61
|
🔍 **Discovery Tools** - Preview files (`-l`), inspect config (`--show-config`), filter by filename/content (`-f`), filter by change type (`-m`), validate with comprehensive warnings including unused pattern detection.
|
|
62
62
|
|
|
@@ -64,7 +64,9 @@ HelmEnvDelta (`hed`) automates environment synchronization for GitOps workflows
|
|
|
64
64
|
|
|
65
65
|
🛡️ **Safety First** - Pre-execution summary, first-run tips, improved error messages with helpful examples.
|
|
66
66
|
|
|
67
|
-
⚡ **High Performance** -
|
|
67
|
+
⚡ **High Performance** - Intelligent caching and parallel processing. Formatting rules, compiled regexes, and array normalization are all cached for fast repeated runs.
|
|
68
|
+
|
|
69
|
+
🔐 **Security Hardened** - Regex inputs (stop rules, transforms, pattern files) are validated against ReDoS (catastrophic backtracking). Fixed values are validated against prototype pollution attacks.
|
|
68
70
|
|
|
69
71
|
🔔 **Auto Updates** - Notifies when newer versions are available (skips in CI/CD).
|
|
70
72
|
|
|
@@ -119,7 +121,7 @@ hed -c config.yaml
|
|
|
119
121
|
hed -c config.yaml -H
|
|
120
122
|
```
|
|
121
123
|
|
|
122
|
-
Self-contained HTML report — works offline, no CDN required. Includes collapsible diff stats dashboard, stop rule violations table (shown in dry-run mode), synchronized side-by-side scrolling, copy buttons, sidebar search,
|
|
124
|
+
Self-contained HTML report — works offline, no CDN required. Includes collapsible diff stats dashboard, stop rule violations table (shown in dry-run mode), synchronized side-by-side scrolling, copy buttons, sidebar search, collapse/expand controls, and jump-to-sidebar navigation. File blocks auto-collapse when there are more than 10 files. Empty categories are automatically hidden.
|
|
123
125
|
|
|
124
126
|
### 5️⃣ Get Smart Suggestions (Optional)
|
|
125
127
|
|
|
@@ -523,7 +525,7 @@ fixedValues:
|
|
|
523
525
|
|
|
524
526
|
**Supported filter operators:** `=` (equals), `^=` (startsWith), `$=` (endsWith), `*=` (contains) - updates ALL matching items
|
|
525
527
|
|
|
526
|
-
**Value types:** String, number, boolean, null, object, array
|
|
528
|
+
**Value types:** String, number, boolean, null, object, array (objects with `__proto__`, `constructor`, or `prototype` keys are rejected)
|
|
527
529
|
|
|
528
530
|
**Behavior:**
|
|
529
531
|
|
|
@@ -678,6 +680,8 @@ stopRules:
|
|
|
678
680
|
|
|
679
681
|
**Override:** Use `--force` to bypass stop rules when needed.
|
|
680
682
|
|
|
683
|
+
**Regex safety:** All `regex` patterns (inline and from files) are validated against catastrophic backtracking (ReDoS). Patterns with nested quantifiers on groups (e.g., `(a+)+`) are rejected at config load time.
|
|
684
|
+
|
|
681
685
|
**Visibility:** Stop rule violations appear in console output, JSON reports, and HTML reports (dry-run mode only, as a collapsible table in the header area).
|
|
682
686
|
|
|
683
687
|
---
|
|
@@ -82,7 +82,7 @@ declare const keySortRuleSchema: z.ZodObject<{
|
|
|
82
82
|
}, z.core.$strip>;
|
|
83
83
|
declare const fixedValueRuleSchema: z.ZodObject<{
|
|
84
84
|
path: z.ZodString;
|
|
85
|
-
value: z.ZodUnknown
|
|
85
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodArray<z.ZodUnknown>, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
|
|
86
86
|
}, z.core.$strip>;
|
|
87
87
|
declare const transformRuleSchema: z.ZodObject<{
|
|
88
88
|
find: z.ZodString;
|
|
@@ -173,7 +173,7 @@ declare const baseConfigSchema: z.ZodObject<{
|
|
|
173
173
|
}, z.core.$strict>], "type">>>>;
|
|
174
174
|
fixedValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
|
|
175
175
|
path: z.ZodString;
|
|
176
|
-
value: z.ZodUnknown
|
|
176
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodArray<z.ZodUnknown>, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
|
|
177
177
|
}, z.core.$strip>>>>;
|
|
178
178
|
}, z.core.$strip>;
|
|
179
179
|
declare const finalConfigSchema: z.ZodObject<{
|
|
@@ -227,7 +227,7 @@ declare const finalConfigSchema: z.ZodObject<{
|
|
|
227
227
|
}, z.core.$strict>], "type">>>>;
|
|
228
228
|
fixedValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
|
|
229
229
|
path: z.ZodString;
|
|
230
|
-
value: z.ZodUnknown
|
|
230
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodArray<z.ZodUnknown>, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
|
|
231
231
|
}, z.core.$strip>>>>;
|
|
232
232
|
include: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
233
233
|
exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -302,7 +302,7 @@ declare const formatOnlyConfigSchema: z.ZodObject<{
|
|
|
302
302
|
}, z.core.$strict>], "type">>>>;
|
|
303
303
|
fixedValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
|
|
304
304
|
path: z.ZodString;
|
|
305
|
-
value: z.ZodUnknown
|
|
305
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull, z.ZodArray<z.ZodUnknown>, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
|
|
306
306
|
}, z.core.$strip>>>>;
|
|
307
307
|
include: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
308
308
|
exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.isConfigValidationError = exports.ConfigValidationError = exports.parseConfig = exports.parseFormatOnlyConfig = exports.parseFinalConfig = exports.parseBaseConfig = void 0;
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
8
|
const zod_1 = require("zod");
|
|
9
|
+
const regexSafety_1 = require("../utils/regexSafety");
|
|
9
10
|
const ZodError_1 = require("./ZodError");
|
|
10
11
|
const semverMajorUpgradeRuleSchema = zod_1.z.object({
|
|
11
12
|
type: zod_1.z.literal('semverMajorUpgrade'),
|
|
@@ -47,6 +48,10 @@ const regexRuleSchema = zod_1.z
|
|
|
47
48
|
}, {
|
|
48
49
|
message: 'Invalid regular expression pattern',
|
|
49
50
|
path: ['regex']
|
|
51
|
+
})
|
|
52
|
+
.refine((data) => (0, regexSafety_1.isSafeRegex)(data.regex), {
|
|
53
|
+
message: 'Potentially unsafe regex pattern (may cause catastrophic backtracking / ReDoS)',
|
|
54
|
+
path: ['regex']
|
|
50
55
|
});
|
|
51
56
|
const regexFileRuleSchema = zod_1.z.object({
|
|
52
57
|
type: zod_1.z.literal('regexFile'),
|
|
@@ -80,9 +85,25 @@ const arraySortRuleSchema = zod_1.z.object({
|
|
|
80
85
|
order: zod_1.z.enum(['asc', 'desc']).default('asc')
|
|
81
86
|
});
|
|
82
87
|
const keySortRuleSchema = zod_1.z.object({ path: zod_1.z.string().min(1) });
|
|
88
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
89
|
+
const hasDangerousKeys = (value) => {
|
|
90
|
+
if (value === null || typeof value !== 'object')
|
|
91
|
+
return false;
|
|
92
|
+
if (Array.isArray(value))
|
|
93
|
+
return value.some((item) => hasDangerousKeys(item));
|
|
94
|
+
for (const key of Object.keys(value))
|
|
95
|
+
if (DANGEROUS_KEYS.has(key) || hasDangerousKeys(value[key]))
|
|
96
|
+
return true;
|
|
97
|
+
return false;
|
|
98
|
+
};
|
|
99
|
+
const safeFixedValue = zod_1.z
|
|
100
|
+
.union([zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean(), zod_1.z.null(), zod_1.z.array(zod_1.z.unknown()), zod_1.z.record(zod_1.z.string(), zod_1.z.unknown())])
|
|
101
|
+
.refine((value) => !hasDangerousKeys(value), {
|
|
102
|
+
message: 'Value must not contain prototype-polluting keys (__proto__, constructor, prototype)'
|
|
103
|
+
});
|
|
83
104
|
const fixedValueRuleSchema = zod_1.z.object({
|
|
84
105
|
path: zod_1.z.string().min(1).describe('JSONPath to the value to set'),
|
|
85
|
-
value:
|
|
106
|
+
value: safeFixedValue.describe('The constant value to set (any type: string, number, boolean, null, object, array)')
|
|
86
107
|
});
|
|
87
108
|
const transformRuleSchema = zod_1.z
|
|
88
109
|
.object({
|
|
@@ -100,6 +121,10 @@ const transformRuleSchema = zod_1.z
|
|
|
100
121
|
}, {
|
|
101
122
|
message: 'Invalid regular expression pattern',
|
|
102
123
|
path: ['find']
|
|
124
|
+
})
|
|
125
|
+
.refine((data) => (0, regexSafety_1.isSafeRegex)(data.find), {
|
|
126
|
+
message: 'Potentially unsafe regex pattern (may cause catastrophic backtracking / ReDoS)',
|
|
127
|
+
path: ['find']
|
|
103
128
|
});
|
|
104
129
|
const transformRulesSchema = zod_1.z
|
|
105
130
|
.object({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Config, FixedValueConfig, TransformConfig } from '../config';
|
|
1
|
+
import { Config, FixedValueConfig, FixedValueRule, TransformConfig } from '../config';
|
|
2
2
|
import { FileMap } from './fileLoader';
|
|
3
3
|
export interface FileDiffResult {
|
|
4
4
|
addedFiles: AddedFile[];
|
|
@@ -16,6 +16,7 @@ export interface ChangedFile {
|
|
|
16
16
|
rawParsedSource: unknown;
|
|
17
17
|
rawParsedDest: unknown;
|
|
18
18
|
skipPaths: string[];
|
|
19
|
+
fixedValueRules: FixedValueRule[];
|
|
19
20
|
normalizedSource?: unknown;
|
|
20
21
|
normalizedDest?: unknown;
|
|
21
22
|
parsedSource?: unknown;
|
|
@@ -117,10 +117,33 @@ const applySkipPaths = (data, skipPaths) => {
|
|
|
117
117
|
return data;
|
|
118
118
|
if (skipPaths.length === 0)
|
|
119
119
|
return data;
|
|
120
|
-
const
|
|
120
|
+
const affectedKeys = new Set();
|
|
121
|
+
let needsFullClone = false;
|
|
122
|
+
for (const skipPath of skipPaths) {
|
|
123
|
+
const parts = (0, jsonPath_1.parseJsonPath)(skipPath);
|
|
124
|
+
if (parts.length === 0)
|
|
125
|
+
continue;
|
|
126
|
+
const firstPart = parts[0];
|
|
127
|
+
if (firstPart === '*' || ((0, jsonPath_1.isFilterSegment)(firstPart) && Array.isArray(data))) {
|
|
128
|
+
needsFullClone = true;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
affectedKeys.add(firstPart);
|
|
132
|
+
}
|
|
133
|
+
if (needsFullClone) {
|
|
134
|
+
const cloned = structuredClone(data);
|
|
135
|
+
for (const path of skipPaths)
|
|
136
|
+
deleteJsonPath(cloned, path);
|
|
137
|
+
return cloned;
|
|
138
|
+
}
|
|
139
|
+
const object = data;
|
|
140
|
+
const result = { ...object };
|
|
141
|
+
for (const key of affectedKeys)
|
|
142
|
+
if (key in result)
|
|
143
|
+
result[key] = structuredClone(result[key]);
|
|
121
144
|
for (const path of skipPaths)
|
|
122
|
-
deleteJsonPath(
|
|
123
|
-
return
|
|
145
|
+
deleteJsonPath(result, path);
|
|
146
|
+
return result;
|
|
124
147
|
};
|
|
125
148
|
const getSkipPathsForFile = (filePath, skipPath) => {
|
|
126
149
|
if (!skipPath)
|
|
@@ -193,6 +216,7 @@ const processYamlFile = (options) => {
|
|
|
193
216
|
rawParsedSource: sourceFiltered,
|
|
194
217
|
rawParsedDest: destinationFiltered,
|
|
195
218
|
skipPaths: pathsToSkip,
|
|
219
|
+
fixedValueRules,
|
|
196
220
|
normalizedSource,
|
|
197
221
|
normalizedDest: normalizedDestination,
|
|
198
222
|
parsedSource: sourceParsed,
|
|
@@ -237,7 +261,8 @@ const processChangedFiles = (sourceFiles, destinationFiles, skipPath, transforms
|
|
|
237
261
|
processedDestContent: destinationContent,
|
|
238
262
|
rawParsedSource: sourceContent,
|
|
239
263
|
rawParsedDest: destinationContent,
|
|
240
|
-
skipPaths: []
|
|
264
|
+
skipPaths: [],
|
|
265
|
+
fixedValueRules: []
|
|
241
266
|
});
|
|
242
267
|
}
|
|
243
268
|
return { changedFiles, unchangedFiles };
|
|
@@ -93,9 +93,21 @@ const deepMerge = (fullTarget, filteredSource, filteredTarget, currentPath = [],
|
|
|
93
93
|
}
|
|
94
94
|
result.push(sourceItem);
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
const resultKeySet = new Set(result
|
|
97
|
+
.filter((item) => !!item && typeof item === 'object')
|
|
98
|
+
.map((item) => JSON.stringify(applicableFilters.map((f) => item[f.filter.property]))));
|
|
99
|
+
for (const item of fullTargetArray) {
|
|
100
|
+
if (!item || typeof item !== 'object')
|
|
101
|
+
continue;
|
|
102
|
+
const { matches } = (0, arrayMerger_1.itemMatchesAnyFilter)(item, applicableFilters);
|
|
103
|
+
if (!matches)
|
|
104
|
+
continue;
|
|
105
|
+
const key = JSON.stringify(applicableFilters.map((f) => item[f.filter.property]));
|
|
106
|
+
if (!resultKeySet.has(key)) {
|
|
98
107
|
result.push(item);
|
|
108
|
+
resultKeySet.add(key);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
99
111
|
return result;
|
|
100
112
|
}
|
|
101
113
|
if (typeof filteredSource === 'object' && typeof fullTarget === 'object') {
|
|
@@ -142,7 +154,7 @@ const mergeYamlContent = (destinationContent, processedSourceContent, filteredDe
|
|
|
142
154
|
});
|
|
143
155
|
}
|
|
144
156
|
try {
|
|
145
|
-
return yaml_1.default.stringify(merged);
|
|
157
|
+
return { content: yaml_1.default.stringify(merged), merged };
|
|
146
158
|
}
|
|
147
159
|
catch (error) {
|
|
148
160
|
throw new FileUpdaterError('Failed to serialize merged YAML', {
|
|
@@ -198,15 +210,21 @@ const updateFile = async (options) => {
|
|
|
198
210
|
logger.fileOp('update', changedFile.path, true);
|
|
199
211
|
return;
|
|
200
212
|
}
|
|
201
|
-
let contentToWrite
|
|
202
|
-
|
|
203
|
-
|
|
213
|
+
let contentToWrite;
|
|
214
|
+
let mergedObject;
|
|
215
|
+
if ((0, fileType_1.isYamlFile)(changedFile.path)) {
|
|
216
|
+
const mergeResult = mergeYamlContent(changedFile.destinationContent, changedFile.rawParsedSource, changedFile.rawParsedDest, changedFile.path, changedFile.skipPaths);
|
|
217
|
+
contentToWrite = mergeResult.content;
|
|
218
|
+
mergedObject = mergeResult.merged;
|
|
219
|
+
}
|
|
220
|
+
else
|
|
221
|
+
contentToWrite = changedFile.sourceContent;
|
|
204
222
|
if ((0, fileType_1.isYamlFile)(changedFile.path)) {
|
|
205
|
-
const fixedValueRules = (0, fixedValues_1.getFixedValuesForFile)(changedFile.path, config.fixedValues);
|
|
223
|
+
const fixedValueRules = changedFile.fixedValueRules ?? (0, fixedValues_1.getFixedValuesForFile)(changedFile.path, config.fixedValues);
|
|
206
224
|
if (fixedValueRules.length > 0) {
|
|
207
|
-
const
|
|
208
|
-
(0, fixedValues_1.applyFixedValues)(
|
|
209
|
-
contentToWrite = yaml_1.default.stringify(
|
|
225
|
+
const target = mergedObject ?? yaml_1.default.parse(contentToWrite);
|
|
226
|
+
(0, fixedValues_1.applyFixedValues)(target, fixedValueRules);
|
|
227
|
+
contentToWrite = yaml_1.default.stringify(target);
|
|
210
228
|
}
|
|
211
229
|
const effectiveOutputFormat = skipFormat ? undefined : config.outputFormat;
|
|
212
230
|
contentToWrite = (0, yamlFormatter_1.formatYaml)(contentToWrite, changedFile.path, effectiveOutputFormat);
|
|
@@ -18,7 +18,16 @@ exports.YamlFormatterError = (0, errors_1.createErrorClass)('YAML Formatter Erro
|
|
|
18
18
|
PATTERN_MATCH_ERROR: 'File pattern matching failed'
|
|
19
19
|
});
|
|
20
20
|
exports.isYamlFormatterError = (0, errors_1.createErrorTypeGuard)(exports.YamlFormatterError);
|
|
21
|
+
const formattingRulesCache = new WeakMap();
|
|
21
22
|
const getFormattingRules = (filePath, outputFormat) => {
|
|
23
|
+
let fileMap = formattingRulesCache.get(outputFormat);
|
|
24
|
+
if (!fileMap) {
|
|
25
|
+
fileMap = new Map();
|
|
26
|
+
formattingRulesCache.set(outputFormat, fileMap);
|
|
27
|
+
}
|
|
28
|
+
const cached = fileMap.get(filePath);
|
|
29
|
+
if (cached)
|
|
30
|
+
return cached;
|
|
22
31
|
const keyOrders = [];
|
|
23
32
|
const keySort = [];
|
|
24
33
|
const arraySort = [];
|
|
@@ -52,7 +61,9 @@ const getFormattingRules = (filePath, outputFormat) => {
|
|
|
52
61
|
if (quoteValue)
|
|
53
62
|
quoteValues.push(quoteValue);
|
|
54
63
|
}
|
|
55
|
-
|
|
64
|
+
const rules = { keyOrders, keySort, arraySort, quoteValues };
|
|
65
|
+
fileMap.set(filePath, rules);
|
|
66
|
+
return rules;
|
|
56
67
|
};
|
|
57
68
|
const preserveMultilineStrings = (yamlDocument) => {
|
|
58
69
|
if (!yamlDocument.contents)
|
|
@@ -59,13 +59,14 @@ const generateAddedFileSummary = (file) => {
|
|
|
59
59
|
return file.path;
|
|
60
60
|
return `<span class="filename-transform">${file.originalPath} → ${file.path}</span>`;
|
|
61
61
|
};
|
|
62
|
-
const
|
|
62
|
+
const JUMP_TO_SIDEBAR_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 2h3v12H2V2zm0-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H2zm5 4h7v1H7V5zm0 3h7v1H7V8zm0 3h5v1H7v-1z"/></svg>`;
|
|
63
|
+
const generateAddedFileSection = (file, fileId, open) => {
|
|
63
64
|
const summary = generateAddedFileSummary(file);
|
|
64
65
|
const escapedContent = (0, treeRenderer_1.escapeHtml)(file.processedContent);
|
|
65
66
|
const filename = file.path.split('/').pop() || file.path;
|
|
66
67
|
return `
|
|
67
|
-
<details class="file-section" id="${fileId}" data-file-id="${fileId}" open>
|
|
68
|
-
<summary>${summary}</summary>
|
|
68
|
+
<details class="file-section" id="${fileId}" data-file-id="${fileId}"${open ? ' open' : ''}>
|
|
69
|
+
<summary><a class="jump-to-sidebar-link" data-file-id="${fileId}" href="#" title="Show in file browser">${JUMP_TO_SIDEBAR_ICON}</a><span class="summary-expand-icon"></span>${summary}</summary>
|
|
69
70
|
<div class="content-container">
|
|
70
71
|
<div class="content-actions">
|
|
71
72
|
<button class="copy-btn" data-file-id="${fileId}" title="Copy to clipboard">📋 Copy</button>
|
|
@@ -76,7 +77,7 @@ const generateAddedFileSection = (file, fileId) => {
|
|
|
76
77
|
</details>
|
|
77
78
|
`;
|
|
78
79
|
};
|
|
79
|
-
const generateChangedFileSection = (file, fileId) => {
|
|
80
|
+
const generateChangedFileSection = (file, fileId, open) => {
|
|
80
81
|
const isYaml = (0, fileType_1.isYamlFile)(file.path);
|
|
81
82
|
const summary = generateFileSummary(file);
|
|
82
83
|
const destinationContent = (0, serialization_1.serializeForDiff)(file.processedDestContent, isYaml);
|
|
@@ -86,8 +87,8 @@ const generateChangedFileSection = (file, fileId) => {
|
|
|
86
87
|
const { added, removed } = countDiffLines(unifiedDiff);
|
|
87
88
|
const escapedDiff = (0, treeRenderer_1.escapeHtml)(unifiedDiff);
|
|
88
89
|
const html = `
|
|
89
|
-
<details class="file-section" id="${fileId}" data-file-id="${fileId}" open>
|
|
90
|
-
<summary>${summary}<span class="summary-badges"><span class="line-badge line-added">+${added}</span><span class="line-badge line-removed">-${removed}</span></span></summary>
|
|
90
|
+
<details class="file-section" id="${fileId}" data-file-id="${fileId}"${open ? ' open' : ''}>
|
|
91
|
+
<summary><a class="jump-to-sidebar-link" data-file-id="${fileId}" href="#" title="Show in file browser">${JUMP_TO_SIDEBAR_ICON}</a><span class="summary-expand-icon"></span>${summary}<span class="summary-badges"><span class="line-badge line-added">+${added}</span><span class="line-badge line-removed">-${removed}</span></span></summary>
|
|
91
92
|
<div class="diff-toolbar">
|
|
92
93
|
<button class="copy-diff-btn" data-file-id="${fileId}">Copy Diff</button>
|
|
93
94
|
</div>
|
|
@@ -131,16 +132,19 @@ const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun, lo
|
|
|
131
132
|
const changedFileIds = new Map();
|
|
132
133
|
for (const [index, file] of diffResult.changedFiles.entries())
|
|
133
134
|
changedFileIds.set(file.path, `file-${index}`);
|
|
135
|
+
const COLLAPSE_THRESHOLD = 10;
|
|
136
|
+
const changedOpen = diffResult.changedFiles.length <= COLLAPSE_THRESHOLD;
|
|
137
|
+
const addedOpen = diffResult.addedFiles.length <= COLLAPSE_THRESHOLD;
|
|
134
138
|
const fileStats = new Map();
|
|
135
139
|
const changedSections = diffResult.changedFiles.map((file, index) => {
|
|
136
|
-
const result = generateChangedFileSection(file, `file-${index}
|
|
140
|
+
const result = generateChangedFileSection(file, `file-${index}`, changedOpen);
|
|
137
141
|
fileStats.set(file.path, { added: result.added, removed: result.removed });
|
|
138
142
|
return result.html;
|
|
139
143
|
});
|
|
140
144
|
const addedFileIds = new Map();
|
|
141
145
|
for (const [index, file] of diffResult.addedFiles.entries())
|
|
142
146
|
addedFileIds.set(file.path, `added-file-${index}`);
|
|
143
|
-
const addedSections = diffResult.addedFiles.map((file, index) => generateAddedFileSection(file, `added-file-${index}
|
|
147
|
+
const addedSections = diffResult.addedFiles.map((file, index) => generateAddedFileSection(file, `added-file-${index}`, addedOpen));
|
|
144
148
|
let totalAdded = 0;
|
|
145
149
|
let totalRemoved = 0;
|
|
146
150
|
const statsArray = [];
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export declare const DIFF2HTML_STYLES: string;
|
|
2
|
-
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 .file-section[open] > summary {\n position: sticky;\n top: 0;\n z-index: 10;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\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\n /* Added content area (same as changed-content) */\n .added-content {\n flex: 1;\n min-width: 0;\n padding-left: 20px;\n }\n\n /* Content container for added files */\n .content-container {\n padding: 16px;\n background: #f6f8fa;\n border-top: 1px solid #d0d7de;\n }\n\n .content-actions {\n display: flex;\n gap: 8px;\n margin-bottom: 12px;\n }\n\n .copy-btn,\n .download-btn {\n padding: 6px 12px;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n background: white;\n cursor: pointer;\n font-size: 13px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n color: #24292e;\n transition: all 0.2s;\n }\n\n .copy-btn:hover,\n .download-btn:hover {\n background: #f3f4f6;\n border-color: #b0b7be;\n }\n\n .copy-btn.copied {\n background: #d4edda;\n border-color: #28a745;\n color: #155724;\n }\n\n .file-content {\n margin: 0;\n padding: 16px;\n background: white;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n overflow-x: auto;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre;\n }\n\n .file-content code {\n font-family: inherit;\n }\n\n /* Scroll-to-top button */\n .scroll-to-top {\n display: none;\n position: fixed;\n bottom: 30px;\n right: 30px;\n width: 40px;\n height: 40px;\n border: none;\n border-radius: 50%;\n background: #0969da;\n color: white;\n font-size: 18px;\n cursor: pointer;\n box-shadow: 0 2px 8px rgba(0,0,0,0.2);\n transition: opacity 0.2s, background 0.2s;\n z-index: 200;\n line-height: 40px;\n text-align: center;\n padding: 0;\n }\n\n .scroll-to-top:hover {\n background: #0550ae;\n }\n\n .scroll-to-top.visible {\n display: block;\n }\n\n /* Content toolbar (collapse/expand buttons) */\n .content-toolbar {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n margin-bottom: 12px;\n }\n\n .collapse-all-btn,\n .expand-all-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .collapse-all-btn:hover,\n .expand-all-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n /* Line change count badges */\n .summary-badges {\n float: right;\n display: inline-flex;\n gap: 6px;\n margin-left: 12px;\n }\n\n .line-badge {\n display: inline-block;\n padding: 1px 8px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n line-height: 18px;\n }\n\n .line-added {\n background: #d4edda;\n color: #155724;\n }\n\n .line-removed {\n background: #f8d7da;\n color: #721c24;\n }\n\n /* Diff toolbar */\n .diff-toolbar {\n display: flex;\n justify-content: flex-end;\n padding: 8px 16px;\n border-bottom: 1px solid #d0d7de;\n background: #f6f8fa;\n }\n\n .copy-diff-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n background: white;\n cursor: pointer;\n font-size: 12px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n color: #24292e;\n transition: all 0.2s;\n }\n\n .copy-diff-btn:hover {\n background: #f3f4f6;\n border-color: #b0b7be;\n }\n\n .copy-diff-btn.copied {\n background: #d4edda;\n border-color: #28a745;\n color: #155724;\n }\n\n /* Sidebar search */\n .sidebar-search {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n margin-bottom: 8px;\n box-sizing: border-box;\n position: sticky;\n top: 0;\n z-index: 1;\n background: #fff;\n }\n\n .sidebar-search:focus {\n outline: none;\n border-color: #0969da;\n box-shadow: 0 0 0 3px rgba(9,105,218,0.15);\n }\n\n /* Stats toggle button */\n .stats-toggle-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .stats-toggle-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n /* Statistics dashboard */\n .stats-dashboard {\n margin: 15px 0 0;\n padding: 12px 0 0;\n border-top: 1px solid #e1e4e8;\n }\n\n .stats-summary {\n display: flex;\n gap: 16px;\n align-items: center;\n margin-bottom: 10px;\n }\n\n .stats-summary .total-added {\n font-weight: 700;\n color: #155724;\n font-size: 16px;\n }\n\n .stats-summary .total-removed {\n font-weight: 700;\n color: #721c24;\n font-size: 16px;\n }\n\n .stats-bar {\n display: flex;\n height: 8px;\n border-radius: 4px;\n overflow: hidden;\n background: #e1e4e8;\n margin-bottom: 10px;\n }\n\n .stats-segment {\n height: 100%;\n min-width: 2px;\n }\n\n .stats-segment.added-segment {\n background: #28a745;\n }\n\n .stats-segment.removed-segment {\n background: #d73a49;\n }\n\n .top-changed-files {\n list-style: none;\n padding: 0;\n margin: 0;\n font-size: 13px;\n }\n\n .top-changed-files li {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 3px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n color: #586069;\n }\n\n .top-changed-files .file-path {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n margin-right: 8px;\n }\n\n .top-changed-files .file-stats {\n white-space: nowrap;\n flex-shrink: 0;\n }\n\n /* Stop rules violations badge */\n .stat.violations { background: #f8d7da; color: #721c24; }\n\n /* Violations section */\n .violations-section {\n margin: 15px 0 0;\n padding: 12px 0 0;\n border-top: 1px solid #e1e4e8;\n }\n\n .violations-toggle-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .violations-toggle-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n .violations-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n margin-top: 10px;\n }\n\n .violations-table th {\n background: #f6f8fa;\n text-align: left;\n padding: 8px 12px;\n border-bottom: 2px solid #d0d7de;\n font-weight: 600;\n color: #24292e;\n }\n\n .violations-table td {\n padding: 8px 12px;\n border-bottom: 1px solid #e1e4e8;\n color: #24292e;\n vertical-align: top;\n }\n\n .violations-table tr:hover td {\n background: #f6f8fa;\n }\n\n .violation-rule-badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n background: #fff3cd;\n color: #856404;\n white-space: nowrap;\n }\n\n .violation-value {\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n background: #f6f8fa;\n padding: 2px 6px;\n border-radius: 3px;\n border: 1px solid #e1e4e8;\n }\n";
|
|
2
|
+
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 display: flex;\n align-items: center;\n gap: 6px;\n list-style: none;\n }\n\n .file-section summary::-webkit-details-marker {\n display: none;\n }\n\n .summary-expand-icon::before {\n content: '\u25B6';\n font-size: 10px;\n color: #6a737d;\n }\n\n .file-section[open] > summary .summary-expand-icon::before {\n content: '\u25BC';\n }\n\n .jump-to-sidebar-link {\n display: inline-flex;\n align-items: center;\n color: #6a737d;\n text-decoration: none;\n padding: 2px 3px;\n border-radius: 3px;\n flex-shrink: 0;\n line-height: 1;\n }\n\n .jump-to-sidebar-link:hover {\n color: #0969da;\n background: rgba(9, 105, 218, 0.08);\n }\n\n .file-section summary:hover {\n background: #eaeef2;\n }\n\n .file-section[open] > summary {\n position: sticky;\n top: 0;\n z-index: 10;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\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\n /* Added content area (same as changed-content) */\n .added-content {\n flex: 1;\n min-width: 0;\n padding-left: 20px;\n }\n\n /* Content container for added files */\n .content-container {\n padding: 16px;\n background: #f6f8fa;\n border-top: 1px solid #d0d7de;\n }\n\n .content-actions {\n display: flex;\n gap: 8px;\n margin-bottom: 12px;\n }\n\n .copy-btn,\n .download-btn {\n padding: 6px 12px;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n background: white;\n cursor: pointer;\n font-size: 13px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n color: #24292e;\n transition: all 0.2s;\n }\n\n .copy-btn:hover,\n .download-btn:hover {\n background: #f3f4f6;\n border-color: #b0b7be;\n }\n\n .copy-btn.copied {\n background: #d4edda;\n border-color: #28a745;\n color: #155724;\n }\n\n .file-content {\n margin: 0;\n padding: 16px;\n background: white;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n overflow-x: auto;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre;\n }\n\n .file-content code {\n font-family: inherit;\n }\n\n /* Scroll-to-top button */\n .scroll-to-top {\n display: none;\n position: fixed;\n bottom: 30px;\n right: 30px;\n width: 40px;\n height: 40px;\n border: none;\n border-radius: 50%;\n background: #0969da;\n color: white;\n font-size: 18px;\n cursor: pointer;\n box-shadow: 0 2px 8px rgba(0,0,0,0.2);\n transition: opacity 0.2s, background 0.2s;\n z-index: 200;\n line-height: 40px;\n text-align: center;\n padding: 0;\n }\n\n .scroll-to-top:hover {\n background: #0550ae;\n }\n\n .scroll-to-top.visible {\n display: block;\n }\n\n /* Content toolbar (collapse/expand buttons) */\n .content-toolbar {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n margin-bottom: 12px;\n }\n\n .collapse-all-btn,\n .expand-all-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .collapse-all-btn:hover,\n .expand-all-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n /* Line change count badges */\n .summary-badges {\n display: inline-flex;\n gap: 6px;\n margin-left: auto;\n }\n\n .line-badge {\n display: inline-block;\n padding: 1px 8px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n line-height: 18px;\n }\n\n .line-added {\n background: #d4edda;\n color: #155724;\n }\n\n .line-removed {\n background: #f8d7da;\n color: #721c24;\n }\n\n /* Diff toolbar */\n .diff-toolbar {\n display: flex;\n justify-content: flex-end;\n padding: 8px 16px;\n border-bottom: 1px solid #d0d7de;\n background: #f6f8fa;\n }\n\n .copy-diff-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n background: white;\n cursor: pointer;\n font-size: 12px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n color: #24292e;\n transition: all 0.2s;\n }\n\n .copy-diff-btn:hover {\n background: #f3f4f6;\n border-color: #b0b7be;\n }\n\n .copy-diff-btn.copied {\n background: #d4edda;\n border-color: #28a745;\n color: #155724;\n }\n\n /* Sidebar search */\n .sidebar-search {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n margin-bottom: 8px;\n box-sizing: border-box;\n position: sticky;\n top: 0;\n z-index: 1;\n background: #fff;\n }\n\n .sidebar-search:focus {\n outline: none;\n border-color: #0969da;\n box-shadow: 0 0 0 3px rgba(9,105,218,0.15);\n }\n\n /* Stats toggle button */\n .stats-toggle-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .stats-toggle-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n /* Statistics dashboard */\n .stats-dashboard {\n margin: 15px 0 0;\n padding: 12px 0 0;\n border-top: 1px solid #e1e4e8;\n }\n\n .stats-summary {\n display: flex;\n gap: 16px;\n align-items: center;\n margin-bottom: 10px;\n }\n\n .stats-summary .total-added {\n font-weight: 700;\n color: #155724;\n font-size: 16px;\n }\n\n .stats-summary .total-removed {\n font-weight: 700;\n color: #721c24;\n font-size: 16px;\n }\n\n .stats-bar {\n display: flex;\n height: 8px;\n border-radius: 4px;\n overflow: hidden;\n background: #e1e4e8;\n margin-bottom: 10px;\n }\n\n .stats-segment {\n height: 100%;\n min-width: 2px;\n }\n\n .stats-segment.added-segment {\n background: #28a745;\n }\n\n .stats-segment.removed-segment {\n background: #d73a49;\n }\n\n .top-changed-files {\n list-style: none;\n padding: 0;\n margin: 0;\n font-size: 13px;\n }\n\n .top-changed-files li {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 3px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n color: #586069;\n }\n\n .top-changed-files .file-path {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n margin-right: 8px;\n }\n\n .top-changed-files .file-stats {\n white-space: nowrap;\n flex-shrink: 0;\n }\n\n /* Stop rules violations badge */\n .stat.violations { background: #f8d7da; color: #721c24; }\n\n /* Violations section */\n .violations-section {\n margin: 15px 0 0;\n padding: 12px 0 0;\n border-top: 1px solid #e1e4e8;\n }\n\n .violations-toggle-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .violations-toggle-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n .violations-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n margin-top: 10px;\n }\n\n .violations-table th {\n background: #f6f8fa;\n text-align: left;\n padding: 8px 12px;\n border-bottom: 2px solid #d0d7de;\n font-weight: 600;\n color: #24292e;\n }\n\n .violations-table td {\n padding: 8px 12px;\n border-bottom: 1px solid #e1e4e8;\n color: #24292e;\n vertical-align: top;\n }\n\n .violations-table tr:hover td {\n background: #f6f8fa;\n }\n\n .violation-rule-badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n background: #fff3cd;\n color: #856404;\n white-space: nowrap;\n }\n\n .violation-value {\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n background: #f6f8fa;\n padding: 2px 6px;\n border-radius: 3px;\n border: 1px solid #e1e4e8;\n }\n";
|
|
3
3
|
export declare const TAB_SCRIPT: string;
|
|
@@ -119,6 +119,40 @@ exports.HTML_STYLES = `
|
|
|
119
119
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
120
120
|
font-size: 13px;
|
|
121
121
|
color: #24292e;
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 6px;
|
|
125
|
+
list-style: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.file-section summary::-webkit-details-marker {
|
|
129
|
+
display: none;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.summary-expand-icon::before {
|
|
133
|
+
content: '\u25B6';
|
|
134
|
+
font-size: 10px;
|
|
135
|
+
color: #6a737d;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.file-section[open] > summary .summary-expand-icon::before {
|
|
139
|
+
content: '\u25BC';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.jump-to-sidebar-link {
|
|
143
|
+
display: inline-flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
color: #6a737d;
|
|
146
|
+
text-decoration: none;
|
|
147
|
+
padding: 2px 3px;
|
|
148
|
+
border-radius: 3px;
|
|
149
|
+
flex-shrink: 0;
|
|
150
|
+
line-height: 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.jump-to-sidebar-link:hover {
|
|
154
|
+
color: #0969da;
|
|
155
|
+
background: rgba(9, 105, 218, 0.08);
|
|
122
156
|
}
|
|
123
157
|
|
|
124
158
|
.file-section summary:hover {
|
|
@@ -445,10 +479,9 @@ exports.HTML_STYLES = `
|
|
|
445
479
|
|
|
446
480
|
/* Line change count badges */
|
|
447
481
|
.summary-badges {
|
|
448
|
-
float: right;
|
|
449
482
|
display: inline-flex;
|
|
450
483
|
gap: 6px;
|
|
451
|
-
margin-left:
|
|
484
|
+
margin-left: auto;
|
|
452
485
|
}
|
|
453
486
|
|
|
454
487
|
.line-badge {
|
|
@@ -770,6 +803,37 @@ exports.TAB_SCRIPT = String.raw `
|
|
|
770
803
|
});
|
|
771
804
|
});
|
|
772
805
|
|
|
806
|
+
// Jump-to-sidebar link in file block headers
|
|
807
|
+
document.querySelectorAll('.jump-to-sidebar-link').forEach(link => {
|
|
808
|
+
link.addEventListener('click', (e) => {
|
|
809
|
+
e.preventDefault();
|
|
810
|
+
e.stopPropagation(); // prevent <details> toggle
|
|
811
|
+
const fileId = link.getAttribute('data-file-id');
|
|
812
|
+
const tabContent = link.closest('.tab-content');
|
|
813
|
+
if (!tabContent || !fileId) return;
|
|
814
|
+
|
|
815
|
+
const treeFile = tabContent.querySelector('.tree-file[data-file-id="' + fileId + '"]');
|
|
816
|
+
if (!treeFile) return;
|
|
817
|
+
|
|
818
|
+
// Expand any collapsed parent folders
|
|
819
|
+
let el = treeFile.parentElement;
|
|
820
|
+
while (el) {
|
|
821
|
+
if (el.classList.contains('tree-children')) {
|
|
822
|
+
el.style.display = '';
|
|
823
|
+
if (el.parentElement) el.parentElement.classList.remove('collapsed');
|
|
824
|
+
}
|
|
825
|
+
el = el.parentElement;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Highlight in sidebar
|
|
829
|
+
tabContent.querySelectorAll('.sidebar-tree .tree-file').forEach(f => f.classList.remove('active'));
|
|
830
|
+
treeFile.classList.add('active');
|
|
831
|
+
|
|
832
|
+
// Scroll sidebar to the file entry
|
|
833
|
+
treeFile.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
773
837
|
// IntersectionObserver to highlight current file on scroll
|
|
774
838
|
const fileSections = document.querySelectorAll('.file-section[id]');
|
|
775
839
|
if (fileSections.length > 0 && 'IntersectionObserver' in window) {
|
|
@@ -144,6 +144,10 @@ const generateHtmlTemplate = (diffResult, formattedFiles, trulyUnchangedFiles, m
|
|
|
144
144
|
</aside>
|
|
145
145
|
<button class="sidebar-expand-btn" data-sidebar="added">▶</button>
|
|
146
146
|
<div class="added-content">
|
|
147
|
+
<div class="content-toolbar">
|
|
148
|
+
<button class="collapse-all-btn">Collapse All</button>
|
|
149
|
+
<button class="expand-all-btn">Expand All</button>
|
|
150
|
+
</div>
|
|
147
151
|
${addedSections.join('\n')}
|
|
148
152
|
</div>
|
|
149
153
|
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.loadRegexPatternsFromKeys = exports.loadRegexPatternArray = exports.isRegexPatternFileLoaderError = exports.RegexPatternFileLoaderError = void 0;
|
|
4
4
|
const errors_1 = require("./errors");
|
|
5
|
+
const regexSafety_1 = require("./regexSafety");
|
|
5
6
|
const yamlFileLoader_1 = require("./yamlFileLoader");
|
|
6
7
|
exports.RegexPatternFileLoaderError = (0, errors_1.createErrorClass)('RegexPatternFileLoaderError', {
|
|
7
8
|
INVALID_FORMAT_NOT_ARRAY: 'Pattern file must contain a YAML array of regex patterns',
|
|
@@ -38,6 +39,8 @@ const validateRegexPattern = (pattern, source) => {
|
|
|
38
39
|
cause: error
|
|
39
40
|
});
|
|
40
41
|
}
|
|
42
|
+
if (!(0, regexSafety_1.isSafeRegex)(pattern))
|
|
43
|
+
throw new exports.RegexPatternFileLoaderError(`Potentially unsafe regex pattern "${pattern}" ${source} — may cause catastrophic backtracking (ReDoS)`, { code: 'INVALID_REGEX' });
|
|
41
44
|
};
|
|
42
45
|
const loadRegexPatternArray = (filePath, configDirectory) => {
|
|
43
46
|
let parsedData;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isSafeRegex: (pattern: string) => boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSafeRegex = void 0;
|
|
4
|
+
const isSafeRegex = (pattern) => {
|
|
5
|
+
if (/\([^()]*[*+][^()]*\)[*+{]/.test(pattern))
|
|
6
|
+
return false;
|
|
7
|
+
return true;
|
|
8
|
+
};
|
|
9
|
+
exports.isSafeRegex = isSafeRegex;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.applyRegexRulesSequentially = void 0;
|
|
4
|
+
const regexCache = new Map();
|
|
4
5
|
const applyRegexRulesSequentially = (value, rules, throwOnError = false) => {
|
|
5
6
|
let result = value;
|
|
6
7
|
for (const rule of rules)
|
|
7
8
|
try {
|
|
8
|
-
|
|
9
|
+
let regex = regexCache.get(rule.find);
|
|
10
|
+
if (!regex) {
|
|
11
|
+
regex = new RegExp(rule.find, 'g');
|
|
12
|
+
regexCache.set(rule.find, regex);
|
|
13
|
+
}
|
|
9
14
|
result = result.replace(regex, rule.replace);
|
|
10
15
|
}
|
|
11
16
|
catch (error) {
|
|
@@ -15,6 +15,19 @@ const serializeForDiff = (content, isYaml) => {
|
|
|
15
15
|
});
|
|
16
16
|
};
|
|
17
17
|
exports.serializeForDiff = serializeForDiff;
|
|
18
|
+
const deepSortKeys = (value) => {
|
|
19
|
+
if (value === null || value === undefined)
|
|
20
|
+
return value;
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return value.map((item) => deepSortKeys(item));
|
|
23
|
+
if (typeof value === 'object') {
|
|
24
|
+
const sorted = {};
|
|
25
|
+
for (const key of Object.keys(value).toSorted())
|
|
26
|
+
sorted[key] = deepSortKeys(value[key]);
|
|
27
|
+
return sorted;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
18
31
|
const normalizeForComparison = (value) => {
|
|
19
32
|
if (value === null || value === undefined)
|
|
20
33
|
return value;
|
|
@@ -25,7 +38,7 @@ const normalizeForComparison = (value) => {
|
|
|
25
38
|
const normalized = value.map((item) => (0, exports.normalizeForComparison)(item));
|
|
26
39
|
const serializedItems = normalized.map((item) => ({
|
|
27
40
|
item,
|
|
28
|
-
serialized:
|
|
41
|
+
serialized: JSON.stringify(deepSortKeys(item)) ?? ''
|
|
29
42
|
}));
|
|
30
43
|
return serializedItems.toSorted((a, b) => a.serialized.localeCompare(b.serialized)).map(({ item }) => item);
|
|
31
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helm-env-delta",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.2",
|
|
4
4
|
"description": "HelmEnvDelta – environment-aware YAML delta and sync for GitOps",
|
|
5
5
|
"author": "BCsabaEngine",
|
|
6
6
|
"license": "ISC",
|
|
@@ -65,18 +65,18 @@
|
|
|
65
65
|
],
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@eslint/js": "^10.0.1",
|
|
68
|
-
"@types/node": "^25.
|
|
68
|
+
"@types/node": "^25.5.0",
|
|
69
69
|
"@types/picomatch": "^4.0.2",
|
|
70
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
71
|
-
"@vitest/coverage-v8": "^4.0
|
|
72
|
-
"eslint": "^10.0.
|
|
70
|
+
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
|
71
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
72
|
+
"eslint": "^10.0.3",
|
|
73
73
|
"eslint-config-prettier": "^10.1.8",
|
|
74
74
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
75
75
|
"eslint-plugin-unicorn": "^63.0.0",
|
|
76
76
|
"prettier": "^3.8.1",
|
|
77
77
|
"tsx": "^4.21.0",
|
|
78
78
|
"typescript": "^5.9.3",
|
|
79
|
-
"vitest": "^4.0
|
|
79
|
+
"vitest": "^4.1.0"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"chalk": "^5.6.2",
|