@aspruyt/xfg 6.0.2 → 6.1.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.
@@ -42,6 +42,11 @@ function buildCommentOnlyYaml(header, schemaUrl) {
42
42
  return undefined;
43
43
  return lines.join("\n") + "\n";
44
44
  }
45
+ function buildJson5HeaderComment(header) {
46
+ if (!header || header.length === 0)
47
+ return undefined;
48
+ return header.map((h) => `// ${h}`).join("\n") + "\n";
49
+ }
45
50
  /**
46
51
  * Converts content to string in the appropriate format.
47
52
  * Handles null content (empty files), text content (string/string[]), and object content (JSON/YAML).
@@ -55,6 +60,12 @@ export function convertContentToString(content, fileName, options) {
55
60
  return commentOnly;
56
61
  }
57
62
  }
63
+ if (format === "json5" && options) {
64
+ const commentOnly = buildJson5HeaderComment(options.header);
65
+ if (commentOnly) {
66
+ return commentOnly;
67
+ }
68
+ }
58
69
  return "";
59
70
  }
60
71
  if (typeof content === "string") {
@@ -94,6 +105,11 @@ export function convertContentToString(content, fileName, options) {
94
105
  defaultKeyType: "PLAIN",
95
106
  });
96
107
  }
97
- // JSON and JSON5 both use standard JSON.stringify (valid JSON5 superset)
108
+ if (format === "json5" && options) {
109
+ const headerComment = buildJson5HeaderComment(options.header);
110
+ if (headerComment) {
111
+ return headerComment + JSON.stringify(content, null, 2) + "\n";
112
+ }
113
+ }
98
114
  return JSON.stringify(content, null, 2) + "\n";
99
115
  }
@@ -485,6 +485,17 @@ export function normalizeConfig(raw, env) {
485
485
  effectiveSettings = merged.settings;
486
486
  }
487
487
  const fileNames = Object.keys(effectiveRootFiles);
488
+ // Collect repo-only file names (defined at repo level but not in root/groups)
489
+ const repoOnlyFileNames = [];
490
+ if (rawRepo.files) {
491
+ for (const name of Object.keys(rawRepo.files)) {
492
+ if (name === "inherit")
493
+ continue;
494
+ if (!effectiveRootFiles[name]) {
495
+ repoOnlyFileNames.push(name);
496
+ }
497
+ }
498
+ }
488
499
  for (const gitUrl of gitUrls) {
489
500
  const files = [];
490
501
  const inheritFiles = shouldInherit(rawRepo.files);
@@ -496,6 +507,15 @@ export function normalizeConfig(raw, env) {
496
507
  if (entry)
497
508
  files.push(entry);
498
509
  }
510
+ // Process repo-only files (standalone definitions not in root/groups)
511
+ for (const fileName of repoOnlyFileNames) {
512
+ const repoOverride = rawRepo.files[fileName];
513
+ if (repoOverride === false)
514
+ continue;
515
+ const entry = resolveFileEntry(fileName, {}, repoOverride, true, raw.deleteOrphaned, env);
516
+ if (entry)
517
+ files.push(entry);
518
+ }
499
519
  // Merge PR options: per-repo overrides effective (root + groups)
500
520
  const prOptions = mergePROptions(effectivePROptions, rawRepo.prOptions);
501
521
  // Merge settings: per-repo deep merges with effective (root + groups)
@@ -599,7 +599,17 @@ function validateRepoFiles(config, repo, index, repoLabel) {
599
599
  continue;
600
600
  }
601
601
  if (!knownFiles.has(fileName)) {
602
- throw new ValidationError(`Repo at index ${index} references undefined file '${fileName}'. File must be defined in root 'files' object or in a referenced group.`);
602
+ const fileEntry = repo.files[fileName];
603
+ const isStandaloneDefinition = fileEntry != null &&
604
+ fileEntry !== false &&
605
+ typeof fileEntry === "object" &&
606
+ "content" in fileEntry &&
607
+ fileEntry.content !== undefined &&
608
+ fileEntry.content !== null;
609
+ if (!isStandaloneDefinition) {
610
+ throw new ValidationError(`Repo at index ${index} references undefined file '${fileName}'. File must be defined in root 'files' object or in a referenced group, or provide content inline.`);
611
+ }
612
+ validateFileName(fileName);
603
613
  }
604
614
  const fileOverride = repo.files[fileName];
605
615
  if (fileOverride === false) {
@@ -199,6 +199,11 @@ function matchByIndex(current, desired) {
199
199
  for (let i = 0; i < Math.min(current.length, desired.length); i++) {
200
200
  result.push(projectToDesiredShape(current[i], desired[i]));
201
201
  }
202
+ // Append extra current items so deepEqual detects length mismatch (removals).
203
+ // Mirrors matchByKey behavior added for #549.
204
+ for (let i = desired.length; i < current.length; i++) {
205
+ result.push(current[i]);
206
+ }
202
207
  return result;
203
208
  }
204
209
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspruyt/xfg",
3
- "version": "6.0.2",
3
+ "version": "6.1.0",
4
4
  "description": "Manage files, settings, and repositories across GitHub, Azure DevOps, and GitLab — declaratively, from a single YAML config",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",