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

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
@@ -234,6 +234,7 @@ export const rangeFacts = (workspace, git, opts) => {
234
234
  /** Runs every `GitReader` and intent source call a plan needs. */
235
235
  export const collectPlanFacts = async (workspace, git, opts) => {
236
236
  const { head } = opts;
237
+ const date = await git.commitDate?.(head);
237
238
  const line = await lineFacts(git, head);
238
239
  const { tags, bases, groups, isAncestor, tagNames } = await tagFacts(workspace, git, line);
239
240
  const { range, all, warnings } = rangeFacts(workspace, git, opts);
@@ -278,5 +279,13 @@ export const collectPlanFacts = async (workspace, git, opts) => {
278
279
  }))),
279
280
  });
280
281
  }
281
- return { head, units, ranges: all(), groups, tagNames, warnings };
282
+ return {
283
+ head,
284
+ ...(date === undefined ? {} : { date }),
285
+ units,
286
+ ranges: all(),
287
+ groups,
288
+ tagNames,
289
+ warnings,
290
+ };
282
291
  };
@@ -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. */
package/dist/plan.js CHANGED
@@ -61,6 +61,7 @@ const releaseInputs = (workspace, { facts, schemes }, channel) => {
61
61
  range,
62
62
  absorbed: reach(unit.id, workspace.graph.dependencies, released),
63
63
  head: facts.head,
64
+ ...(facts.date === undefined ? {} : { date: facts.date }),
64
65
  channel,
65
66
  },
66
67
  ]));
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: {
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
  };
@@ -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.56",
4
4
  "description": "Pure planning core of tallyrelease",
5
5
  "repository": {
6
6
  "type": "git",