helm-env-delta 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +43 -7
- 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,8 +8,10 @@ const promises_1 = require("node:fs/promises");
|
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const yaml_1 = __importDefault(require("yaml"));
|
|
10
10
|
const consoleFormatter_1 = require("./consoleFormatter");
|
|
11
|
+
const arrayMerger_1 = require("./utils/arrayMerger");
|
|
11
12
|
const errors_1 = require("./utils/errors");
|
|
12
13
|
const fileType_1 = require("./utils/fileType");
|
|
14
|
+
const fixedValues_1 = require("./utils/fixedValues");
|
|
13
15
|
const transformer_1 = require("./utils/transformer");
|
|
14
16
|
const yamlFormatter_1 = require("./yamlFormatter");
|
|
15
17
|
const FileUpdaterErrorClass = (0, errors_1.createErrorClass)('File Updater Error', {
|
|
@@ -61,15 +63,40 @@ const ensureParentDirectory = async (filePath) => {
|
|
|
61
63
|
});
|
|
62
64
|
}
|
|
63
65
|
};
|
|
64
|
-
const deepMerge = (fullTarget, filteredSource, filteredTarget) => {
|
|
66
|
+
const deepMerge = (fullTarget, filteredSource, filteredTarget, currentPath = [], skipPaths = []) => {
|
|
65
67
|
if (filteredSource === null || filteredSource === undefined)
|
|
66
68
|
return fullTarget;
|
|
67
69
|
if (fullTarget === null || fullTarget === undefined)
|
|
68
70
|
return filteredSource;
|
|
69
71
|
if (typeof fullTarget !== typeof filteredSource)
|
|
70
72
|
return filteredSource;
|
|
71
|
-
if (Array.isArray(filteredSource))
|
|
72
|
-
|
|
73
|
+
if (Array.isArray(filteredSource)) {
|
|
74
|
+
const fullTargetArray = Array.isArray(fullTarget) ? fullTarget : [];
|
|
75
|
+
const filteredTargetArray = Array.isArray(filteredTarget) ? filteredTarget : [];
|
|
76
|
+
const applicableFilters = (0, arrayMerger_1.getApplicableArrayFilters)(currentPath, skipPaths);
|
|
77
|
+
if (applicableFilters.length === 0)
|
|
78
|
+
return filteredSource;
|
|
79
|
+
const hasNestedFilters = applicableFilters.some((f) => f.remainingPath.length > 0);
|
|
80
|
+
const result = [];
|
|
81
|
+
for (const sourceItem of filteredSource) {
|
|
82
|
+
if (hasNestedFilters && sourceItem && typeof sourceItem === 'object') {
|
|
83
|
+
const { matches, matchedFilter } = (0, arrayMerger_1.itemMatchesAnyFilter)(sourceItem, applicableFilters);
|
|
84
|
+
if (matches && matchedFilter && matchedFilter.remainingPath.length > 0) {
|
|
85
|
+
const matchingTargetItem = (0, arrayMerger_1.findMatchingTargetItem)(sourceItem, fullTargetArray, applicableFilters);
|
|
86
|
+
const matchingFilteredTargetItem = (0, arrayMerger_1.findMatchingTargetItem)(sourceItem, filteredTargetArray, applicableFilters);
|
|
87
|
+
if (matchingTargetItem) {
|
|
88
|
+
result.push(deepMerge(matchingTargetItem, sourceItem, matchingFilteredTargetItem, currentPath, skipPaths));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
result.push(sourceItem);
|
|
94
|
+
}
|
|
95
|
+
for (const item of fullTargetArray)
|
|
96
|
+
if ((0, arrayMerger_1.shouldPreserveItem)(item, applicableFilters, result))
|
|
97
|
+
result.push(item);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
73
100
|
if (typeof filteredSource === 'object' && typeof fullTarget === 'object') {
|
|
74
101
|
const sourceObject = filteredSource;
|
|
75
102
|
const fullTargetObject = fullTarget;
|
|
@@ -80,12 +107,12 @@ const deepMerge = (fullTarget, filteredSource, filteredTarget) => {
|
|
|
80
107
|
result[key] = value;
|
|
81
108
|
for (const [key, value] of Object.entries(sourceObject))
|
|
82
109
|
if (key in fullTargetObject)
|
|
83
|
-
result[key] = deepMerge(fullTargetObject[key], value, filteredTargetObject[key]);
|
|
110
|
+
result[key] = deepMerge(fullTargetObject[key], value, filteredTargetObject[key], [...currentPath, key], skipPaths);
|
|
84
111
|
return result;
|
|
85
112
|
}
|
|
86
113
|
return filteredSource;
|
|
87
114
|
};
|
|
88
|
-
const mergeYamlContent = (destinationContent, processedSourceContent, filteredDestinationContent, filePath) => {
|
|
115
|
+
const mergeYamlContent = (destinationContent, processedSourceContent, filteredDestinationContent, filePath, skipPaths = []) => {
|
|
89
116
|
let destinationParsed;
|
|
90
117
|
try {
|
|
91
118
|
destinationParsed = yaml_1.default.parse(destinationContent);
|
|
@@ -104,7 +131,7 @@ const mergeYamlContent = (destinationContent, processedSourceContent, filteredDe
|
|
|
104
131
|
}
|
|
105
132
|
let merged;
|
|
106
133
|
try {
|
|
107
|
-
merged = deepMerge(destinationParsed, processedSourceContent, filteredDestinationContent);
|
|
134
|
+
merged = deepMerge(destinationParsed, processedSourceContent, filteredDestinationContent, [], skipPaths);
|
|
108
135
|
}
|
|
109
136
|
catch (error) {
|
|
110
137
|
throw new FileUpdaterError('Failed to merge YAML content', {
|
|
@@ -136,6 +163,9 @@ const addFile = async (options) => {
|
|
|
136
163
|
try {
|
|
137
164
|
const parsed = yaml_1.default.parse(content);
|
|
138
165
|
const transformed = (0, transformer_1.applyTransforms)(parsed, relativePath, config.transforms);
|
|
166
|
+
const fixedValueRules = (0, fixedValues_1.getFixedValuesForFile)(relativePath, config.fixedValues);
|
|
167
|
+
if (fixedValueRules.length > 0)
|
|
168
|
+
(0, fixedValues_1.applyFixedValues)(transformed, fixedValueRules);
|
|
139
169
|
contentToWrite = yaml_1.default.stringify(transformed);
|
|
140
170
|
const effectiveOutputFormat = skipFormat ? undefined : config.outputFormat;
|
|
141
171
|
contentToWrite = (0, yamlFormatter_1.formatYaml)(contentToWrite, relativePath, effectiveOutputFormat);
|
|
@@ -168,9 +198,15 @@ const updateFile = async (options) => {
|
|
|
168
198
|
return;
|
|
169
199
|
}
|
|
170
200
|
let contentToWrite = (0, fileType_1.isYamlFile)(changedFile.path)
|
|
171
|
-
? mergeYamlContent(changedFile.destinationContent, changedFile.rawParsedSource, changedFile.rawParsedDest, changedFile.path)
|
|
201
|
+
? mergeYamlContent(changedFile.destinationContent, changedFile.rawParsedSource, changedFile.rawParsedDest, changedFile.path, changedFile.skipPaths)
|
|
172
202
|
: changedFile.sourceContent;
|
|
173
203
|
if ((0, fileType_1.isYamlFile)(changedFile.path)) {
|
|
204
|
+
const fixedValueRules = (0, fixedValues_1.getFixedValuesForFile)(changedFile.path, config.fixedValues);
|
|
205
|
+
if (fixedValueRules.length > 0) {
|
|
206
|
+
const parsed = yaml_1.default.parse(contentToWrite);
|
|
207
|
+
(0, fixedValues_1.applyFixedValues)(parsed, fixedValueRules);
|
|
208
|
+
contentToWrite = yaml_1.default.stringify(parsed);
|
|
209
|
+
}
|
|
174
210
|
const effectiveOutputFormat = skipFormat ? undefined : config.outputFormat;
|
|
175
211
|
contentToWrite = (0, yamlFormatter_1.formatYaml)(contentToWrite, changedFile.path, effectiveOutputFormat);
|
|
176
212
|
}
|
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;
|