@tallyrelease/core 0.4.1 → 0.4.2-beta.12

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/README.md CHANGED
@@ -26,7 +26,7 @@ See `Plugin` in [`src/plugin.ts`](https://github.com/abinnovision/tallyrelease/b
26
26
 
27
27
  ## Testing
28
28
 
29
- The `@tallyrelease/core/testing` export provides in-memory fakes: `memoryFs`, `memoryCtx` and `memoryGit`.
29
+ The `@tallyrelease/core/testing` export provides in-memory fakes: `memoryFs`, `memoryCtx` and `memoryGit`. The `@tallyrelease/core/testing/conformance` export provides `gitReaderConformance`, a shared `GitReader` spec that requires vitest.
30
30
 
31
31
  ## Licence
32
32
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /** @deprecated Unused, will be removed in 1.0. */
1
2
  export declare const version = "0.0.0";
2
3
  export type { AffectedDoc, AffectedOptions, AffectedReason, AffectedUnit, BaseSource, } from "./affected.js";
3
4
  export type { ChangelogDependency, ChangelogInput } from "./changelog.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** @deprecated Unused, will be removed in 1.0. */
1
2
  export const version = "0.0.0";
2
3
  export { computeAffected } from "./affected.js";
3
4
  export { maintenanceCut } from "./branches.js";
@@ -0,0 +1,15 @@
1
+ import type { Dag } from "./index.js";
2
+ import type { GitReader, MaybePromise } from "../plugin.js";
3
+ /** Builds a reader for `dag`; `sha` maps a commit id to the reader's sha. */
4
+ export type ReaderFactory = (dag: Dag) => MaybePromise<{
5
+ git: GitReader;
6
+ sha: (id: string) => string;
7
+ }>;
8
+ /**
9
+ * Shared `GitReader` spec. Fixture: a - b - d - m - e - f on the first-parent
10
+ * chain, c branches from b and merges into m, d renames pkg/x.ts to pkg/y.ts,
11
+ * f adds a path with a quote, a tab and a non-ASCII character, g is empty and
12
+ * h carries footers. `a` has a later committer date than b, c and d.
13
+ * `introduced` runs on its own fixture.
14
+ */
15
+ export declare const gitReaderConformance: (makeReader: ReaderFactory) => void;
@@ -1,8 +1,5 @@
1
- import picomatch from "picomatch/posix.js";
2
1
  import { beforeAll, describe, expect, it } from "vitest";
3
- import { compare, uniqueSorted } from "./util.js";
4
- const SKIPPED = new Set([".git", "node_modules"]);
5
- const changed = (a, b) => uniqueSorted([...a.keys(), ...b.keys()]).filter((p) => a.get(p) !== b.get(p));
2
+ import { compare } from "../util.js";
6
3
  /** Path git would C-quote without `-z`. */
7
4
  const ODD_PATH = 'odd/a "q"\tü.txt';
8
5
  const FOOTER_MESSAGE = "feat: h\n\nbody\n\nRelease-As: core@1.0.0\nRefs: #1";
@@ -63,145 +60,6 @@ const INTRODUCED_DAG = {
63
60
  { id: "v", files: { "pkg/a/x.ts": "v\n", "gone/x.ts": "v\n" } },
64
61
  ],
65
62
  };
66
- /** In-memory `ReadonlyFs` over a path-to-content map. */
67
- export const memoryFs = (files) => {
68
- const paths = Object.keys(files).sort(compare);
69
- const dirs = uniqueSorted(paths.flatMap((p) => {
70
- const parts = p.split("/");
71
- return parts.slice(1).map((_, i) => parts.slice(0, i + 1).join("/"));
72
- }));
73
- const visible = (p) => {
74
- const segments = p.split("/");
75
- return segments[0] !== "vendor" && !segments.some((s) => SKIPPED.has(s));
76
- };
77
- return {
78
- read: (path) => (Object.hasOwn(files, path) ? files[path] : undefined),
79
- exists: (path) => path === "." || Object.hasOwn(files, path) || dirs.includes(path),
80
- glob: (patterns, opts) => {
81
- const match = picomatch(patterns, { dot: true });
82
- return (opts?.dirs ? dirs : paths).filter((p) => visible(p) && match(p));
83
- },
84
- };
85
- };
86
- /** Plugin `Ctx` over `memoryFs`; collects `log.warn` messages in `warnings`. */
87
- export const memoryCtx = (opts = {}) => {
88
- const warnings = [];
89
- const options = opts.options ?? {};
90
- return {
91
- options: options,
92
- repo: { name: opts.repo ?? "repo" },
93
- fs: memoryFs(opts.files ?? {}),
94
- log: { warn: (msg) => warnings.push(msg), debug: () => undefined },
95
- warnings,
96
- };
97
- };
98
- /** In-memory `GitReader` over a commit graph. */
99
- export const memoryGit = (dag) => {
100
- const nodes = new Map();
101
- const node = (ref) => {
102
- const id = ref === "HEAD" ? dag.commits.at(-1)?.id : ref;
103
- const found = nodes.get(id ?? "") ??
104
- nodes.get(dag.tags?.find((t) => t.name === ref)?.commit ?? "");
105
- if (!found) {
106
- throw new Error(`unknown ref "${ref}"`);
107
- }
108
- return found;
109
- };
110
- const mergeBase = (a, b) => {
111
- const common = [...a.ancestors].filter((id) => b.ancestors.has(id));
112
- const best = common.find((id) => !common.some((o) => o !== id && node(o).ancestors.has(id)));
113
- return best === undefined ? new Map() : node(best).tree;
114
- };
115
- dag.commits.forEach((c, i) => {
116
- const prior = dag.commits[i - 1];
117
- const parents = (c.parents ?? (prior ? [prior.id] : [])).map(node);
118
- const [first, ...others] = parents;
119
- const tree = new Map(first?.tree);
120
- for (const other of others) {
121
- const base = first ? mergeBase(first, other) : new Map();
122
- for (const path of changed(base, other.tree)) {
123
- const content = other.tree.get(path);
124
- if (content === undefined) {
125
- tree.delete(path);
126
- }
127
- else {
128
- tree.set(path, content);
129
- }
130
- }
131
- }
132
- for (const [path, content] of Object.entries(c.files ?? {})) {
133
- if (content === null) {
134
- tree.delete(path);
135
- }
136
- else {
137
- tree.set(path, content);
138
- }
139
- }
140
- nodes.set(c.id, {
141
- id: c.id,
142
- parents: parents.map((p) => p.id),
143
- message: c.message ?? c.id,
144
- date: c.date ?? new Date(Date.UTC(2024, 0, 1, 0, i)).toISOString(),
145
- tree,
146
- ancestors: new Set([c.id, ...parents.flatMap((p) => [...p.ancestors])]),
147
- });
148
- });
149
- const firstParentChain = (to) => {
150
- const chain = [];
151
- for (let n = node(to); n;) {
152
- chain.push(n);
153
- const parent = n.parents[0];
154
- n = parent === undefined ? undefined : node(parent);
155
- }
156
- return chain;
157
- };
158
- const tagRefs = () => (dag.tags ?? [])
159
- .map((t) => ({ name: t.name, sha: node(t.commit).id }))
160
- .sort((a, b) => compare(a.name, b.name));
161
- const excluded = (from) => from === null ? new Set() : node(from).ancestors;
162
- return {
163
- tags: tagRefs,
164
- tagsMerged: (ref) => {
165
- const reachable = node(ref).ancestors;
166
- return tagRefs().filter((t) => reachable.has(t.sha));
167
- },
168
- isAncestor: (a, b) => node(b).ancestors.has(node(a).id),
169
- isFirstParentAncestor: (a, b) => {
170
- const id = node(a).id;
171
- return firstParentChain(b).some((n) => n.id === id);
172
- },
173
- log: (from, to) => {
174
- const skip = excluded(from);
175
- const reachable = node(to).ancestors;
176
- return dag.commits
177
- .map((c) => node(c.id))
178
- .filter((n) => reachable.has(n.id) && !skip.has(n.id))
179
- .filter((n) => n.parents.length <= 1)
180
- .reverse()
181
- .map((n) => ({
182
- sha: n.id,
183
- parents: [...n.parents],
184
- message: n.message,
185
- files: changed(n.parents[0] === undefined ? new Map() : node(n.parents[0]).tree, n.tree),
186
- date: n.date,
187
- }));
188
- },
189
- countFirstParent: (from, to) => {
190
- const skip = excluded(from);
191
- return firstParentChain(to).filter((n) => !skip.has(n.id)).length;
192
- },
193
- diffFiles: (from, to) => changed(node(from).tree, node(to).tree),
194
- introduced: (path, to) => {
195
- const under = (p) => p === path || p.startsWith(`${path}/`);
196
- const intro = firstParentChain(to)
197
- .reverse()
198
- .find((n) => changed(n.parents[0] === undefined ? new Map() : node(n.parents[0]).tree, n.tree).some(under));
199
- return intro?.id ?? null;
200
- },
201
- tree: (ref) => Object.fromEntries(node(ref).tree),
202
- parents: (ref) => [...node(ref).parents],
203
- };
204
- };
205
63
  /**
206
64
  * Shared `GitReader` spec. Fixture: a - b - d - m - e - f on the first-parent
207
65
  * chain, c branches from b and merges into m, d renames pkg/x.ts to pkg/y.ts,
@@ -1,4 +1,5 @@
1
- import type { Ctx, GitReader, MaybePromise, ReadonlyFs } from "./plugin.js";
1
+ import type { Ctx, GitReader, ReadonlyFs } from "../plugin.js";
2
+ import type { ReaderFactory } from "./conformance.js";
2
3
  /** In-memory `ReadonlyFs` over a path-to-content map. */
3
4
  export declare const memoryFs: (files: Record<string, string>) => ReadonlyFs;
4
5
  export interface MemoryCtxOptions<O> {
@@ -42,16 +43,6 @@ export interface MemoryGit extends GitReader {
42
43
  }
43
44
  /** In-memory `GitReader` over a commit graph. */
44
45
  export declare const memoryGit: (dag: Dag) => MemoryGit;
45
- /** Builds a reader for `dag`; `sha` maps a commit id to the reader's sha. */
46
- export type ReaderFactory = (dag: Dag) => MaybePromise<{
47
- git: GitReader;
48
- sha: (id: string) => string;
49
- }>;
50
- /**
51
- * Shared `GitReader` spec. Fixture: a - b - d - m - e - f on the first-parent
52
- * chain, c branches from b and merges into m, d renames pkg/x.ts to pkg/y.ts,
53
- * f adds a path with a quote, a tab and a non-ASCII character, g is empty and
54
- * h carries footers. `a` has a later committer date than b, c and d.
55
- * `introduced` runs on its own fixture.
56
- */
46
+ export type { ReaderFactory } from "./conformance.js";
47
+ /** Re-export of `@tallyrelease/core/testing/conformance`; requires vitest. */
57
48
  export declare const gitReaderConformance: (makeReader: ReaderFactory) => void;
@@ -0,0 +1,158 @@
1
+ import picomatch from "picomatch/posix.js";
2
+ import { compare, uniqueSorted } from "../util.js";
3
+ // Loaded optionally so the fakes work without vitest installed.
4
+ // eslint-disable-next-line no-restricted-syntax
5
+ const conformance = await import("./conformance.js").catch((error) => {
6
+ if (error.code === "ERR_MODULE_NOT_FOUND") {
7
+ return null;
8
+ }
9
+ throw error;
10
+ });
11
+ const SKIPPED = new Set([".git", "node_modules"]);
12
+ const changed = (a, b) => uniqueSorted([...a.keys(), ...b.keys()]).filter((p) => a.get(p) !== b.get(p));
13
+ /** In-memory `ReadonlyFs` over a path-to-content map. */
14
+ export const memoryFs = (files) => {
15
+ const paths = Object.keys(files).sort(compare);
16
+ const dirs = uniqueSorted(paths.flatMap((p) => {
17
+ const parts = p.split("/");
18
+ return parts.slice(1).map((_, i) => parts.slice(0, i + 1).join("/"));
19
+ }));
20
+ const visible = (p) => {
21
+ const segments = p.split("/");
22
+ return segments[0] !== "vendor" && !segments.some((s) => SKIPPED.has(s));
23
+ };
24
+ return {
25
+ read: (path) => (Object.hasOwn(files, path) ? files[path] : undefined),
26
+ exists: (path) => path === "." || Object.hasOwn(files, path) || dirs.includes(path),
27
+ glob: (patterns, opts) => {
28
+ const match = picomatch(patterns, { dot: true });
29
+ return (opts?.dirs ? dirs : paths).filter((p) => visible(p) && match(p));
30
+ },
31
+ };
32
+ };
33
+ /** Plugin `Ctx` over `memoryFs`; collects `log.warn` messages in `warnings`. */
34
+ export const memoryCtx = (opts = {}) => {
35
+ const warnings = [];
36
+ const options = opts.options ?? {};
37
+ return {
38
+ options: options,
39
+ repo: { name: opts.repo ?? "repo" },
40
+ fs: memoryFs(opts.files ?? {}),
41
+ log: { warn: (msg) => warnings.push(msg), debug: () => undefined },
42
+ warnings,
43
+ };
44
+ };
45
+ /** In-memory `GitReader` over a commit graph. */
46
+ export const memoryGit = (dag) => {
47
+ const nodes = new Map();
48
+ const node = (ref) => {
49
+ const id = ref === "HEAD" ? dag.commits.at(-1)?.id : ref;
50
+ const found = nodes.get(id ?? "") ??
51
+ nodes.get(dag.tags?.find((t) => t.name === ref)?.commit ?? "");
52
+ if (!found) {
53
+ throw new Error(`unknown ref "${ref}"`);
54
+ }
55
+ return found;
56
+ };
57
+ const mergeBase = (a, b) => {
58
+ const common = [...a.ancestors].filter((id) => b.ancestors.has(id));
59
+ const best = common.find((id) => !common.some((o) => o !== id && node(o).ancestors.has(id)));
60
+ return best === undefined ? new Map() : node(best).tree;
61
+ };
62
+ dag.commits.forEach((c, i) => {
63
+ const prior = dag.commits[i - 1];
64
+ const parents = (c.parents ?? (prior ? [prior.id] : [])).map(node);
65
+ const [first, ...others] = parents;
66
+ const tree = new Map(first?.tree);
67
+ for (const other of others) {
68
+ const base = first ? mergeBase(first, other) : new Map();
69
+ for (const path of changed(base, other.tree)) {
70
+ const content = other.tree.get(path);
71
+ if (content === undefined) {
72
+ tree.delete(path);
73
+ }
74
+ else {
75
+ tree.set(path, content);
76
+ }
77
+ }
78
+ }
79
+ for (const [path, content] of Object.entries(c.files ?? {})) {
80
+ if (content === null) {
81
+ tree.delete(path);
82
+ }
83
+ else {
84
+ tree.set(path, content);
85
+ }
86
+ }
87
+ nodes.set(c.id, {
88
+ id: c.id,
89
+ parents: parents.map((p) => p.id),
90
+ message: c.message ?? c.id,
91
+ date: c.date ?? new Date(Date.UTC(2024, 0, 1, 0, i)).toISOString(),
92
+ tree,
93
+ ancestors: new Set([c.id, ...parents.flatMap((p) => [...p.ancestors])]),
94
+ });
95
+ });
96
+ const firstParentChain = (to) => {
97
+ const chain = [];
98
+ for (let n = node(to); n;) {
99
+ chain.push(n);
100
+ const parent = n.parents[0];
101
+ n = parent === undefined ? undefined : node(parent);
102
+ }
103
+ return chain;
104
+ };
105
+ const tagRefs = () => (dag.tags ?? [])
106
+ .map((t) => ({ name: t.name, sha: node(t.commit).id }))
107
+ .sort((a, b) => compare(a.name, b.name));
108
+ const excluded = (from) => from === null ? new Set() : node(from).ancestors;
109
+ return {
110
+ tags: tagRefs,
111
+ tagsMerged: (ref) => {
112
+ const reachable = node(ref).ancestors;
113
+ return tagRefs().filter((t) => reachable.has(t.sha));
114
+ },
115
+ isAncestor: (a, b) => node(b).ancestors.has(node(a).id),
116
+ isFirstParentAncestor: (a, b) => {
117
+ const id = node(a).id;
118
+ return firstParentChain(b).some((n) => n.id === id);
119
+ },
120
+ log: (from, to) => {
121
+ const skip = excluded(from);
122
+ const reachable = node(to).ancestors;
123
+ return dag.commits
124
+ .map((c) => node(c.id))
125
+ .filter((n) => reachable.has(n.id) && !skip.has(n.id))
126
+ .filter((n) => n.parents.length <= 1)
127
+ .reverse()
128
+ .map((n) => ({
129
+ sha: n.id,
130
+ parents: [...n.parents],
131
+ message: n.message,
132
+ files: changed(n.parents[0] === undefined ? new Map() : node(n.parents[0]).tree, n.tree),
133
+ date: n.date,
134
+ }));
135
+ },
136
+ countFirstParent: (from, to) => {
137
+ const skip = excluded(from);
138
+ return firstParentChain(to).filter((n) => !skip.has(n.id)).length;
139
+ },
140
+ diffFiles: (from, to) => changed(node(from).tree, node(to).tree),
141
+ introduced: (path, to) => {
142
+ const under = (p) => p === path || p.startsWith(`${path}/`);
143
+ const intro = firstParentChain(to)
144
+ .reverse()
145
+ .find((n) => changed(n.parents[0] === undefined ? new Map() : node(n.parents[0]).tree, n.tree).some(under));
146
+ return intro?.id ?? null;
147
+ },
148
+ tree: (ref) => Object.fromEntries(node(ref).tree),
149
+ parents: (ref) => [...node(ref).parents],
150
+ };
151
+ };
152
+ /** Re-export of `@tallyrelease/core/testing/conformance`; requires vitest. */
153
+ export const gitReaderConformance = (makeReader) => {
154
+ if (conformance === null) {
155
+ throw new Error("gitReaderConformance requires vitest");
156
+ }
157
+ conformance.gitReaderConformance(makeReader);
158
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyrelease/core",
3
- "version": "0.4.1",
3
+ "version": "0.4.2-beta.12",
4
4
  "description": "Pure planning core of tallyrelease",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,9 +21,14 @@
21
21
  "default": "./dist/index.js"
22
22
  },
23
23
  "./testing": {
24
- "types": "./dist/testing.d.ts",
25
- "import": "./dist/testing.js",
26
- "default": "./dist/testing.js"
24
+ "types": "./dist/testing/index.d.ts",
25
+ "import": "./dist/testing/index.js",
26
+ "default": "./dist/testing/index.js"
27
+ },
28
+ "./testing/conformance": {
29
+ "types": "./dist/testing/conformance.d.ts",
30
+ "import": "./dist/testing/conformance.js",
31
+ "default": "./dist/testing/conformance.js"
27
32
  }
28
33
  },
29
34
  "files": [
@@ -31,7 +36,7 @@
31
36
  ],
32
37
  "scripts": {
33
38
  "build": "tsc --build tsconfig.build.json",
34
- "check:purity": "node scripts/check-purity.mjs src/index.ts",
39
+ "check:purity": "node ../../tools/check-purity.mjs src/index.ts",
35
40
  "format:check": "prettier --check '{{src,test}/**/*,*}.{{t,j}s{,x},json{,5},md,y{,a}ml,css,html,gql,graphql}'",
36
41
  "format:fix": "prettier --write '{{src,test}/**/*,*}.{{t,j}s{,x},json{,5},md,y{,a}ml,css,html,gql,graphql}'",
37
42
  "lint:check": "eslint '{{src,test}/**/*,*}.{t,j}s{,x}'",
@@ -58,7 +63,6 @@
58
63
  "@types/picomatch": "^4.0.3",
59
64
  "@types/semver": "^7.8.0",
60
65
  "@vitest/coverage-v8": "^5.0.4",
61
- "esbuild": "^0.28.2",
62
66
  "eslint": "^10.12.0",
63
67
  "prettier": "^3.9.10",
64
68
  "typescript": "^6.0.3",
@@ -86,9 +90,14 @@
86
90
  "default": "./dist/index.js"
87
91
  },
88
92
  "./testing": {
89
- "types": "./dist/testing.d.ts",
90
- "import": "./dist/testing.js",
91
- "default": "./dist/testing.js"
93
+ "types": "./dist/testing/index.d.ts",
94
+ "import": "./dist/testing/index.js",
95
+ "default": "./dist/testing/index.js"
96
+ },
97
+ "./testing/conformance": {
98
+ "types": "./dist/testing/conformance.d.ts",
99
+ "import": "./dist/testing/conformance.js",
100
+ "default": "./dist/testing/conformance.js"
92
101
  }
93
102
  },
94
103
  "npm": true