@tallyrelease/core 0.5.0-beta.52 → 0.5.0-beta.60

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/dist/builtin.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { calver } from "./calver.js";
1
2
  import { conventionalCommits } from "./conventional.js";
2
3
  import { PLUGIN_API_VERSION } from "./plugin.js";
3
4
  import { semver } from "./semver.js";
@@ -7,6 +8,6 @@ export const CORE_PLUGIN = "core";
7
8
  export const corePlugin = {
8
9
  apiVersion: PLUGIN_API_VERSION,
9
10
  name: CORE_PLUGIN,
10
- schemes: { semver },
11
+ schemes: { semver, calver },
11
12
  intent: [conventionalCommits],
12
13
  };
@@ -0,0 +1,8 @@
1
+ import type { VersionScheme } from "./plugin.js";
2
+ import type { SemVer } from "./semver.js";
3
+ /**
4
+ * Calendar versions `Y.M.N` with SemVer syntax: every bump increments `N`,
5
+ * releasing moves to `Y.M.0` of the planned commit's UTC date when that
6
+ * month is newer.
7
+ */
8
+ export declare const calver: VersionScheme<SemVer>;
package/dist/calver.js ADDED
@@ -0,0 +1,37 @@
1
+ import { semver } from "./semver.js";
2
+ /** Year and month of `ctx.date` in UTC. */
3
+ const monthOf = (ctx) => {
4
+ if (ctx.date === undefined) {
5
+ throw new Error("calver needs the commit date; the git reader does not provide commitDate");
6
+ }
7
+ const date = Date.parse(ctx.date);
8
+ if (Number.isNaN(date)) {
9
+ throw new Error(`calver cannot parse the commit date "${ctx.date}"`);
10
+ }
11
+ const utc = new Date(date);
12
+ return { year: utc.getUTCFullYear(), month: utc.getUTCMonth() + 1 };
13
+ };
14
+ /** `next`, or `Y.M.0` of the commit date when that month is newer. */
15
+ const release = (next, ctx) => {
16
+ const { year, month } = monthOf(ctx);
17
+ const newer = year > next.major || (year === next.major && month > next.minor);
18
+ return newer
19
+ ? { major: year, minor: month, patch: 0, prerelease: [], build: [] }
20
+ : next;
21
+ };
22
+ /**
23
+ * Calendar versions `Y.M.N` with SemVer syntax: every bump increments `N`,
24
+ * releasing moves to `Y.M.0` of the planned commit's UTC date when that
25
+ * month is newer.
26
+ */
27
+ export const calver = {
28
+ ...semver,
29
+ parse: (raw) => {
30
+ const v = semver.parse(raw);
31
+ return v && v.build.length === 0 ? v : undefined;
32
+ },
33
+ initial: () => ({ major: 0, minor: 0, patch: 0, prerelease: [], build: [] }),
34
+ bump: (v) => semver.bump(v, "patch"),
35
+ release,
36
+ prerelease: (next, ctx) => semver.prerelease(release(next, ctx), ctx),
37
+ };
package/dist/codes.js CHANGED
@@ -94,7 +94,7 @@ export const coreCodes = defineCodes({
94
94
  fix: "Fix the options named in the message, or remove them.",
95
95
  },
96
96
  E_PLUGIN_UNKNOWN: {
97
- cause: "`plugins` lists a name that matches no built-in plugin (`node`, `php`, `kustomize`).",
97
+ cause: "`plugins` lists a name that matches no built-in plugin (`node`, `php`, `kustomize`, `dart`).",
98
98
  fix: "Correct the plugin name in `plugins`, or remove the entry.",
99
99
  },
100
100
  E_RELEASE_ITEM_NAME: {
package/dist/facts.js CHANGED
@@ -212,7 +212,12 @@ export const rangeFacts = (workspace, git, opts) => {
212
212
  const changes = [];
213
213
  for (const { plugin, source, ctx: sourceCtx } of intents) {
214
214
  // eslint-disable-next-line no-await-in-loop -- hooks run sequentially in config order
215
- const collected = await runHook(plugin, "intent", () => source.collect(sourceCtx, { from, to: head, commits }));
215
+ const collected = await runHook(plugin, "intent", () => source.collect(sourceCtx, {
216
+ from,
217
+ to: head,
218
+ commits,
219
+ units: workspace.units,
220
+ }));
216
221
  warnUnknown(plugin, collected);
217
222
  changes.push(...collected);
218
223
  }
@@ -234,6 +239,7 @@ export const rangeFacts = (workspace, git, opts) => {
234
239
  /** Runs every `GitReader` and intent source call a plan needs. */
235
240
  export const collectPlanFacts = async (workspace, git, opts) => {
236
241
  const { head } = opts;
242
+ const date = await git.commitDate?.(head);
237
243
  const line = await lineFacts(git, head);
238
244
  const { tags, bases, groups, isAncestor, tagNames } = await tagFacts(workspace, git, line);
239
245
  const { range, all, warnings } = rangeFacts(workspace, git, opts);
@@ -278,5 +284,13 @@ export const collectPlanFacts = async (workspace, git, opts) => {
278
284
  }))),
279
285
  });
280
286
  }
281
- return { head, units, ranges: all(), groups, tagNames, warnings };
287
+ return {
288
+ head,
289
+ ...(date === undefined ? {} : { date }),
290
+ units,
291
+ ranges: all(),
292
+ groups,
293
+ tagNames,
294
+ warnings,
295
+ };
282
296
  };
@@ -0,0 +1,14 @@
1
+ import type { VersionScheme } from "./plugin.js";
2
+ /** A parsed flutter version `x.y.z+build`. */
3
+ export interface FlutterVersion {
4
+ major: number;
5
+ minor: number;
6
+ patch: number;
7
+ build: number;
8
+ }
9
+ /**
10
+ * Flutter `x.y.z+build` scheme. The build number is the larger of the
11
+ * first-parent total plus the owning plugin's `buildOffset` meta and the
12
+ * highest tagged build plus one.
13
+ */
14
+ export declare const flutter: VersionScheme<FlutterVersion>;
@@ -0,0 +1,48 @@
1
+ const RE = /^(\d+)\.(\d+)\.(\d+)\+(\d+)$/u;
2
+ /** A version at the end of a tag name. */
3
+ const TAG_VERSION = /\d+\.\d+\.\d+\+\d+$/u;
4
+ const bumpCore = (v, level) => {
5
+ switch (level) {
6
+ case "major":
7
+ return { ...v, major: v.major + 1, minor: 0, patch: 0 };
8
+ case "minor":
9
+ return { ...v, minor: v.minor + 1, patch: 0 };
10
+ case "patch":
11
+ return { ...v, patch: v.patch + 1 };
12
+ }
13
+ };
14
+ const parse = (raw) => {
15
+ const m = RE.exec(raw);
16
+ return m
17
+ ? {
18
+ major: Number(m[1]),
19
+ minor: Number(m[2]),
20
+ patch: Number(m[3]),
21
+ build: Number(m[4]),
22
+ }
23
+ : undefined;
24
+ };
25
+ const buildNo = (ctx) => {
26
+ const tagged = ctx.tags.map((t) => parse(TAG_VERSION.exec(t)?.[0] ?? "")?.build ?? 0);
27
+ const offset = Number(ctx.unit.meta[ctx.unit.plugin]?.["buildOffset"] ?? 0);
28
+ return Math.max(ctx.firstParent.total + offset, Math.max(0, ...tagged) + 1);
29
+ };
30
+ /**
31
+ * Flutter `x.y.z+build` scheme. The build number is the larger of the
32
+ * first-parent total plus the owning plugin's `buildOffset` meta and the
33
+ * highest tagged build plus one.
34
+ */
35
+ export const flutter = {
36
+ parse,
37
+ format: (v) => `${[v.major, v.minor, v.patch].join(".")}+${String(v.build)}`,
38
+ compare: (a, b) => a.major - b.major ||
39
+ a.minor - b.minor ||
40
+ a.patch - b.patch ||
41
+ a.build - b.build,
42
+ core: ({ major, minor, patch }) => ({ major, minor, patch }),
43
+ isPrerelease: () => false,
44
+ initial: () => ({ major: 0, minor: 1, patch: 0, build: 0 }),
45
+ bump: (v, level) => bumpCore(v, level),
46
+ release: (next, ctx) => ({ ...next, build: buildNo(ctx) }),
47
+ prerelease: (next, ctx) => ({ ...next, build: buildNo(ctx) }),
48
+ };
package/dist/index.d.ts CHANGED
@@ -14,10 +14,12 @@ export type { BumpLevel, Change, Ctx, Edge, EdgeDraft, GitReader, IntentSource,
14
14
  export type { Marker, MarkerRelease } from "./marker.js";
15
15
  export type { ReleaseItem } from "./release-items.js";
16
16
  export type { ReleaseSettings } from "./release-settings.js";
17
+ export type { FlutterVersion } from "./flutter.js";
17
18
  export type { SemVer } from "./semver.js";
18
19
  export type { TagFormat } from "./tags.js";
19
20
  export type { IntentEntry, Workspace, WorkspaceInput } from "./workspace.js";
20
21
  export { computeAffected } from "./affected.js";
22
+ export { calver } from "./calver.js";
21
23
  export { maintenanceCut } from "./branches.js";
22
24
  export { releaseNotes } from "./changelog.js";
23
25
  export { coreCodes, defineCodes } from "./codes.js";
@@ -25,6 +27,7 @@ export { CONFIG_FILE } from "./config.js";
25
27
  export { parseCommit } from "./conventional.js";
26
28
  export { TallyError } from "./errors.js";
27
29
  export { explainUnit } from "./explain.js";
30
+ export { flutter } from "./flutter.js";
28
31
  export { definePlugin, defaultUnitId, PLUGIN_API_VERSION } from "./plugin.js";
29
32
  export { computePlan } from "./plan.js";
30
33
  export { decodeMarker, encodeMarker } from "./marker.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /** @deprecated Unused, will be removed in 1.0. */
2
2
  export const version = "0.0.0";
3
3
  export { computeAffected } from "./affected.js";
4
+ export { calver } from "./calver.js";
4
5
  export { maintenanceCut } from "./branches.js";
5
6
  export { releaseNotes } from "./changelog.js";
6
7
  export { coreCodes, defineCodes } from "./codes.js";
@@ -8,6 +9,7 @@ export { CONFIG_FILE } from "./config.js";
8
9
  export { parseCommit } from "./conventional.js";
9
10
  export { TallyError } from "./errors.js";
10
11
  export { explainUnit } from "./explain.js";
12
+ export { flutter } from "./flutter.js";
11
13
  export { definePlugin, defaultUnitId, PLUGIN_API_VERSION } from "./plugin.js";
12
14
  export { computePlan } from "./plan.js";
13
15
  export { decodeMarker, encodeMarker } from "./marker.js";
@@ -70,6 +70,8 @@ export interface GroupFacts {
70
70
  /** Git and intent data of every released unit, collected before release math. */
71
71
  export interface PlanFacts {
72
72
  readonly head: string;
73
+ /** Committer date of `head`; set when the git reader has `commitDate`. */
74
+ readonly date?: string;
73
75
  /** Released units, sorted by id. */
74
76
  readonly units: readonly UnitFacts[];
75
77
  /** One entry per distinct base tag commit. */
@@ -16,6 +16,10 @@ export interface PlanChange {
16
16
  via?: string;
17
17
  /** Release-As value of this commit for the unit. */
18
18
  releaseAs?: string;
19
+ /** Units the change names; set with `files`. */
20
+ units?: string[];
21
+ /** Files consumed once every unit named by the changes with them releases. */
22
+ files?: string[];
19
23
  }
20
24
  export interface UnitPlan {
21
25
  id: string;
package/dist/plan.js CHANGED
@@ -36,6 +36,9 @@ const planChanges = (intent) => (intent?.changes ?? []).map(({ change, via }) =>
36
36
  ...(change.pr === undefined ? {} : { pr: change.pr }),
37
37
  ...(via === undefined ? {} : { via }),
38
38
  ...(releaseAs === undefined ? {} : { releaseAs }),
39
+ ...(change.files === undefined
40
+ ? {}
41
+ : { units: [...(change.units ?? [])], files: [...change.files] }),
39
42
  };
40
43
  });
41
44
  /**
@@ -61,6 +64,7 @@ const releaseInputs = (workspace, { facts, schemes }, channel) => {
61
64
  range,
62
65
  absorbed: reach(unit.id, workspace.graph.dependencies, released),
63
66
  head: facts.head,
67
+ ...(facts.date === undefined ? {} : { date: facts.date }),
64
68
  channel,
65
69
  },
66
70
  ]));
package/dist/plugin.d.ts CHANGED
@@ -74,6 +74,8 @@ export interface GitReader {
74
74
  * `path`; null if none.
75
75
  */
76
76
  introduced: (path: string, to: string) => MaybePromise<string | null>;
77
+ /** Committer date of `ref`, ISO 8601. */
78
+ commitDate?: (ref: string) => MaybePromise<string>;
77
79
  }
78
80
  export interface TagRef {
79
81
  name: string;
@@ -174,6 +176,8 @@ export interface PreCtx {
174
176
  /** Full sha of the planned commit. */
175
177
  head: string;
176
178
  baseTag: string | null;
179
+ /** Committer date of `head`, ISO 8601; set when the git reader has `commitDate`. */
180
+ date?: string;
177
181
  /** All tags of this unit, reachable or not, scheme-sorted. */
178
182
  tags: readonly string[];
179
183
  firstParent: {
@@ -187,6 +191,8 @@ export interface IntentSource<O = unknown> {
187
191
  from: string | null;
188
192
  to: string;
189
193
  commits: readonly RawCommit[];
194
+ /** All units of the workspace. */
195
+ units: readonly Unit[];
190
196
  }) => MaybePromise<Change[]>;
191
197
  }
192
198
  /** A `Release-As` value; `unit` is set by the qualified form `<unit>@<version>`. */
@@ -209,6 +215,11 @@ export interface Change {
209
215
  releaseAs?: ReleaseAsValue[];
210
216
  pr?: number;
211
217
  date?: string;
218
+ /**
219
+ * Files consumed when the change is released: deleted in the release
220
+ * commit that releases every unit named by the changes with the file.
221
+ */
222
+ files?: string[];
212
223
  }
213
224
  /** Throws `E_PLUGIN_API` or `E_PLUGIN_NAME` for an unsupported plugin. */
214
225
  export declare const assertPlugin: (plugin: {
package/dist/release.d.ts CHANGED
@@ -42,6 +42,8 @@ export interface ReleaseInput<V> {
42
42
  /** Unreleased units absorbed by the unit (`reach`). */
43
43
  readonly absorbed: ReadonlySet<string>;
44
44
  readonly head: string;
45
+ /** Committer date of `head`. */
46
+ readonly date?: string;
45
47
  readonly channel: string | null;
46
48
  }
47
49
  /**
package/dist/release.js CHANGED
@@ -43,8 +43,12 @@ export const releaseUnit = (input, intent, warnings) => {
43
43
  const from = fromOf(scheme, facts);
44
44
  const initial = initialOf(unit, scheme);
45
45
  const bumped = bumpedOf(scheme, bump, { from, initial });
46
+ const ctx = ctxOf(unit, input, {
47
+ ...facts,
48
+ tags: facts.tags.map((t) => t.name),
49
+ });
46
50
  if (from === undefined && releaseAs === undefined && bumped !== undefined) {
47
- warnings.push(...initialBelowPrerelease(scheme, initial, facts));
51
+ warnings.push(...initialBelowPrerelease(scheme, scheme.release(initial, ctx), facts));
48
52
  }
49
53
  const raw = releaseAs ?? bumped;
50
54
  return {
@@ -59,15 +63,7 @@ export const releaseUnit = (input, intent, warnings) => {
59
63
  : from === undefined
60
64
  ? "initial"
61
65
  : "bump",
62
- ...render({
63
- scheme,
64
- raw,
65
- ctx: ctxOf(unit, input.head, {
66
- ...facts,
67
- tags: facts.tags.map((t) => t.name),
68
- }),
69
- channel: input.channel,
70
- }),
66
+ ...render({ scheme, raw, ctx, channel: input.channel }),
71
67
  };
72
68
  };
73
69
  /**
@@ -82,18 +78,13 @@ export const releaseGroup = (group, members, warnings) => {
82
78
  if (!first) {
83
79
  throw new TypeError("a group has at least one member");
84
80
  }
85
- const { scheme, facts, channel, head } = first.input;
81
+ const { scheme, facts, channel } = first.input;
86
82
  const max = (a, b) => a === undefined || (b !== undefined && scheme.compare(b, a) > 0) ? b : a;
87
83
  const from = fromOf(scheme, facts);
88
84
  const bump = members.reduce((level, m) => maxLevel(level, m.bump), null);
89
85
  const releaseAs = members.reduce((value, m) => max(value, m.releaseAs?.version), undefined);
90
86
  const initial = members.reduce((value, m) => max(value, initialOf(m.input.unit, scheme)) ?? value, initialOf(first.input.unit, scheme));
91
87
  const bumped = bumpedOf(scheme, bump, { from, initial });
92
- if (from === undefined && releaseAs === undefined && bumped !== undefined) {
93
- for (const m of members) {
94
- warnings.push(...initialBelowPrerelease(scheme, initial, m.input.facts));
95
- }
96
- }
97
88
  const tags = members
98
89
  .flatMap((m) => m.input.facts.tags)
99
90
  .flatMap((t) => {
@@ -102,6 +93,18 @@ export const releaseGroup = (group, members, warnings) => {
102
93
  })
103
94
  .sort((a, b) => scheme.compare(a.version, b.version) || compare(a.name, b.name))
104
95
  .map((t) => t.name);
96
+ const ctx = ctxOf(first.input.unit, first.input, {
97
+ baseTag: group.baseTag,
98
+ tags,
99
+ sinceLineBase: group.sinceLineBase,
100
+ total: Math.max(...members.map((m) => m.input.facts.total)),
101
+ });
102
+ if (from === undefined && releaseAs === undefined && bumped !== undefined) {
103
+ const released = scheme.release(initial, ctx);
104
+ for (const m of members) {
105
+ warnings.push(...initialBelowPrerelease(scheme, released, m.input.facts));
106
+ }
107
+ }
105
108
  const raw = max(bumped, releaseAs);
106
109
  const byReleaseAs = releaseAs !== undefined &&
107
110
  (bumped === undefined || scheme.compare(releaseAs, bumped) > 0);
@@ -116,16 +119,6 @@ export const releaseGroup = (group, members, warnings) => {
116
119
  : from === undefined
117
120
  ? "initial"
118
121
  : "bump",
119
- ...render({
120
- scheme,
121
- raw,
122
- ctx: ctxOf(first.input.unit, head, {
123
- baseTag: group.baseTag,
124
- tags,
125
- sinceLineBase: group.sinceLineBase,
126
- total: Math.max(...members.map((m) => m.input.facts.total)),
127
- }),
128
- channel,
129
- }),
122
+ ...render({ scheme, raw, ctx, channel }),
130
123
  };
131
124
  };
@@ -16,6 +16,9 @@ export declare const gitReaderConformance: (makeReader: ReaderFactory) => void;
16
16
  /**
17
17
  * Shared `VersionScheme` spec. Versions are derived from `initial()`:
18
18
  * `parse`/`format` round-trip, `compare` order, `bump` monotonicity per level,
19
- * and `release`/`prerelease` keeping the core triple.
19
+ * and `release`/`prerelease` keeping the core triple. With `dateDriven` the
20
+ * level order, the core reset and the core-keeping checks are skipped.
20
21
  */
21
- export declare const schemeConformance: <V>(scheme: VersionScheme<V>) => void;
22
+ export declare const schemeConformance: <V>(scheme: VersionScheme<V>, { dateDriven }?: {
23
+ dateDriven?: boolean;
24
+ }) => void;
@@ -133,6 +133,18 @@ export const gitReaderConformance = (makeReader) => {
133
133
  expect(Date.parse(byId("a")?.date ?? "")).toBe(Date.parse("2024-01-01T10:00:00Z"));
134
134
  expect(Date.parse(byId("e")?.date ?? "")).toBe(Date.parse("2024-01-02T12:30:00Z"));
135
135
  });
136
+ it("reads the committer date of a commit when supported", async () => {
137
+ if (git.commitDate === undefined) {
138
+ return;
139
+ }
140
+ const commits = await git.log(null, sha("e"));
141
+ for (const id of ["a", "e"]) {
142
+ // eslint-disable-next-line no-await-in-loop -- one commit at a time
143
+ const date = await git.commitDate(sha(id));
144
+ expect(date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/u);
145
+ expect(date).toBe(commits.find((c) => c.sha === sha(id))?.date);
146
+ }
147
+ });
136
148
  it("lists every commit before its parents", async () => {
137
149
  const commits = await git.log(null, sha("e"));
138
150
  const index = new Map(commits.map((c, i) => [c.sha, i]));
@@ -219,6 +231,7 @@ const PRE_CTX = {
219
231
  meta: {},
220
232
  },
221
233
  head: "0".repeat(40),
234
+ date: "2024-01-01T00:00:00Z",
222
235
  baseTag: null,
223
236
  tags: [],
224
237
  firstParent: { sinceLineBase: 3, total: 7 },
@@ -227,9 +240,10 @@ const PRE_CTX = {
227
240
  /**
228
241
  * Shared `VersionScheme` spec. Versions are derived from `initial()`:
229
242
  * `parse`/`format` round-trip, `compare` order, `bump` monotonicity per level,
230
- * and `release`/`prerelease` keeping the core triple.
243
+ * and `release`/`prerelease` keeping the core triple. With `dateDriven` the
244
+ * level order, the core reset and the core-keeping checks are skipped.
231
245
  */
232
- export const schemeConformance = (scheme) => {
246
+ export const schemeConformance = (scheme, { dateDriven = false } = {}) => {
233
247
  describe("VersionScheme conformance", () => {
234
248
  const initial = scheme.initial();
235
249
  const bumped = (level) => scheme.bump(initial, level);
@@ -258,35 +272,41 @@ export const schemeConformance = (scheme) => {
258
272
  expect(scheme.compare(next, initial)).toBeGreaterThan(0);
259
273
  expect(scheme.isPrerelease(next)).toBe(false);
260
274
  });
261
- it("orders bump levels major > minor > patch", () => {
262
- const [major, minor, patch] = LEVELS.map(bumped);
263
- expect(scheme.compare(major, minor)).toBeGreaterThan(0);
264
- expect(scheme.compare(minor, patch)).toBeGreaterThan(0);
265
- });
266
- it("resets lower core parts on a bump", () => {
267
- const { major, minor, patch } = scheme.core(initial);
268
- expect(scheme.core(bumped("major"))).toEqual({
269
- major: major + 1,
270
- minor: 0,
271
- patch: 0,
275
+ // A date-driven scheme takes the core from the commit date.
276
+ if (!dateDriven) {
277
+ it("orders bump levels major > minor > patch", () => {
278
+ const [major, minor, patch] = LEVELS.map(bumped);
279
+ expect(scheme.compare(major, minor)).toBeGreaterThan(0);
280
+ expect(scheme.compare(minor, patch)).toBeGreaterThan(0);
272
281
  });
273
- expect(scheme.core(bumped("minor"))).toEqual({
274
- major,
275
- minor: minor + 1,
276
- patch: 0,
282
+ it("resets lower core parts on a bump", () => {
283
+ const { major, minor, patch } = scheme.core(initial);
284
+ expect(scheme.core(bumped("major"))).toEqual({
285
+ major: major + 1,
286
+ minor: 0,
287
+ patch: 0,
288
+ });
289
+ expect(scheme.core(bumped("minor"))).toEqual({
290
+ major,
291
+ minor: minor + 1,
292
+ patch: 0,
293
+ });
294
+ expect(scheme.core(bumped("patch"))).toEqual({
295
+ major,
296
+ minor,
297
+ patch: patch + 1,
298
+ });
277
299
  });
278
- expect(scheme.core(bumped("patch"))).toEqual({
279
- major,
280
- minor,
281
- patch: patch + 1,
300
+ it("keeps the core when releasing or prereleasing", () => {
301
+ const next = bumped("minor");
302
+ expect(scheme.core(scheme.release(next, PRE_CTX))).toEqual(scheme.core(next));
303
+ expect(scheme.core(scheme.prerelease(next, PRE_CTX))).toEqual(scheme.core(next));
282
304
  });
283
- });
284
- it("keeps the core when releasing or prereleasing", () => {
305
+ }
306
+ it("orders a prerelease at or below its release", () => {
285
307
  const next = bumped("minor");
286
308
  const release = scheme.release(next, PRE_CTX);
287
309
  const pre = scheme.prerelease(next, PRE_CTX);
288
- expect(scheme.core(release)).toEqual(scheme.core(next));
289
- expect(scheme.core(pre)).toEqual(scheme.core(next));
290
310
  expect(scheme.isPrerelease(release)).toBe(false);
291
311
  expect(scheme.compare(pre, release)).toBeLessThanOrEqual(0);
292
312
  expect(scheme.parse(scheme.format(pre))).toBeDefined();
@@ -137,6 +137,7 @@ export const memoryGit = (dag) => {
137
137
  .find((n) => changed(n.parents[0] === undefined ? new Map() : node(n.parents[0]).tree, n.tree).some(under));
138
138
  return intro?.id ?? null;
139
139
  },
140
+ commitDate: (ref) => node(ref).date,
140
141
  tree: (ref) => Object.fromEntries(node(ref).tree),
141
142
  parents: (ref) => [...node(ref).parents],
142
143
  };
@@ -15,7 +15,8 @@ export declare const versionFileClaims: (workspace: Workspace) => Promise<Readon
15
15
  * Files of a release of `items` (stable `next`): plugin version files,
16
16
  * templates, markers and `<root>/CHANGELOG.md` per unit, read through `fs`; the
17
17
  * owning plugin's `changelog` hook may replace a unit's section. Returns
18
- * only files whose content changes, and each unit's changelog section;
18
+ * only files whose content changes, the consumed files of changes whose
19
+ * named units all release in `items`, and each unit's changelog section;
19
20
  * checks claims first. Templates skip first releases and warn
20
21
  * `W_VERSION_FILE_NO_MATCH` without any match, markers warn
21
22
  * `W_MARKER_NO_MATCH`; a missing version file is
@@ -30,6 +31,8 @@ export declare const releaseFiles: (workspace: Workspace, plan: Plan, opts: {
30
31
  date: string;
31
32
  }) => Promise<{
32
33
  files: Map<string, string>;
34
+ /** Files consumed by released changes, deleted by the release. */
35
+ deletes: Set<string>;
33
36
  /** Changelog section of each unit, by unit id. */
34
37
  sections: Map<string, string>;
35
38
  warnings: Warning[];
@@ -259,6 +259,24 @@ const changelogStep = async (staged, step) => {
259
259
  }
260
260
  return section;
261
261
  };
262
+ /**
263
+ * Readable files of the plan's changes where every unit named by a change
264
+ * with the file is in `released`; `written` files are kept.
265
+ */
266
+ const consumedFiles = (plan, released, { fs, written }) => {
267
+ const named = new Map();
268
+ for (const { files = [], units = [] } of plan.units.flatMap((u) => u.changes)) {
269
+ for (const path of files) {
270
+ named.set(path, [...(named.get(path) ?? []), ...units]);
271
+ }
272
+ }
273
+ return new Set([...named]
274
+ .filter(([path, units]) => units.length > 0 &&
275
+ units.every((id) => released.has(id)) &&
276
+ !written.has(path) &&
277
+ fs.read(path) !== undefined)
278
+ .map(([path]) => path));
279
+ };
262
280
  /** Checks template files and patterns (`E_CONFIG`); runs no plugin hook. */
263
281
  export const validateTemplates = (workspace) => {
264
282
  for (const [id, templates] of workspace.release.templates) {
@@ -310,7 +328,8 @@ export const versionFileClaims = async (workspace) => {
310
328
  * Files of a release of `items` (stable `next`): plugin version files,
311
329
  * templates, markers and `<root>/CHANGELOG.md` per unit, read through `fs`; the
312
330
  * owning plugin's `changelog` hook may replace a unit's section. Returns
313
- * only files whose content changes, and each unit's changelog section;
331
+ * only files whose content changes, the consumed files of changes whose
332
+ * named units all release in `items`, and each unit's changelog section;
314
333
  * checks claims first. Templates skip first releases and warn
315
334
  * `W_VERSION_FILE_NO_MATCH` without any match, markers warn
316
335
  * `W_MARKER_NO_MATCH`; a missing version file is
@@ -371,6 +390,10 @@ export const releaseFiles = async (workspace, plan, opts) => {
371
390
  }
372
391
  return {
373
392
  files: new Map([...files].filter(([path, c]) => fs.read(path) !== c)),
393
+ deletes: consumedFiles(plan, new Set(versions.keys()), {
394
+ fs,
395
+ written: files,
396
+ }),
374
397
  sections,
375
398
  warnings,
376
399
  };
@@ -14,7 +14,10 @@ export declare const bumpedOf: <V>(scheme: VersionScheme<V>, bump: BumpLevel | n
14
14
  from: From<V> | undefined;
15
15
  initial: V;
16
16
  }) => V | undefined;
17
- export declare const ctxOf: (unit: Unit, head: string, at: {
17
+ export declare const ctxOf: (unit: Unit, { head, date }: {
18
+ head: string;
19
+ date?: string;
20
+ }, at: {
18
21
  baseTag: {
19
22
  name: string;
20
23
  } | null;
@@ -21,9 +21,10 @@ export const bumpedOf = (scheme, bump, { from, initial }) => bump === null
21
21
  : from === undefined
22
22
  ? initial
23
23
  : scheme.bump(from.version, bump);
24
- export const ctxOf = (unit, head, at) => ({
24
+ export const ctxOf = (unit, { head, date }, at) => ({
25
25
  unit,
26
26
  head,
27
+ ...(date === undefined ? {} : { date }),
27
28
  baseTag: at.baseTag?.name ?? null,
28
29
  tags: at.tags,
29
30
  firstParent: { sinceLineBase: at.sinceLineBase, total: at.total },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyrelease/core",
3
- "version": "0.5.0-beta.52",
3
+ "version": "0.5.0-beta.60",
4
4
  "description": "Pure planning core of tallyrelease",
5
5
  "repository": {
6
6
  "type": "git",