@aspruyt/json-config-sync 3.1.0 → 3.2.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 +57 -5
- package/dist/config-formatter.d.ts +9 -1
- package/dist/config-formatter.js +61 -3
- package/dist/config-normalizer.js +37 -8
- package/dist/config-validator.js +22 -0
- package/dist/config.d.ts +8 -2
- package/dist/repository-processor.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,8 @@ json-config-sync --config ./config.yaml
|
|
|
66
66
|
- **Environment Variables** - Use `${VAR}` syntax for dynamic values
|
|
67
67
|
- **Merge Strategies** - Control how arrays merge (replace, append, prepend)
|
|
68
68
|
- **Override Mode** - Skip merging entirely for specific repos
|
|
69
|
+
- **Empty Files** - Create files with no content (e.g., `.prettierignore`)
|
|
70
|
+
- **YAML Comments** - Add header comments and schema directives to YAML files
|
|
69
71
|
- **GitHub & Azure DevOps** - Works with both platforms
|
|
70
72
|
- **Dry-Run Mode** - Preview changes without creating PRs
|
|
71
73
|
- **Error Resilience** - Continues processing if individual repos fail
|
|
@@ -165,11 +167,13 @@ repos: # List of repositories
|
|
|
165
167
|
|
|
166
168
|
### Per-File Fields
|
|
167
169
|
|
|
168
|
-
| Field | Description
|
|
169
|
-
| --------------- |
|
|
170
|
-
| `content` | Base config inherited by all repos
|
|
171
|
-
| `mergeStrategy` | Array merge strategy: `replace`, `append`, `prepend`
|
|
172
|
-
| `createOnly` | If `true`, only create file if it doesn't exist
|
|
170
|
+
| Field | Description | Required |
|
|
171
|
+
| --------------- | ---------------------------------------------------------- | -------- |
|
|
172
|
+
| `content` | Base config inherited by all repos (omit for empty file) | No |
|
|
173
|
+
| `mergeStrategy` | Array merge strategy: `replace`, `append`, `prepend` | No |
|
|
174
|
+
| `createOnly` | If `true`, only create file if it doesn't exist | No |
|
|
175
|
+
| `header` | Comment line(s) at top of YAML files (string or array) | No |
|
|
176
|
+
| `schemaUrl` | Adds `# yaml-language-server: $schema=<url>` to YAML files | No |
|
|
173
177
|
|
|
174
178
|
### Per-Repo Fields
|
|
175
179
|
|
|
@@ -185,6 +189,8 @@ repos: # List of repositories
|
|
|
185
189
|
| `content` | Content overlay merged onto file's base content | No |
|
|
186
190
|
| `override` | If `true`, ignore base content and use only this repo's | No |
|
|
187
191
|
| `createOnly` | Override root-level `createOnly` for this repo | No |
|
|
192
|
+
| `header` | Override root-level `header` for this repo | No |
|
|
193
|
+
| `schemaUrl` | Override root-level `schemaUrl` for this repo | No |
|
|
188
194
|
|
|
189
195
|
**File Exclusion:** Set a file to `false` to exclude it from a specific repo:
|
|
190
196
|
|
|
@@ -398,6 +404,52 @@ repos:
|
|
|
398
404
|
createOnly: false # Override: always sync this file
|
|
399
405
|
```
|
|
400
406
|
|
|
407
|
+
### YAML Comments and Empty Files
|
|
408
|
+
|
|
409
|
+
Add schema directives and comments to YAML files, or create empty files:
|
|
410
|
+
|
|
411
|
+
```yaml
|
|
412
|
+
files:
|
|
413
|
+
# YAML file with schema directive and header comment
|
|
414
|
+
trivy.yaml:
|
|
415
|
+
schemaUrl: https://trivy.dev/latest/docs/references/configuration/config-file/
|
|
416
|
+
header: "Trivy security scanner configuration"
|
|
417
|
+
content:
|
|
418
|
+
exit-code: 1
|
|
419
|
+
scan:
|
|
420
|
+
scanners:
|
|
421
|
+
- vuln
|
|
422
|
+
|
|
423
|
+
# Empty file (content omitted)
|
|
424
|
+
.prettierignore:
|
|
425
|
+
createOnly: true
|
|
426
|
+
# No content = empty file
|
|
427
|
+
|
|
428
|
+
# YAML with multi-line header
|
|
429
|
+
config.yaml:
|
|
430
|
+
header:
|
|
431
|
+
- "Auto-generated configuration"
|
|
432
|
+
- "Do not edit manually"
|
|
433
|
+
content:
|
|
434
|
+
version: 1
|
|
435
|
+
|
|
436
|
+
repos:
|
|
437
|
+
- git: git@github.com:org/repo.git
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Output for trivy.yaml:**
|
|
441
|
+
|
|
442
|
+
```yaml
|
|
443
|
+
# yaml-language-server: $schema=https://trivy.dev/latest/docs/references/configuration/config-file/
|
|
444
|
+
# Trivy security scanner configuration
|
|
445
|
+
exit-code: 1
|
|
446
|
+
scan:
|
|
447
|
+
scanners:
|
|
448
|
+
- vuln
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Note:** `header` and `schemaUrl` only apply to YAML output files (`.yaml`, `.yml`). They are ignored for JSON files.
|
|
452
|
+
|
|
401
453
|
## Supported Git URL Formats
|
|
402
454
|
|
|
403
455
|
### GitHub
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
export type OutputFormat = "json" | "yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Options for content conversion.
|
|
4
|
+
*/
|
|
5
|
+
export interface ConvertOptions {
|
|
6
|
+
header?: string[];
|
|
7
|
+
schemaUrl?: string;
|
|
8
|
+
}
|
|
2
9
|
/**
|
|
3
10
|
* Detects output format from file extension.
|
|
4
11
|
*/
|
|
5
12
|
export declare function detectOutputFormat(fileName: string): OutputFormat;
|
|
6
13
|
/**
|
|
7
14
|
* Converts content object to string in the appropriate format.
|
|
15
|
+
* Handles null content (empty files) and comments (YAML only).
|
|
8
16
|
*/
|
|
9
|
-
export declare function convertContentToString(content: Record<string, unknown
|
|
17
|
+
export declare function convertContentToString(content: Record<string, unknown> | null, fileName: string, options?: ConvertOptions): string;
|
package/dist/config-formatter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { stringify } from "yaml";
|
|
1
|
+
import { Document, stringify } from "yaml";
|
|
2
2
|
/**
|
|
3
3
|
* Detects output format from file extension.
|
|
4
4
|
*/
|
|
@@ -9,13 +9,71 @@ export function detectOutputFormat(fileName) {
|
|
|
9
9
|
}
|
|
10
10
|
return "json";
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds header comment string from header lines and schemaUrl.
|
|
14
|
+
* Returns undefined if no comments to add.
|
|
15
|
+
* Each line gets a space prefix since yaml library adds # directly.
|
|
16
|
+
*/
|
|
17
|
+
function buildHeaderComment(header, schemaUrl) {
|
|
18
|
+
const lines = [];
|
|
19
|
+
// Add yaml-language-server schema directive first (if present)
|
|
20
|
+
if (schemaUrl) {
|
|
21
|
+
lines.push(` yaml-language-server: $schema=${schemaUrl}`);
|
|
22
|
+
}
|
|
23
|
+
// Add custom header lines (with space prefix for proper formatting)
|
|
24
|
+
if (header && header.length > 0) {
|
|
25
|
+
lines.push(...header.map((h) => ` ${h}`));
|
|
26
|
+
}
|
|
27
|
+
if (lines.length === 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
// Join with newlines - the yaml library adds # prefix to each line
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Builds comment-only output for empty YAML files with headers.
|
|
34
|
+
*/
|
|
35
|
+
function buildCommentOnlyYaml(header, schemaUrl) {
|
|
36
|
+
const lines = [];
|
|
37
|
+
// Add yaml-language-server schema directive first (if present)
|
|
38
|
+
if (schemaUrl) {
|
|
39
|
+
lines.push(`# yaml-language-server: $schema=${schemaUrl}`);
|
|
40
|
+
}
|
|
41
|
+
// Add custom header lines
|
|
42
|
+
if (header && header.length > 0) {
|
|
43
|
+
lines.push(...header.map((h) => `# ${h}`));
|
|
44
|
+
}
|
|
45
|
+
if (lines.length === 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
return lines.join("\n") + "\n";
|
|
48
|
+
}
|
|
12
49
|
/**
|
|
13
50
|
* Converts content object to string in the appropriate format.
|
|
51
|
+
* Handles null content (empty files) and comments (YAML only).
|
|
14
52
|
*/
|
|
15
|
-
export function convertContentToString(content, fileName) {
|
|
53
|
+
export function convertContentToString(content, fileName, options) {
|
|
16
54
|
const format = detectOutputFormat(fileName);
|
|
55
|
+
// Handle empty file case
|
|
56
|
+
if (content === null) {
|
|
57
|
+
if (format === "yaml" && options) {
|
|
58
|
+
const commentOnly = buildCommentOnlyYaml(options.header, options.schemaUrl);
|
|
59
|
+
if (commentOnly) {
|
|
60
|
+
return commentOnly;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
17
65
|
if (format === "yaml") {
|
|
18
|
-
|
|
66
|
+
// Use Document API for YAML to support comments
|
|
67
|
+
const doc = new Document(content);
|
|
68
|
+
// Add header comment if present
|
|
69
|
+
if (options) {
|
|
70
|
+
const headerComment = buildHeaderComment(options.header, options.schemaUrl);
|
|
71
|
+
if (headerComment) {
|
|
72
|
+
doc.commentBefore = headerComment;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return stringify(doc, { indent: 2 });
|
|
19
76
|
}
|
|
77
|
+
// JSON format - comments not supported, ignore header/schemaUrl
|
|
20
78
|
return JSON.stringify(content, null, 2);
|
|
21
79
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { deepMerge, stripMergeDirectives, createMergeContext, } from "./merge.js";
|
|
2
2
|
import { interpolateEnvVars } from "./env.js";
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes header to array format.
|
|
5
|
+
*/
|
|
6
|
+
function normalizeHeader(header) {
|
|
7
|
+
if (header === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (typeof header === "string")
|
|
10
|
+
return [header];
|
|
11
|
+
return header;
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* Normalizes raw config into expanded, merged config.
|
|
5
15
|
* Pipeline: expand git arrays -> merge content -> interpolate env vars
|
|
@@ -20,32 +30,51 @@ export function normalizeConfig(raw) {
|
|
|
20
30
|
continue;
|
|
21
31
|
}
|
|
22
32
|
const fileConfig = raw.files[fileName];
|
|
23
|
-
const baseContent = fileConfig.content ?? {};
|
|
24
33
|
const fileStrategy = fileConfig.mergeStrategy ?? "replace";
|
|
25
34
|
// Step 3: Compute merged content for this file
|
|
26
35
|
let mergedContent;
|
|
27
36
|
if (repoOverride?.override) {
|
|
28
|
-
// Override mode: use only repo file content
|
|
29
|
-
|
|
37
|
+
// Override mode: use only repo file content (may be undefined for empty file)
|
|
38
|
+
if (repoOverride.content === undefined) {
|
|
39
|
+
mergedContent = null;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
mergedContent = stripMergeDirectives(structuredClone(repoOverride.content));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (fileConfig.content === undefined) {
|
|
46
|
+
// Root file has no content = empty file (unless repo provides content)
|
|
47
|
+
if (repoOverride?.content) {
|
|
48
|
+
mergedContent = stripMergeDirectives(structuredClone(repoOverride.content));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
mergedContent = null;
|
|
52
|
+
}
|
|
30
53
|
}
|
|
31
54
|
else if (!repoOverride?.content) {
|
|
32
55
|
// No repo override: use file base content as-is
|
|
33
|
-
mergedContent = structuredClone(
|
|
56
|
+
mergedContent = structuredClone(fileConfig.content);
|
|
34
57
|
}
|
|
35
58
|
else {
|
|
36
59
|
// Merge mode: deep merge file base + repo overlay
|
|
37
60
|
const ctx = createMergeContext(fileStrategy);
|
|
38
|
-
mergedContent = deepMerge(structuredClone(
|
|
61
|
+
mergedContent = deepMerge(structuredClone(fileConfig.content), repoOverride.content, ctx);
|
|
39
62
|
mergedContent = stripMergeDirectives(mergedContent);
|
|
40
63
|
}
|
|
41
|
-
// Step 4: Interpolate env vars
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
// Step 4: Interpolate env vars (only if content exists)
|
|
65
|
+
if (mergedContent !== null) {
|
|
66
|
+
mergedContent = interpolateEnvVars(mergedContent, { strict: true });
|
|
67
|
+
}
|
|
68
|
+
// Resolve fields: per-repo overrides root level
|
|
44
69
|
const createOnly = repoOverride?.createOnly ?? fileConfig.createOnly;
|
|
70
|
+
const header = normalizeHeader(repoOverride?.header ?? fileConfig.header);
|
|
71
|
+
const schemaUrl = repoOverride?.schemaUrl ?? fileConfig.schemaUrl;
|
|
45
72
|
files.push({
|
|
46
73
|
fileName,
|
|
47
74
|
content: mergedContent,
|
|
48
75
|
createOnly,
|
|
76
|
+
header,
|
|
77
|
+
schemaUrl,
|
|
49
78
|
});
|
|
50
79
|
}
|
|
51
80
|
expandedRepos.push({
|
package/dist/config-validator.js
CHANGED
|
@@ -33,6 +33,17 @@ export function validateRawConfig(config) {
|
|
|
33
33
|
typeof fileConfig.createOnly !== "boolean") {
|
|
34
34
|
throw new Error(`File '${fileName}' createOnly must be a boolean`);
|
|
35
35
|
}
|
|
36
|
+
if (fileConfig.header !== undefined) {
|
|
37
|
+
if (typeof fileConfig.header !== "string" &&
|
|
38
|
+
(!Array.isArray(fileConfig.header) ||
|
|
39
|
+
!fileConfig.header.every((h) => typeof h === "string"))) {
|
|
40
|
+
throw new Error(`File '${fileName}' header must be a string or array of strings`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (fileConfig.schemaUrl !== undefined &&
|
|
44
|
+
typeof fileConfig.schemaUrl !== "string") {
|
|
45
|
+
throw new Error(`File '${fileName}' schemaUrl must be a string`);
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
if (!config.repos || !Array.isArray(config.repos)) {
|
|
38
49
|
throw new Error("Config missing required field: repos (must be an array)");
|
|
@@ -74,6 +85,17 @@ export function validateRawConfig(config) {
|
|
|
74
85
|
typeof fileOverride.createOnly !== "boolean") {
|
|
75
86
|
throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' createOnly must be a boolean`);
|
|
76
87
|
}
|
|
88
|
+
if (fileOverride.header !== undefined) {
|
|
89
|
+
if (typeof fileOverride.header !== "string" &&
|
|
90
|
+
(!Array.isArray(fileOverride.header) ||
|
|
91
|
+
!fileOverride.header.every((h) => typeof h === "string"))) {
|
|
92
|
+
throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' header must be a string or array of strings`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (fileOverride.schemaUrl !== undefined &&
|
|
96
|
+
typeof fileOverride.schemaUrl !== "string") {
|
|
97
|
+
throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' schemaUrl must be a string`);
|
|
98
|
+
}
|
|
77
99
|
}
|
|
78
100
|
}
|
|
79
101
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type { ArrayMergeStrategy } from "./merge.js";
|
|
2
2
|
export { convertContentToString } from "./config-formatter.js";
|
|
3
3
|
export interface RawFileConfig {
|
|
4
|
-
content
|
|
4
|
+
content?: Record<string, unknown>;
|
|
5
5
|
mergeStrategy?: ArrayMergeStrategy;
|
|
6
6
|
createOnly?: boolean;
|
|
7
|
+
header?: string | string[];
|
|
8
|
+
schemaUrl?: string;
|
|
7
9
|
}
|
|
8
10
|
export interface RawRepoFileOverride {
|
|
9
11
|
content?: Record<string, unknown>;
|
|
10
12
|
override?: boolean;
|
|
11
13
|
createOnly?: boolean;
|
|
14
|
+
header?: string | string[];
|
|
15
|
+
schemaUrl?: string;
|
|
12
16
|
}
|
|
13
17
|
export interface RawRepoConfig {
|
|
14
18
|
git: string | string[];
|
|
@@ -20,8 +24,10 @@ export interface RawConfig {
|
|
|
20
24
|
}
|
|
21
25
|
export interface FileContent {
|
|
22
26
|
fileName: string;
|
|
23
|
-
content: Record<string, unknown
|
|
27
|
+
content: Record<string, unknown> | null;
|
|
24
28
|
createOnly?: boolean;
|
|
29
|
+
header?: string[];
|
|
30
|
+
schemaUrl?: string;
|
|
25
31
|
}
|
|
26
32
|
export interface RepoConfig {
|
|
27
33
|
git: string;
|
|
@@ -47,7 +47,10 @@ export class RepositoryProcessor {
|
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
49
49
|
this.log.info(`Writing ${file.fileName}...`);
|
|
50
|
-
const fileContent = convertContentToString(file.content, file.fileName
|
|
50
|
+
const fileContent = convertContentToString(file.content, file.fileName, {
|
|
51
|
+
header: file.header,
|
|
52
|
+
schemaUrl: file.schemaUrl,
|
|
53
|
+
});
|
|
51
54
|
// Determine action type (create vs update)
|
|
52
55
|
const action = fileExists ? "update" : "create";
|
|
53
56
|
if (dryRun) {
|
package/package.json
CHANGED