helm-env-delta 1.8.1 → 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 +62 -1
- package/dist/configFile.d.ts +14 -0
- package/dist/configFile.js +6 -1
- package/dist/configWarnings.js +12 -0
- package/dist/fileDiff.d.ts +2 -1
- package/dist/fileDiff.js +9 -4
- package/dist/fileUpdater.js +16 -78
- package/dist/htmlReporter.js +9 -82
- package/dist/patternUsageValidator.d.ts +1 -1
- package/dist/patternUsageValidator.js +34 -1
- package/dist/reporters/htmlStyles.d.ts +2 -2
- package/dist/reporters/htmlStyles.js +194 -66
- package/dist/reporters/htmlTemplate.d.ts +1 -1
- package/dist/reporters/htmlTemplate.js +32 -15
- package/dist/reporters/treeBuilder.d.ts +7 -0
- package/dist/reporters/treeBuilder.js +62 -0
- package/dist/reporters/treeRenderer.d.ts +3 -0
- package/dist/reporters/treeRenderer.js +52 -0
- package/dist/utils/arrayMerger.d.ts +15 -0
- package/dist/utils/arrayMerger.js +80 -0
- package/dist/utils/fixedValues.d.ts +4 -0
- package/dist/utils/fixedValues.js +89 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +10 -1
- package/package.json +1 -1
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** -
|
|
844
|
+
✅ **Reliability** - 1150+ tests, 84% coverage. Battle-tested.
|
|
784
845
|
|
|
785
846
|
---
|
|
786
847
|
|
package/dist/configFile.d.ts
CHANGED
|
@@ -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;
|
package/dist/configFile.js
CHANGED
|
@@ -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 })
|
package/dist/configWarnings.js
CHANGED
|
@@ -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
|
package/dist/fileDiff.d.ts
CHANGED
|
@@ -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;
|
package/dist/fileUpdater.js
CHANGED
|
@@ -8,9 +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");
|
|
13
|
-
const
|
|
14
|
+
const fixedValues_1 = require("./utils/fixedValues");
|
|
14
15
|
const transformer_1 = require("./utils/transformer");
|
|
15
16
|
const yamlFormatter_1 = require("./yamlFormatter");
|
|
16
17
|
const FileUpdaterErrorClass = (0, errors_1.createErrorClass)('File Updater Error', {
|
|
@@ -62,78 +63,6 @@ const ensureParentDirectory = async (filePath) => {
|
|
|
62
63
|
});
|
|
63
64
|
}
|
|
64
65
|
};
|
|
65
|
-
const getApplicableArrayFilters = (currentPath, skipPaths) => {
|
|
66
|
-
const filters = [];
|
|
67
|
-
for (const skipPath of skipPaths) {
|
|
68
|
-
const segments = (0, jsonPath_1.parseJsonPath)(skipPath);
|
|
69
|
-
if (segments.length <= currentPath.length)
|
|
70
|
-
continue;
|
|
71
|
-
let isPrefix = true;
|
|
72
|
-
for (let index = 0; index < currentPath.length; index++)
|
|
73
|
-
if (segments[index] !== currentPath[index]) {
|
|
74
|
-
isPrefix = false;
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
if (!isPrefix)
|
|
78
|
-
continue;
|
|
79
|
-
const nextSegment = segments[currentPath.length];
|
|
80
|
-
if (!nextSegment || !(0, jsonPath_1.isFilterSegment)(nextSegment))
|
|
81
|
-
continue;
|
|
82
|
-
const filter = (0, jsonPath_1.parseFilterSegment)(nextSegment);
|
|
83
|
-
if (!filter)
|
|
84
|
-
continue;
|
|
85
|
-
const remainingPath = segments.slice(currentPath.length + 1);
|
|
86
|
-
filters.push({ filter, remainingPath });
|
|
87
|
-
}
|
|
88
|
-
return filters;
|
|
89
|
-
};
|
|
90
|
-
const itemMatchesAnyFilter = (item, applicableFilters) => {
|
|
91
|
-
if (!item || typeof item !== 'object')
|
|
92
|
-
return { matches: false };
|
|
93
|
-
const itemObject = item;
|
|
94
|
-
for (const applicableFilter of applicableFilters) {
|
|
95
|
-
const itemValue = itemObject[applicableFilter.filter.property];
|
|
96
|
-
if (itemValue !== undefined && (0, jsonPath_1.matchesFilter)(itemValue, applicableFilter.filter))
|
|
97
|
-
return { matches: true, matchedFilter: applicableFilter };
|
|
98
|
-
}
|
|
99
|
-
return { matches: false };
|
|
100
|
-
};
|
|
101
|
-
const findMatchingTargetItem = (sourceItem, fullTargetArray, applicableFilters) => {
|
|
102
|
-
if (!sourceItem || typeof sourceItem !== 'object')
|
|
103
|
-
return undefined;
|
|
104
|
-
const sourceObject = sourceItem;
|
|
105
|
-
for (const targetItem of fullTargetArray) {
|
|
106
|
-
if (!targetItem || typeof targetItem !== 'object')
|
|
107
|
-
continue;
|
|
108
|
-
const targetObject = targetItem;
|
|
109
|
-
for (const { filter } of applicableFilters)
|
|
110
|
-
if (sourceObject[filter.property] === targetObject[filter.property])
|
|
111
|
-
return targetItem;
|
|
112
|
-
}
|
|
113
|
-
return undefined;
|
|
114
|
-
};
|
|
115
|
-
const shouldPreserveItem = (item, applicableFilters, existingResult) => {
|
|
116
|
-
if (!item || typeof item !== 'object')
|
|
117
|
-
return false;
|
|
118
|
-
const itemObject = item;
|
|
119
|
-
const { matches } = itemMatchesAnyFilter(item, applicableFilters);
|
|
120
|
-
if (!matches)
|
|
121
|
-
return false;
|
|
122
|
-
for (const existingItem of existingResult) {
|
|
123
|
-
if (!existingItem || typeof existingItem !== 'object')
|
|
124
|
-
continue;
|
|
125
|
-
const existingObject = existingItem;
|
|
126
|
-
let isDuplicate = true;
|
|
127
|
-
for (const { filter } of applicableFilters)
|
|
128
|
-
if (existingObject[filter.property] !== itemObject[filter.property]) {
|
|
129
|
-
isDuplicate = false;
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
if (isDuplicate)
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
return true;
|
|
136
|
-
};
|
|
137
66
|
const deepMerge = (fullTarget, filteredSource, filteredTarget, currentPath = [], skipPaths = []) => {
|
|
138
67
|
if (filteredSource === null || filteredSource === undefined)
|
|
139
68
|
return fullTarget;
|
|
@@ -144,17 +73,17 @@ const deepMerge = (fullTarget, filteredSource, filteredTarget, currentPath = [],
|
|
|
144
73
|
if (Array.isArray(filteredSource)) {
|
|
145
74
|
const fullTargetArray = Array.isArray(fullTarget) ? fullTarget : [];
|
|
146
75
|
const filteredTargetArray = Array.isArray(filteredTarget) ? filteredTarget : [];
|
|
147
|
-
const applicableFilters = getApplicableArrayFilters(currentPath, skipPaths);
|
|
76
|
+
const applicableFilters = (0, arrayMerger_1.getApplicableArrayFilters)(currentPath, skipPaths);
|
|
148
77
|
if (applicableFilters.length === 0)
|
|
149
78
|
return filteredSource;
|
|
150
79
|
const hasNestedFilters = applicableFilters.some((f) => f.remainingPath.length > 0);
|
|
151
80
|
const result = [];
|
|
152
81
|
for (const sourceItem of filteredSource) {
|
|
153
82
|
if (hasNestedFilters && sourceItem && typeof sourceItem === 'object') {
|
|
154
|
-
const { matches, matchedFilter } = itemMatchesAnyFilter(sourceItem, applicableFilters);
|
|
83
|
+
const { matches, matchedFilter } = (0, arrayMerger_1.itemMatchesAnyFilter)(sourceItem, applicableFilters);
|
|
155
84
|
if (matches && matchedFilter && matchedFilter.remainingPath.length > 0) {
|
|
156
|
-
const matchingTargetItem = findMatchingTargetItem(sourceItem, fullTargetArray, applicableFilters);
|
|
157
|
-
const matchingFilteredTargetItem = findMatchingTargetItem(sourceItem, filteredTargetArray, applicableFilters);
|
|
85
|
+
const matchingTargetItem = (0, arrayMerger_1.findMatchingTargetItem)(sourceItem, fullTargetArray, applicableFilters);
|
|
86
|
+
const matchingFilteredTargetItem = (0, arrayMerger_1.findMatchingTargetItem)(sourceItem, filteredTargetArray, applicableFilters);
|
|
158
87
|
if (matchingTargetItem) {
|
|
159
88
|
result.push(deepMerge(matchingTargetItem, sourceItem, matchingFilteredTargetItem, currentPath, skipPaths));
|
|
160
89
|
continue;
|
|
@@ -164,7 +93,7 @@ const deepMerge = (fullTarget, filteredSource, filteredTarget, currentPath = [],
|
|
|
164
93
|
result.push(sourceItem);
|
|
165
94
|
}
|
|
166
95
|
for (const item of fullTargetArray)
|
|
167
|
-
if (shouldPreserveItem(item, applicableFilters, result))
|
|
96
|
+
if ((0, arrayMerger_1.shouldPreserveItem)(item, applicableFilters, result))
|
|
168
97
|
result.push(item);
|
|
169
98
|
return result;
|
|
170
99
|
}
|
|
@@ -234,6 +163,9 @@ const addFile = async (options) => {
|
|
|
234
163
|
try {
|
|
235
164
|
const parsed = yaml_1.default.parse(content);
|
|
236
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);
|
|
237
169
|
contentToWrite = yaml_1.default.stringify(transformed);
|
|
238
170
|
const effectiveOutputFormat = skipFormat ? undefined : config.outputFormat;
|
|
239
171
|
contentToWrite = (0, yamlFormatter_1.formatYaml)(contentToWrite, relativePath, effectiveOutputFormat);
|
|
@@ -269,6 +201,12 @@ const updateFile = async (options) => {
|
|
|
269
201
|
? mergeYamlContent(changedFile.destinationContent, changedFile.rawParsedSource, changedFile.rawParsedDest, changedFile.path, changedFile.skipPaths)
|
|
270
202
|
: changedFile.sourceContent;
|
|
271
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
|
+
}
|
|
272
210
|
const effectiveOutputFormat = skipFormat ? undefined : config.outputFormat;
|
|
273
211
|
contentToWrite = (0, yamlFormatter_1.formatYaml)(contentToWrite, changedFile.path, effectiveOutputFormat);
|
|
274
212
|
}
|
package/dist/htmlReporter.js
CHANGED
|
@@ -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('&', '&')
|
|
38
|
-
.replaceAll('<', '<')
|
|
39
|
-
.replaceAll('>', '>')
|
|
40
|
-
.replaceAll('"', '"')
|
|
41
|
-
.replaceAll("'", ''');
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
167
|
-
const
|
|
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 .
|
|
2
|
-
export declare const TAB_SCRIPT
|
|
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;
|