@tallyrelease/core 0.5.0-beta.31 → 0.5.0-beta.33

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/facts.d.ts CHANGED
@@ -1,10 +1,45 @@
1
- import type { PlanFacts } from "./model/facts-types.js";
2
- import type { GitReader, ReadonlyFs } from "./plugin.js";
1
+ import type { Warning } from "./errors.js";
2
+ import type { GroupFacts, PlanFacts, RangeFacts } from "./model/facts-types.js";
3
+ import type { GitReader, ReadonlyFs, VersionScheme } from "./plugin.js";
4
+ import type { UnitTag } from "./tags.js";
3
5
  import type { Workspace } from "./workspace.js";
6
+ type LineFacts = Awaited<ReturnType<typeof lineFacts>>;
7
+ interface UnitBase {
8
+ scheme: VersionScheme<any>;
9
+ baseTag: UnitTag | null;
10
+ lineBaseTag: UnitTag | null;
11
+ introduced: string | null;
12
+ sinceLineBase: number;
13
+ }
4
14
  export interface FactsOptions {
5
15
  head: string;
6
16
  fs: ReadonlyFs;
7
17
  debug?: (message: string) => void;
8
18
  }
19
+ /** First-parent commit counters of `head`, memoized per commit. */
20
+ export declare const lineFacts: (git: GitReader, head: string) => Promise<{
21
+ head: string;
22
+ total: number;
23
+ countSince: (sha: string) => Promise<number>;
24
+ lineCount: (root: string, lineBaseTag: UnitTag | null) => Promise<{
25
+ introduced: string | null;
26
+ sinceLineBase: number;
27
+ }>;
28
+ }>;
29
+ /** Tags of every unit and fixed group, relative to `head`. */
30
+ export declare const tagFacts: (workspace: Workspace, git: GitReader, line: LineFacts) => Promise<{
31
+ tags: Map<string, import("./tags.js").UnitTags>;
32
+ bases: Map<string, UnitBase>;
33
+ groups: GroupFacts[];
34
+ isAncestor: (a: string, b: string) => Promise<boolean>;
35
+ tagNames: string[];
36
+ }>;
37
+ /** Attributed changes of `(from, head]`, memoized per `from`. */
38
+ export declare const rangeFacts: (workspace: Workspace, git: GitReader, opts: FactsOptions) => {
39
+ range: (from: string | null) => Promise<RangeFacts>;
40
+ all: () => RangeFacts[];
41
+ warnings: Warning[];
42
+ };
9
43
  /** Runs every `GitReader` and intent source call a plan needs. */
10
44
  export declare const collectPlanFacts: (workspace: Workspace, git: GitReader, opts: FactsOptions) => Promise<PlanFacts>;
45
+ export {};
package/dist/facts.js CHANGED
@@ -46,38 +46,9 @@ const dedupe = (changes) => {
46
46
  }
47
47
  return kept;
48
48
  };
49
- /** Runs every `GitReader` and intent source call a plan needs. */
50
- export const collectPlanFacts = async (workspace, git, opts) => {
51
- const { head } = opts;
52
- const warnings = [];
53
- const ctx = pluginCtx({ ...opts, repo: { name: workspace.repo } }, warnings);
54
- const intents = workspace.intents.map((entry) => ({
55
- ...entry,
56
- ctx: ctx(entry.plugin, entry.options),
57
- }));
58
- const schemes = schemeRegistry(workspace.schemes);
59
- const allTags = await git.tags();
60
- const tags = matchUnitTags(workspace.units, allTags, schemes);
61
- const merged = new Set((await git.tagsMerged(head)).map((t) => t.name));
49
+ /** First-parent commit counters of `head`, memoized per commit. */
50
+ export const lineFacts = async (git, head) => {
62
51
  const total = await git.countFirstParent(null, head);
63
- const ignored = matcher(workspace.ignore);
64
- const isGlobal = matcher(workspace.globalInputs);
65
- const owners = attribution(workspace.units);
66
- const known = new Set(workspace.units.map((u) => u.id));
67
- // File-less commits belong to every released unit (release-please parity).
68
- const released = workspace.units
69
- .filter((u) => u.version !== "none")
70
- .map((u) => u.id);
71
- const warned = new Set();
72
- const warnUnknown = (plugin, changes) => {
73
- for (const id of changes.flatMap((c) => c.units ?? [])) {
74
- const key = `${plugin}\0${id}`;
75
- if (!known.has(id) && !warned.has(key)) {
76
- warned.add(key);
77
- warnings.push(coreCodes.warning("W_UNIT_UNKNOWN", `intent names unknown unit "${id}"`, { plugin }));
78
- }
79
- }
80
- };
81
52
  const counts = new Map();
82
53
  const countSince = async (sha) => {
83
54
  const cached = counts.get(sha);
@@ -88,42 +59,29 @@ export const collectPlanFacts = async (workspace, git, opts) => {
88
59
  counts.set(sha, count);
89
60
  return count;
90
61
  };
91
- const ranges = new Map();
92
- const range = async (from) => {
93
- const cached = ranges.get(from);
94
- if (cached) {
95
- return cached;
96
- }
97
- const commits = Object.freeze([...(await git.log(from, head))].sort(byCommitOrder));
98
- const position = new Map(commits.map((c, i) => [c.sha, i]));
99
- const commitUnits = new Map(commits.map((c) => [
100
- c.sha,
101
- c.files.length === 0
102
- ? released
103
- : uniqueSorted(c.files
104
- .filter((f) => !ignored(f) && !isGlobal(f))
105
- .flatMap(owners)),
106
- ]));
107
- const changes = [];
108
- for (const { plugin, source, ctx: sourceCtx } of intents) {
109
- // eslint-disable-next-line no-await-in-loop -- hooks run sequentially in config order
110
- const collected = await runHook(plugin, "intent", () => source.collect(sourceCtx, { from, to: head, commits }));
111
- warnUnknown(plugin, collected);
112
- changes.push(...collected);
62
+ /** Counter since the line base; without one, since the unit's introduction. */
63
+ const lineCount = async (root, lineBaseTag) => {
64
+ if (lineBaseTag) {
65
+ return {
66
+ introduced: null,
67
+ sinceLineBase: await countSince(lineBaseTag.sha),
68
+ };
113
69
  }
114
- const attributed = dedupe(changes)
115
- .map((change) => ({
116
- change,
117
- units: change.units
118
- ? uniqueSorted(change.units.filter((id) => known.has(id)))
119
- : (commitUnits.get(change.sha ?? "") ?? []),
120
- }))
121
- .sort((a, b) => (position.get(a.change.sha ?? "") ?? commits.length) -
122
- (position.get(b.change.sha ?? "") ?? commits.length));
123
- const result = { from, changes: attributed };
124
- ranges.set(from, result);
125
- return result;
70
+ const introduced = root === "." ? null : await git.introduced(root, head);
71
+ return {
72
+ introduced,
73
+ sinceLineBase: introduced === null ? total : (await countSince(introduced)) + 1,
74
+ };
126
75
  };
76
+ return { head, total, countSince, lineCount };
77
+ };
78
+ /** Tags of every unit and fixed group, relative to `head`. */
79
+ export const tagFacts = async (workspace, git, line) => {
80
+ const { head } = line;
81
+ const schemes = schemeRegistry(workspace.schemes);
82
+ const allTags = await git.tags();
83
+ const tags = matchUnitTags(workspace.units, allTags, schemes);
84
+ const merged = new Set((await git.tagsMerged(head)).map((t) => t.name));
127
85
  const ancestry = new Map();
128
86
  const isAncestor = async (a, b) => {
129
87
  const key = `${a}\0${b}`;
@@ -152,20 +110,6 @@ export const collectPlanFacts = async (workspace, git, opts) => {
152
110
  }
153
111
  return a.sha !== b.sha && (await isAncestor(a.sha, b.sha)) ? b : a;
154
112
  };
155
- /** Counter since the line base; without one, since the unit's introduction. */
156
- const lineCount = async (root, lineBaseTag) => {
157
- if (lineBaseTag) {
158
- return {
159
- introduced: null,
160
- sinceLineBase: await countSince(lineBaseTag.sha),
161
- };
162
- }
163
- const introduced = root === "." ? null : await git.introduced(root, head);
164
- return {
165
- introduced,
166
- sinceLineBase: introduced === null ? total : (await countSince(introduced)) + 1,
167
- };
168
- };
169
113
  const bases = new Map();
170
114
  for (const unit of workspace.units) {
171
115
  const matched = tags.get(unit.id);
@@ -188,7 +132,7 @@ export const collectPlanFacts = async (workspace, git, opts) => {
188
132
  baseTag: stable.at(-1) ?? null,
189
133
  lineBaseTag,
190
134
  // eslint-disable-next-line no-await-in-loop -- one git call per unit
191
- ...(await lineCount(unit.root, lineBaseTag)),
135
+ ...(await line.lineCount(unit.root, lineBaseTag)),
192
136
  });
193
137
  }
194
138
  const groups = [];
@@ -211,10 +155,89 @@ export const collectPlanFacts = async (workspace, git, opts) => {
211
155
  lineBaseTag,
212
156
  sinceLineBase: lineBaseTag
213
157
  ? // eslint-disable-next-line no-await-in-loop -- memoized per line base
214
- await countSince(lineBaseTag.sha)
158
+ await line.countSince(lineBaseTag.sha)
215
159
  : Math.max(0, ...members.map((id) => bases.get(id)?.sinceLineBase ?? 0)),
216
160
  });
217
161
  }
162
+ return {
163
+ tags,
164
+ bases,
165
+ groups,
166
+ isAncestor,
167
+ tagNames: uniqueSorted(allTags.map((t) => t.name)),
168
+ };
169
+ };
170
+ /** Attributed changes of `(from, head]`, memoized per `from`. */
171
+ export const rangeFacts = (workspace, git, opts) => {
172
+ const { head } = opts;
173
+ const warnings = [];
174
+ const ctx = pluginCtx({ ...opts, repo: { name: workspace.repo } }, warnings);
175
+ const intents = workspace.intents.map((entry) => ({
176
+ ...entry,
177
+ ctx: ctx(entry.plugin, entry.options),
178
+ }));
179
+ const ignored = matcher(workspace.ignore);
180
+ const isGlobal = matcher(workspace.globalInputs);
181
+ const owners = attribution(workspace.units);
182
+ const known = new Set(workspace.units.map((u) => u.id));
183
+ // File-less commits belong to every released unit (release-please parity).
184
+ const released = workspace.units
185
+ .filter((u) => u.version !== "none")
186
+ .map((u) => u.id);
187
+ const warned = new Set();
188
+ const warnUnknown = (plugin, changes) => {
189
+ for (const id of changes.flatMap((c) => c.units ?? [])) {
190
+ const key = `${plugin}\0${id}`;
191
+ if (!known.has(id) && !warned.has(key)) {
192
+ warned.add(key);
193
+ warnings.push(coreCodes.warning("W_UNIT_UNKNOWN", `intent names unknown unit "${id}"`, { plugin }));
194
+ }
195
+ }
196
+ };
197
+ const ranges = new Map();
198
+ const range = async (from) => {
199
+ const cached = ranges.get(from);
200
+ if (cached) {
201
+ return cached;
202
+ }
203
+ const commits = Object.freeze([...(await git.log(from, head))].sort(byCommitOrder));
204
+ const position = new Map(commits.map((c, i) => [c.sha, i]));
205
+ const commitUnits = new Map(commits.map((c) => [
206
+ c.sha,
207
+ c.files.length === 0
208
+ ? released
209
+ : uniqueSorted(c.files
210
+ .filter((f) => !ignored(f) && !isGlobal(f))
211
+ .flatMap(owners)),
212
+ ]));
213
+ const changes = [];
214
+ for (const { plugin, source, ctx: sourceCtx } of intents) {
215
+ // eslint-disable-next-line no-await-in-loop -- hooks run sequentially in config order
216
+ const collected = await runHook(plugin, "intent", () => source.collect(sourceCtx, { from, to: head, commits }));
217
+ warnUnknown(plugin, collected);
218
+ changes.push(...collected);
219
+ }
220
+ const attributed = dedupe(changes)
221
+ .map((change) => ({
222
+ change,
223
+ units: change.units
224
+ ? uniqueSorted(change.units.filter((id) => known.has(id)))
225
+ : (commitUnits.get(change.sha ?? "") ?? []),
226
+ }))
227
+ .sort((a, b) => (position.get(a.change.sha ?? "") ?? commits.length) -
228
+ (position.get(b.change.sha ?? "") ?? commits.length));
229
+ const result = { from, changes: attributed };
230
+ ranges.set(from, result);
231
+ return result;
232
+ };
233
+ return { range, all: () => [...ranges.values()], warnings };
234
+ };
235
+ /** Runs every `GitReader` and intent source call a plan needs. */
236
+ export const collectPlanFacts = async (workspace, git, opts) => {
237
+ const { head } = opts;
238
+ const line = await lineFacts(git, head);
239
+ const { tags, bases, groups, isAncestor, tagNames } = await tagFacts(workspace, git, line);
240
+ const { range, all, warnings } = rangeFacts(workspace, git, opts);
218
241
  const groupOf = new Map(groups.flatMap((g) => g.members.map((id) => [id, g])));
219
242
  const units = [];
220
243
  for (const unit of workspace.units) {
@@ -246,7 +269,7 @@ export const collectPlanFacts = async (workspace, git, opts) => {
246
269
  lineBaseTag,
247
270
  introduced,
248
271
  sinceLineBase,
249
- total,
272
+ total: line.total,
250
273
  changes: changes.filter((c) => c.units.includes(unit.id)),
251
274
  releaseAs: changes.flatMap(({ change }) => (change.releaseAs ?? [])
252
275
  .filter((r) => r.unit === unit.id)
@@ -256,12 +279,5 @@ export const collectPlanFacts = async (workspace, git, opts) => {
256
279
  }))),
257
280
  });
258
281
  }
259
- return {
260
- head,
261
- units,
262
- ranges: [...ranges.values()],
263
- groups,
264
- tagNames: uniqueSorted(allTags.map((t) => t.name)),
265
- warnings,
266
- };
282
+ return { head, units, ranges: all(), groups, tagNames, warnings };
267
283
  };
@@ -123,6 +123,64 @@ const templateFile = (file, unit) => {
123
123
  }
124
124
  return path;
125
125
  };
126
+ const required = (staged, path, unit) => {
127
+ const content = staged.read(path);
128
+ if (content === undefined) {
129
+ throw coreCodes.error("E_VERSION_FILE_MISSING", `version file "${path}" of unit "${unit}" does not exist`, { path, unit });
130
+ }
131
+ return content;
132
+ };
133
+ /** Writes the plugin version files of the unit. */
134
+ const versionFileStep = async (staged, step, pluginFiles) => {
135
+ const { workspace, item, unit, versions } = step;
136
+ const ctx = { unit, units: workspace.units, versions };
137
+ for (const file of pluginFiles) {
138
+ const content = required(staged, file.path, unit.id);
139
+ // eslint-disable-next-line no-await-in-loop -- files are written in order
140
+ const written = await runHook(unit.plugin, "write", () => file.write(content, item.version, ctx));
141
+ staged.files.set(file.path, written);
142
+ }
143
+ };
144
+ /** Rewrites the version templates of the unit; skips first releases. */
145
+ const templateStep = (staged, step) => {
146
+ const { workspace, item, unit, scheme, from } = step;
147
+ const warnings = [];
148
+ const templates = from === null ? [] : (workspace.release.templates.get(unit.id) ?? []);
149
+ for (const template of templates) {
150
+ const { pattern } = template;
151
+ const file = templateFile(template.file, unit.id);
152
+ const result = applyTemplate(required(staged, file, unit.id), {
153
+ pattern,
154
+ unit: unit.id,
155
+ from: from ?? "",
156
+ next: item.version,
157
+ scheme,
158
+ });
159
+ staged.files.set(file, result.content);
160
+ if (!result.hit) {
161
+ warnings.push(coreCodes.warning("W_VERSION_FILE_NO_MATCH", `version file pattern "${pattern}" of unit "${unit.id}" matches nothing in "${file}"`, { unit: unit.id }));
162
+ }
163
+ }
164
+ return warnings;
165
+ };
166
+ /** Updates the unit's changelog; returns the new section, if any. */
167
+ const changelogStep = (staged, step) => {
168
+ const { workspace, plan, unit, scheme, from, repoUrl, date, prUnits } = step;
169
+ const section = changelogSection(workspace, plan, {
170
+ id: unit.id,
171
+ repoUrl,
172
+ date,
173
+ prUnits,
174
+ });
175
+ if (section !== undefined) {
176
+ const path = changelogPath(unit);
177
+ staged.files.set(path, updateChangelog(staged.read(path), section, {
178
+ baseVersion: from,
179
+ scheme,
180
+ }));
181
+ }
182
+ return section;
183
+ };
126
184
  /** Checks template files and patterns (`E_CONFIG`); runs no plugin hook. */
127
185
  export const validateTemplates = (workspace) => {
128
186
  for (const [id, templates] of workspace.release.templates) {
@@ -177,16 +235,12 @@ export const releaseFiles = async (workspace, plan, opts) => {
177
235
  const versions = new Map(items.flatMap((i) => i.units.map((id) => [id, i.version])));
178
236
  const pr = (item) => item.mode === "shared" ? "shared" : item.name;
179
237
  const files = new Map();
238
+ const staged = {
239
+ files,
240
+ read: (path) => files.get(path) ?? fs.read(path),
241
+ };
180
242
  const sections = new Map();
181
243
  const warnings = [];
182
- const read = (path) => files.get(path) ?? fs.read(path);
183
- const required = (path, unit) => {
184
- const content = read(path);
185
- if (content === undefined) {
186
- throw coreCodes.error("E_VERSION_FILE_MISSING", `version file "${path}" of unit "${unit}" does not exist`, { path, unit });
187
- }
188
- return content;
189
- };
190
244
  for (const item of items) {
191
245
  const prUnits = items
192
246
  .filter((i) => pr(i) === pr(item))
@@ -198,40 +252,24 @@ export const releaseFiles = async (workspace, plan, opts) => {
198
252
  if (!unit || !entry || !scheme) {
199
253
  continue;
200
254
  }
201
- const ctx = { unit, units: workspace.units, versions };
202
- for (const file of pluginFiles.get(id) ?? []) {
203
- const content = required(file.path, id);
204
- // eslint-disable-next-line no-await-in-loop -- files are written in order
205
- const written = await runHook(unit.plugin, "write", () => file.write(content, item.version, ctx));
206
- files.set(file.path, written);
207
- }
208
- const { from } = entry;
209
- const templates = from === null ? [] : (workspace.release.templates.get(id) ?? []);
210
- for (const template of templates) {
211
- const { pattern } = template;
212
- const file = templateFile(template.file, id);
213
- const result = applyTemplate(required(file, id), {
214
- pattern,
215
- unit: id,
216
- from: from ?? "",
217
- next: item.version,
218
- scheme,
219
- });
220
- files.set(file, result.content);
221
- if (!result.hit) {
222
- warnings.push(coreCodes.warning("W_VERSION_FILE_NO_MATCH", `version file pattern "${pattern}" of unit "${id}" matches nothing in "${file}"`, { unit: id }));
223
- }
224
- }
225
- const section = changelogSection(workspace, plan, {
226
- id,
255
+ const step = {
256
+ workspace,
257
+ plan,
258
+ item,
259
+ unit,
260
+ scheme,
261
+ from: entry.from,
262
+ versions,
263
+ prUnits,
227
264
  repoUrl,
228
265
  date,
229
- prUnits,
230
- });
266
+ };
267
+ // eslint-disable-next-line no-await-in-loop -- files are written in order
268
+ await versionFileStep(staged, step, pluginFiles.get(id) ?? []);
269
+ warnings.push(...templateStep(staged, step));
270
+ const section = changelogStep(staged, step);
231
271
  if (section !== undefined) {
232
272
  sections.set(id, section);
233
- const path = changelogPath(unit);
234
- files.set(path, updateChangelog(read(path), section, { baseVersion: from, scheme }));
235
273
  }
236
274
  }
237
275
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyrelease/core",
3
- "version": "0.5.0-beta.31",
3
+ "version": "0.5.0-beta.33",
4
4
  "description": "Pure planning core of tallyrelease",
5
5
  "repository": {
6
6
  "type": "git",