rman 2.9.0 → 2.10.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.
@@ -184,6 +184,21 @@ declare const config: {
184
184
  * from commit type to section heading, and an option says `type: 'string'`.
185
185
  */
186
186
  export interface ChangelogExtraKeys {
187
+ /**
188
+ * Where each **named** group's changelog is written under `groupBy: 'group'`, relative to the
189
+ * repository root - `{ core: 'packages/core/CHANGELOG.md', dialects: 'docs/dialects.md' }`.
190
+ * Read off the repository root only.
191
+ *
192
+ * A group not listed keeps the default rule, `CHANGELOG-<group>.md` beside the default file; the
193
+ * default group's own file is `changelog.filePath`. A key naming no group, a path outside the
194
+ * repository, and two groups resolving to one file are all refused before anything is written.
195
+ * An explicit `--file-path` overrides the whole map for that run.
196
+ *
197
+ * **Move an existing file with `git mv`, not by changing the path here.** A file's
198
+ * `documented-up-to` marker is what the next `--write` starts from; at a new path there is none,
199
+ * and the write reads the group's entire history.
200
+ */
201
+ groupFiles?: ConfigValue<Record<string, string>>;
187
202
  /**
188
203
  * The heading each Conventional Commits type is listed under - `{ feat: 'New Features', dev:
189
204
  * 'Development Changes' }`. Per-package cascaded.
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.9.0';
1
+ export const version = '2.10.0';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.9.0",
4
+ "version": "2.10.0",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
@@ -600,33 +600,139 @@ function resolveFilePath(pkg, optionsFilePath) {
600
600
  */
601
601
  function partitionTargets(repository, targets, options) {
602
602
  const grouping = changelogGroupBy(repository, options);
603
- const byKey = new Map();
604
- for (const pkg of targets) {
605
- const key = grouping === 'group' ? groupKeyOf(pkg) : `solo:${pkg.name}`;
606
- const members = byKey.get(key);
607
- if (members)
608
- members.push(pkg);
609
- else
610
- byKey.set(key, [pkg]);
611
- }
603
+ const groupFiles = grouping === 'group' ? readGroupFiles(repository, options) : {};
604
+ /** Checked against the **whole** repository, not against `targets`: a collision is a fact about
605
+ * the configuration, and a `--scope` that happens to leave one of the two groups out must not be
606
+ * what lets it through. */
607
+ if (grouping === 'group')
608
+ assertGroupFiles(repository, groupFiles, options);
612
609
  const repositoryLabel = `${path.basename(repository.dirname)} repository`;
613
- return [...byKey].map(([key, members]) => {
610
+ return [...groupTargets(targets, grouping)].map(([key, members]) => {
614
611
  /** `targets` starts at the root, so a group holding it has it first - which is what makes the
615
612
  * root the `home` of the default group without a second rule saying so. */
616
613
  const home = members[0];
617
614
  const grouped = !key.startsWith('solo:');
618
- const named = key.startsWith('named:') ? key.slice('named:'.length) : undefined;
619
- const filePkg = grouped ? repository.rootPackage : home;
615
+ const named = namedGroupOf(key);
620
616
  return {
621
617
  home,
622
- filePkg,
618
+ filePkg: grouped ? repository.rootPackage : home,
623
619
  grouped,
624
620
  members: new Set(members),
625
621
  label: named ?? (home === repository.rootPackage ? repositoryLabel : home.name),
626
- file: path.join(filePkg.dirname, suffixFileName(resolveFilePath(home, options.filePath), named)),
622
+ file: groupFile(repository, key, home, groupFiles, options),
627
623
  };
628
624
  });
629
625
  }
626
+ /** The packages cut into what each file is about - by `.rmanrc group` under `groupBy: 'group'`, one
627
+ * package each otherwise. Insertion order, so the first member of each group is the first of
628
+ * `packages` to belong to it. */
629
+ function groupTargets(packages, grouping) {
630
+ const byKey = new Map();
631
+ for (const pkg of packages) {
632
+ const key = grouping === 'group' ? groupKeyOf(pkg) : `solo:${pkg.name}`;
633
+ const members = byKey.get(key);
634
+ if (members)
635
+ members.push(pkg);
636
+ else
637
+ byKey.set(key, [pkg]);
638
+ }
639
+ return byKey;
640
+ }
641
+ /** `named:core` -> `core`; `undefined` for the default group and for a package that is its own. */
642
+ function namedGroupOf(key) {
643
+ return key.startsWith('named:') ? key.slice('named:'.length) : undefined;
644
+ }
645
+ /**
646
+ * Where one group's changelog is written, absolute.
647
+ *
648
+ * In order: a named group listed in `changelog.groupFiles` goes where it says, relative to the
649
+ * repository root; any other named group takes the default file's name with its own suffixed
650
+ * (`CHANGELOG-core.md`), at the root; the default group takes the root's `changelog.filePath`; and a
651
+ * package that is a group of itself takes its own, in its own directory.
652
+ *
653
+ * **An explicit `--file-path` beats `groupFiles`**, as it already beats `changelog.filePath`: it
654
+ * names this run's file, and a run asking for one shape of name should not find half its groups
655
+ * somewhere a config put them. Named groups are still suffixed under it, so two cannot collide.
656
+ */
657
+ function groupFile(repository, key, home, groupFiles, options) {
658
+ if (key.startsWith('solo:'))
659
+ return path.join(home.dirname, resolveFilePath(home, options.filePath));
660
+ const named = namedGroupOf(key);
661
+ const listed = named && !options.filePath ? groupFiles[named] : undefined;
662
+ if (listed)
663
+ return path.resolve(repository.dirname, listed);
664
+ return path.join(repository.dirname, suffixFileName(resolveFilePath(home, options.filePath), named));
665
+ }
666
+ /**
667
+ * `.rmanrc changelog.groupFiles`, off the repository root and nowhere else - a map from a named
668
+ * group to the file its changelog is written to, relative to the repository root.
669
+ *
670
+ * **The root's for the reason `groupBy` is the root's**: which file a group shares is a layout, and
671
+ * cascaded per package two members of one group could disagree about it, which has no answer.
672
+ */
673
+ function readGroupFiles(repository, options) {
674
+ /** `--file-path` takes the whole decision - see `groupFile` - so the map is not even read. A
675
+ * mistake in it is still a mistake, but not one this run acts on. */
676
+ if (options.filePath)
677
+ return {};
678
+ const declared = repository.rootPackage.config?.changelog?.groupFiles;
679
+ if (declared === undefined || declared === null)
680
+ return {};
681
+ if (typeof declared !== 'object' || Array.isArray(declared)) {
682
+ throw new Error(`.rmanrc "changelog.groupFiles" must map a group name to a file path, ` +
683
+ `e.g. { core: "packages/core/CHANGELOG.md" }.`);
684
+ }
685
+ for (const [group, file] of Object.entries(declared)) {
686
+ if (typeof file !== 'string' || !file) {
687
+ throw new Error(`.rmanrc "changelog.groupFiles.${group}" must be a file path, relative to the repository root.`);
688
+ }
689
+ }
690
+ return declared;
691
+ }
692
+ /**
693
+ * Refuses a `changelog.groupFiles` that would write somewhere wrong - before anything is written.
694
+ *
695
+ * Three ways, and each is silent otherwise:
696
+ *
697
+ * - **A key naming no group.** rman drops an unknown config key without a word, so a typo here would
698
+ * leave the group on the default rule and nothing would say so. Here the groups are known, so the
699
+ * key can be checked against them.
700
+ * - **A path leaving the repository.** `version --changelog` commits the file with the release; a
701
+ * file outside the working tree is one it cannot add.
702
+ * - **Two groups sharing one file**, including the default group and any package that is a group of
703
+ * itself. The `rman:documented-up-to` marker is one per file, so two groups writing into it would
704
+ * each read the other's marker as its own boundary - entries missing or written twice, with
705
+ * nothing failing. The default rule cannot produce a collision; an explicit map can, and an
706
+ * unlisted group's suffixed name can land on a listed one's path.
707
+ */
708
+ function assertGroupFiles(repository, groupFiles, options) {
709
+ const all = groupTargets([repository.rootPackage, ...repository.packages], 'group');
710
+ const named = [...all.keys()].map(namedGroupOf).filter((name) => !!name);
711
+ for (const [group, file] of Object.entries(groupFiles)) {
712
+ if (!named.includes(group)) {
713
+ throw new Error(`.rmanrc "changelog.groupFiles" names a group "${group}" that no package belongs to. ` +
714
+ (named.length ? `Named groups here: ${named.join(', ')}.` : 'This repository has no named groups.'));
715
+ }
716
+ const rel = path.relative(repository.dirname, path.resolve(repository.dirname, file));
717
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
718
+ throw new Error(`.rmanrc "changelog.groupFiles.${group}" points outside the repository (${file}). ` +
719
+ `The file is committed with the release, so it has to be inside the working tree.`);
720
+ }
721
+ }
722
+ const owner = new Map();
723
+ for (const [key, members] of all) {
724
+ const file = groupFile(repository, key, members[0], groupFiles, options);
725
+ const label = namedGroupOf(key) ?? (key === 'default' ? 'the default group' : members[0].name);
726
+ const other = owner.get(file);
727
+ if (other) {
728
+ throw new Error(`Two changelogs would be written to one file: ${other} and ${label} both resolve to ` +
729
+ `${path.relative(repository.dirname, file)}.\n` +
730
+ ` Each file carries one "documented up to" marker, so two groups sharing it would read each ` +
731
+ `other's boundary. Give each its own path in .rmanrc "changelog.groupFiles".`);
732
+ }
733
+ owner.set(file, label);
734
+ }
735
+ }
630
736
  /** `CHANGELOG.md` + `core` -> `CHANGELOG-core.md`, keeping whatever directory and extension
631
737
  * `changelog.filePath` chose (`docs/HISTORY.md` -> `docs/HISTORY-core.md`). */
632
738
  function suffixFileName(filePath, suffix) {