@reviewflow/sdk 0.0.0-stage → 6.22.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.
Files changed (50) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +86 -2
  3. package/build/files-node.mjs +70 -0
  4. package/build/git-node.mjs +1792 -0
  5. package/build/index-browser.es.js +3328 -0
  6. package/build/index-node.mjs +3328 -0
  7. package/package.json +135 -3
  8. package/src/api/pullRequest.ts +617 -0
  9. package/src/config/insertCheckoutEntry.ts +54 -0
  10. package/src/diff/diffPaths.ts +78 -0
  11. package/src/diff/impacts.ts +47 -0
  12. package/src/diff/readingOrder.ts +428 -0
  13. package/src/diff/themes.ts +105 -0
  14. package/src/diff/types.ts +141 -0
  15. package/src/files/editableFile.ts +163 -0
  16. package/src/files.ts +13 -0
  17. package/src/git/groupDiffPaths.ts +134 -0
  18. package/src/git/manifests.ts +96 -0
  19. package/src/git/moduleBindings.ts +571 -0
  20. package/src/git/pullRequestDiff.ts +159 -0
  21. package/src/git/readBranchDiff.ts +359 -0
  22. package/src/git/readImpacts.ts +797 -0
  23. package/src/git/readingOrder.ts +190 -0
  24. package/src/git/run.ts +213 -0
  25. package/src/git.ts +38 -0
  26. package/src/github/applySuggestions.ts +399 -0
  27. package/src/github/editPullRequest.ts +790 -0
  28. package/src/github/fetchCompareDiff.ts +21 -0
  29. package/src/github/imageProxies.ts +43 -0
  30. package/src/github/lineDiff.ts +145 -0
  31. package/src/github/prQuery.ts +298 -0
  32. package/src/github/pullRequestDetail.ts +1347 -0
  33. package/src/github/pullRequestHead.ts +65 -0
  34. package/src/github/pullRequestRevisions.ts +124 -0
  35. package/src/github/transport.ts +171 -0
  36. package/src/index.ts +35 -0
  37. package/src/reviewflow/board.ts +333 -0
  38. package/src/reviewflow/changedParts.ts +80 -0
  39. package/src/reviewflow/checksFromReviewflow.ts +75 -0
  40. package/src/reviewflow/client.ts +589 -0
  41. package/src/reviewflow/follow.ts +345 -0
  42. package/src/reviewflow/status.ts +17 -0
  43. package/src/reviewflow/stream.ts +199 -0
  44. package/src/types/entities.ts +8 -0
  45. package/src/types/ids.ts +1 -0
  46. package/src/types/reviewflow.ts +32 -0
  47. package/src/utils/pullRequestEdit.ts +108 -0
  48. package/src/utils/pullRequestPart.ts +12 -0
  49. package/src/utils/pullRequestReaction.ts +19 -0
  50. package/src/utils/pullRequestSuggestion.ts +148 -0
@@ -0,0 +1,78 @@
1
+ const cEscapes: Record<string, number> = {
2
+ a: 7,
3
+ b: 8,
4
+ f: 12,
5
+ n: 10,
6
+ r: 13,
7
+ t: 9,
8
+ v: 11,
9
+ '"': 34,
10
+ "\\": 92,
11
+ };
12
+
13
+ /** git quotes a name holding a quote, a backslash or a control character C-style, bytes in octal */
14
+ const unquoteGitPath = (quoted: string): string => {
15
+ const bytes: number[] = [];
16
+ const encoder = new TextEncoder();
17
+ for (let index = 1; index < quoted.length - 1; index++) {
18
+ const char = quoted[index]!;
19
+ if (char !== "\\") {
20
+ bytes.push(...encoder.encode(char));
21
+ continue;
22
+ }
23
+ const next = quoted[index + 1] ?? "";
24
+ const octal = /^[0-7]{3}/.exec(quoted.slice(index + 1));
25
+ if (octal) {
26
+ bytes.push(Number.parseInt(octal[0], 8));
27
+ index += 3;
28
+ } else {
29
+ bytes.push(cEscapes[next] ?? next.codePointAt(0) ?? 0);
30
+ index += 1;
31
+ }
32
+ }
33
+ return new TextDecoder().decode(new Uint8Array(bytes));
34
+ };
35
+
36
+ /**
37
+ * A `---`/`+++` name: git ends one holding a space with a tab, and quotes one
38
+ * holding special characters; `prefix` is `a/` or `b/`.
39
+ */
40
+ const unquoted = (name: string): string =>
41
+ name.length > 1 && name.startsWith('"') && name.endsWith('"')
42
+ ? unquoteGitPath(name)
43
+ : name;
44
+
45
+ const headerName = (raw: string, prefix: string): string | null => {
46
+ const name = unquoted(raw.endsWith("\t") ? raw.slice(0, -1) : raw);
47
+ return name.startsWith(prefix) ? name.slice(prefix.length) : null;
48
+ };
49
+
50
+ /**
51
+ * The file a block of a unified diff names after the change: `+++ b/…`, or
52
+ * `--- a/…` for a deletion, or the header's second half for a binary file or a
53
+ * pure rename, which carry no `+++` line.
54
+ */
55
+ const pathOfBlock = (block: string): string | null => {
56
+ const added = /^\+\+\+ (.*)$/m.exec(block)?.[1];
57
+ const addedName = added === undefined ? null : headerName(added, "b/");
58
+ if (addedName !== null) return addedName;
59
+ const removed = /^--- (.*)$/m.exec(block)?.[1];
60
+ const removedName = removed === undefined ? null : headerName(removed, "a/");
61
+ if (removedName !== null) return removedName;
62
+ const renamed = /^rename to (.*)$/m.exec(block)?.[1];
63
+ if (renamed !== undefined) return unquoted(renamed);
64
+ const header = block.split("\n", 1)[0] ?? "";
65
+ const quotedSeparator = header.lastIndexOf(' "b/');
66
+ if (quotedSeparator !== -1) {
67
+ return headerName(header.slice(quotedSeparator + 1), "b/");
68
+ }
69
+ const separator = header.lastIndexOf(" b/");
70
+ return separator === -1 ? null : header.slice(separator + 3);
71
+ };
72
+
73
+ export const diffPaths = (patch: string): string[] =>
74
+ patch
75
+ .split(/^diff --git /m)
76
+ .slice(1)
77
+ .map(pathOfBlock)
78
+ .filter((path) => path !== null);
@@ -0,0 +1,47 @@
1
+ export interface DiffImporter {
2
+ path: string;
3
+ /** `isAttachablePath`: a test, a story, a fixture */
4
+ test: boolean;
5
+ }
6
+
7
+ export interface DiffExportImpact {
8
+ name: string;
9
+ /** exported at the merge base, not at head */
10
+ removed: boolean;
11
+ importers: DiffImporter[];
12
+ }
13
+
14
+ export interface DiffFileImpacts {
15
+ path: string;
16
+ /** the touched exports used outside the diff only */
17
+ exports: DiffExportImpact[];
18
+ }
19
+
20
+ /**
21
+ * `GET /api/prs/impacts?repo=<owner/repo>&number=<n>&head=<sha>&base=<sha>`
22
+ * — for the range `/api/prs/diff` reads, the exports the change touches and
23
+ * the files outside the diff that use them, across the workspace packages.
24
+ * Read from the registered checkout at `head`; answered with a `DiffRefusal`
25
+ * without one. `tooLarge`: nothing is analyzed, because the tree is past the
26
+ * file budget (`reason: "files"`, which a retry does not change) or the parse
27
+ * time budget (`reason: "time"`: what was parsed stays cached server side, so a
28
+ * retry goes further).
29
+ */
30
+ export type DiffImpactsResponse =
31
+ | {
32
+ tooLarge: false;
33
+ files: DiffFileImpacts[];
34
+ /** changed files not analyzed: not TypeScript, or deleted */
35
+ notAnalyzed: string[];
36
+ /**
37
+ * packages holding imports that could hide a user of a changed file: a
38
+ * path alias in a package holding a changed TypeScript file (a bare
39
+ * specifier that is no workspace package, no node built-in and no
40
+ * dependency of the importing package), or a workspace package specifier resolving to no file (its `exports`
41
+ * pointing at a build output) whose target package holds one. A relative
42
+ * or `#` specifier naming no file is never one: every changed file exists
43
+ * at `head`.
44
+ */
45
+ unresolvedIn: string[];
46
+ }
47
+ | { tooLarge: true; reason: "files" | "time" };
@@ -0,0 +1,428 @@
1
+ import type { DiffGroup, DiffReadingFile, DiffReadingGroup } from "./types.ts";
2
+
3
+ /**
4
+ * A change read the way its code runs: within a package, from the files no
5
+ * other changed file imports, down through what they import; packages from the
6
+ * one depending on the others down to its dependencies. A test or a story is
7
+ * read with the file it covers, below it.
8
+ *
9
+ * TypeScript only, imports within the package only (relative, or `#`
10
+ * through its `package.json` `imports`): what a package imports from another
11
+ * is told by the package order, not by file.
12
+ */
13
+
14
+ const typescriptExtensions = [".ts", ".tsx", ".mts", ".cts"];
15
+
16
+ const extensionOf = (path: string): string => {
17
+ const slash = path.lastIndexOf("/");
18
+ const dot = path.lastIndexOf(".");
19
+ return dot > slash ? path.slice(dot) : "";
20
+ };
21
+
22
+ export const isTypescriptPath = (path: string): boolean =>
23
+ typescriptExtensions.includes(extensionOf(path));
24
+
25
+ const attachableSuffix = /\.(?:test|spec|stories)(?=\.[cm]?tsx?$)/;
26
+
27
+ const testDirectories = new Set([
28
+ "__fixtures__",
29
+ "__mocks__",
30
+ "__tests__",
31
+ "fixtures",
32
+ "test",
33
+ "tests",
34
+ ]);
35
+
36
+ /**
37
+ * A test, a story, or what only tests use (a helper, a mock, a fixture): read
38
+ * below the file it covers rather than on its own, and never what a test is
39
+ * read below.
40
+ */
41
+ export const isAttachablePath = (path: string): boolean =>
42
+ isTypescriptPath(path) &&
43
+ (attachableSuffix.test(path) ||
44
+ path
45
+ .split("/")
46
+ .slice(0, -1)
47
+ .some((directory) => testDirectories.has(directory)));
48
+
49
+ const javascriptToTypescript: Readonly<Record<string, string>> = {
50
+ ".js": ".ts",
51
+ ".jsx": ".tsx",
52
+ ".mjs": ".mts",
53
+ ".cjs": ".cts",
54
+ };
55
+
56
+ const directoryOf = (path: string): string => {
57
+ const slash = path.lastIndexOf("/");
58
+ return slash === -1 ? "" : path.slice(0, slash);
59
+ };
60
+
61
+ /** `a/b/../c/./d` as `a/c/d`; null when it climbs above the root. */
62
+ const normalize = (path: string): string | null => {
63
+ const segments: string[] = [];
64
+ for (const segment of path.split("/")) {
65
+ if (segment === "" || segment === ".") continue;
66
+ if (segment === "..") {
67
+ if (segments.length === 0) return null;
68
+ segments.pop();
69
+ } else {
70
+ segments.push(segment);
71
+ }
72
+ }
73
+ return segments.join("/");
74
+ };
75
+
76
+ const candidatesOf = (target: string): string[] => {
77
+ const extension = extensionOf(target);
78
+ const rewritten = javascriptToTypescript[extension];
79
+ const declaration =
80
+ rewritten === undefined
81
+ ? `${target}.d.ts`
82
+ : `${target.slice(0, -extension.length)}.d.ts`;
83
+ return [
84
+ target,
85
+ ...(rewritten === undefined
86
+ ? []
87
+ : [target.slice(0, -extension.length) + rewritten]),
88
+ ...typescriptExtensions.map((typescript) => target + typescript),
89
+ declaration,
90
+ `${target}/index.ts`,
91
+ `${target}/index.tsx`,
92
+ ];
93
+ };
94
+
95
+ const resolveTarget = (
96
+ target: string | null,
97
+ exists: (path: string) => boolean,
98
+ ): string | null =>
99
+ target === null
100
+ ? null
101
+ : (candidatesOf(target).find(
102
+ (candidate) => isTypescriptPath(candidate) && exists(candidate),
103
+ ) ?? null);
104
+
105
+ /**
106
+ * The TypeScript file a relative specifier names, as TypeScript resolves it
107
+ * under `moduleResolution: bundler`: as written, a `.js` written for its
108
+ * `.ts`, an extension left out, or a directory's `index`. null for a bare
109
+ * specifier, or one naming no TypeScript file at the commit.
110
+ */
111
+ export const resolveRelative = (
112
+ from: string,
113
+ specifier: string,
114
+ exists: (path: string) => boolean,
115
+ ): string | null => {
116
+ if (!specifier.startsWith(".")) return null;
117
+ return resolveTarget(normalize(`${directoryOf(from)}/${specifier}`), exists);
118
+ };
119
+
120
+ const conditionOrder = ["types", "import", "default"];
121
+
122
+ /** The target of an `exports` or `imports` value, under `types`, `import`, then `default`. */
123
+ export const targetOf = (value: unknown): string | null => {
124
+ if (typeof value === "string") return value;
125
+ if (typeof value !== "object" || value === null) return null;
126
+ const conditions = value as Record<string, unknown>;
127
+ for (const condition of conditionOrder) {
128
+ const target = targetOf(conditions[condition]);
129
+ if (target !== null) return target;
130
+ }
131
+ return null;
132
+ };
133
+
134
+ /** What `*` stands for in `pattern` when it matches `specifier`; "" for an exact match. */
135
+ export const expand = (pattern: string, specifier: string): string | null => {
136
+ const star = pattern.indexOf("*");
137
+ if (star === -1) return pattern === specifier ? "" : null;
138
+ const prefix = pattern.slice(0, star);
139
+ const suffix = pattern.slice(star + 1);
140
+ if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) return null;
141
+ return specifier.slice(prefix.length, specifier.length - suffix.length);
142
+ };
143
+
144
+ /** As node matches them: an exact key, then the pattern with the longest prefix. */
145
+ export const specificity = (pattern: string): number =>
146
+ pattern.includes("*") ? pattern.indexOf("*") : Number.MAX_SAFE_INTEGER;
147
+
148
+ /**
149
+ * A `#` specifier through the `imports` field of the package's own
150
+ * `package.json` (`"#/*": "./src/*"`), then resolved as a relative one. The
151
+ * `types`, `import` and `default` conditions are read, in that order.
152
+ */
153
+ export const resolveSubpathImport = (
154
+ specifier: string,
155
+ { directory, imports }: { directory: string; imports: unknown },
156
+ exists: (path: string) => boolean,
157
+ ): string | null => {
158
+ if (!specifier.startsWith("#")) return null;
159
+ if (typeof imports !== "object" || imports === null) return null;
160
+ const patterns = Object.entries(imports).toSorted(
161
+ ([left], [right]) => specificity(right) - specificity(left),
162
+ );
163
+ for (const [pattern, value] of patterns) {
164
+ const captured = expand(pattern, specifier);
165
+ const target = targetOf(value);
166
+ if (captured === null || target?.startsWith("./") !== true) continue;
167
+ return resolveTarget(
168
+ normalize(`${directory}/${target.replace("*", captured)}`),
169
+ exists,
170
+ );
171
+ }
172
+ return null;
173
+ };
174
+
175
+ const joinPath = (directory: string, name: string): string =>
176
+ directory === "" ? name : `${directory}/${name}`;
177
+
178
+ /** Where the code a test covers sits: beside it, or above its `__tests__`. */
179
+ const coveredDirectoriesOf = (path: string): string[] => {
180
+ const directory = directoryOf(path);
181
+ return directory.split("/").at(-1) === "__tests__"
182
+ ? [directory, directoryOf(directory)]
183
+ : [directory];
184
+ };
185
+
186
+ const attachableKinds = new Set(["spec", "stories", "test"]);
187
+
188
+ /**
189
+ * The files a test or a story may cover, most likely first: every name its
190
+ * own name starts with, before its kind (`a.unit.test.ts` covers `a.unit.ts`,
191
+ * else `a.ts`).
192
+ */
193
+ export const subjectsOf = (path: string): string[] => {
194
+ const parts = path
195
+ .slice(directoryOf(path).length)
196
+ .replace(/^\//, "")
197
+ .replace(/\.[cm]?tsx?$/, "")
198
+ .split(".");
199
+ const kind = parts.findIndex((part) => attachableKinds.has(part));
200
+ const stem = kind === -1 ? parts : parts.slice(0, kind);
201
+ const names = stem.map((_, dropped) =>
202
+ stem.slice(0, stem.length - dropped).join("."),
203
+ );
204
+ return coveredDirectoriesOf(path).flatMap((directory) =>
205
+ names.flatMap((name) =>
206
+ typescriptExtensions.map((extension) =>
207
+ joinPath(directory, name + extension),
208
+ ),
209
+ ),
210
+ );
211
+ };
212
+
213
+ export interface ReadingOrderInput {
214
+ /** the changed files, by package, in the order the patch names them */
215
+ groups: DiffGroup[];
216
+ /**
217
+ * The TypeScript files each read file imports, resolved, in source order:
218
+ * the changed files and the unchanged ones read on the way between them. A
219
+ * file absent from it was not read.
220
+ */
221
+ imports: ReadonlyMap<string, readonly string[]>;
222
+ /** the package directories each package directory depends on */
223
+ dependsOn: ReadonlyMap<string, ReadonlySet<string>>;
224
+ }
225
+
226
+ interface Reach {
227
+ /** the changed files of the package being ordered */
228
+ code: ReadonlySet<string>;
229
+ /** every changed file, of any package */
230
+ changed: ReadonlySet<string>;
231
+ imports: ReadonlyMap<string, readonly string[]>;
232
+ }
233
+
234
+ /**
235
+ * The files of `code` that `path` reaches, in import order: through the
236
+ * unchanged files it imports, never past a changed one.
237
+ */
238
+ const reachedFrom = (
239
+ path: string,
240
+ { code, changed, imports }: Reach,
241
+ ): string[] => {
242
+ const reached: string[] = [];
243
+ const visited = new Set([path]);
244
+ const visit = (from: string): void => {
245
+ for (const target of imports.get(from) ?? []) {
246
+ if (visited.has(target)) continue;
247
+ visited.add(target);
248
+ if (code.has(target)) reached.push(target);
249
+ else if (!changed.has(target)) visit(target);
250
+ }
251
+ };
252
+ visit(path);
253
+ return reached;
254
+ };
255
+
256
+ const countReachable = (
257
+ entry: string,
258
+ edges: ReadonlyMap<string, readonly string[]>,
259
+ ): number => {
260
+ const seen = new Set([entry]);
261
+ const stack = [entry];
262
+ while (stack.length > 0) {
263
+ for (const target of edges.get(stack.pop()!) ?? []) {
264
+ if (seen.has(target)) continue;
265
+ seen.add(target);
266
+ stack.push(target);
267
+ }
268
+ }
269
+ return seen.size - 1;
270
+ };
271
+
272
+ /**
273
+ * Its subject by name; else, of the changed files it imports, one beside it
274
+ * (a test imports shared helpers, a logger, before what it tests); else the
275
+ * first.
276
+ */
277
+ const attachTo = (
278
+ attachable: string,
279
+ code: ReadonlySet<string>,
280
+ reached: readonly string[],
281
+ ): string | null => {
282
+ const covered = coveredDirectoriesOf(attachable);
283
+ return (
284
+ subjectsOf(attachable).find((subject) => code.has(subject)) ??
285
+ reached.find((target) => covered.includes(directoryOf(target))) ??
286
+ reached[0] ??
287
+ null
288
+ );
289
+ };
290
+
291
+ const orderGroup = (
292
+ { name, paths }: DiffGroup,
293
+ changed: ReadonlySet<string>,
294
+ imports: ReadonlyMap<string, readonly string[]>,
295
+ ): DiffReadingGroup => {
296
+ const code = new Set(
297
+ paths.filter((path) => isTypescriptPath(path) && !isAttachablePath(path)),
298
+ );
299
+ const attachables = paths.filter(isAttachablePath);
300
+ const others = paths.filter((path) => !isTypescriptPath(path));
301
+ const reach: Reach = { code, changed, imports };
302
+
303
+ const edges = new Map(
304
+ [...code].map((path) => [path, reachedFrom(path, reach)]),
305
+ );
306
+ const imported = new Set([...edges.values()].flat());
307
+
308
+ const attached = new Map<string, string[]>();
309
+ const standalone: string[] = [];
310
+ for (const attachable of attachables) {
311
+ const subject = attachTo(attachable, code, reachedFrom(attachable, reach));
312
+ if (subject === null) standalone.push(attachable);
313
+ else attached.set(subject, [...(attached.get(subject) ?? []), attachable]);
314
+ }
315
+
316
+ const reachable = new Map(
317
+ [...code].map((path) => [path, countReachable(path, edges)]),
318
+ );
319
+ const byPath = (left: string, right: string): number =>
320
+ left.localeCompare(right);
321
+ const roots = [...code]
322
+ .filter((path) => !imported.has(path))
323
+ .toSorted(
324
+ (left, right) =>
325
+ reachable.get(right)! - reachable.get(left)! || byPath(left, right),
326
+ );
327
+
328
+ const ordered: string[] = [];
329
+ const visited = new Set<string>();
330
+ const visit = (path: string): void => {
331
+ if (visited.has(path)) return;
332
+ visited.add(path);
333
+ ordered.push(path);
334
+ for (const target of edges.get(path) ?? []) visit(target);
335
+ };
336
+ for (const root of roots) visit(root);
337
+ // what only a cycle imports has no root to be reached from
338
+ for (const path of [...code].toSorted(byPath)) visit(path);
339
+
340
+ const entries = new Set(
341
+ roots.filter((root) => (edges.get(root) ?? []).length > 0),
342
+ );
343
+ const rootPaths = new Set(roots);
344
+ const codeFile = (path: string): DiffReadingFile => ({
345
+ path,
346
+ entry: entries.has(path),
347
+ root: rootPaths.has(path),
348
+ attached: (attached.get(path) ?? []).toSorted(byPath),
349
+ });
350
+ const plainFile = (path: string): DiffReadingFile => ({
351
+ path,
352
+ entry: false,
353
+ root: false,
354
+ attached: [],
355
+ });
356
+
357
+ return {
358
+ name,
359
+ files: [
360
+ ...ordered.map(codeFile),
361
+ ...others.map(plainFile),
362
+ ...standalone.map(plainFile),
363
+ ],
364
+ };
365
+ };
366
+
367
+ const transitiveDependencies = (
368
+ name: string,
369
+ dependsOn: ReadonlyMap<string, ReadonlySet<string>>,
370
+ ): Set<string> => {
371
+ const found = new Set<string>();
372
+ const stack = [name];
373
+ while (stack.length > 0) {
374
+ for (const dependency of dependsOn.get(stack.pop()!) ?? []) {
375
+ if (found.has(dependency) || dependency === name) continue;
376
+ found.add(dependency);
377
+ stack.push(dependency);
378
+ }
379
+ }
380
+ return found;
381
+ };
382
+
383
+ /**
384
+ * The root first, as `groupDiffPaths` has it; then a package before every
385
+ * package it depends on, directly or through a package the change does not
386
+ * touch. Alphabetical between two unrelated packages, and to break a cycle.
387
+ */
388
+ export const orderPackages = (
389
+ names: readonly string[],
390
+ dependsOn: ReadonlyMap<string, ReadonlySet<string>>,
391
+ ): string[] => {
392
+ const remaining = new Set(names.filter((name) => name !== ""));
393
+ const dependencies = new Map(
394
+ [...remaining].map((name) => [
395
+ name,
396
+ transitiveDependencies(name, dependsOn),
397
+ ]),
398
+ );
399
+ const isDependedOnByRemaining = (name: string): boolean =>
400
+ [...remaining].some(
401
+ (other) => other !== name && dependencies.get(other)!.has(name),
402
+ );
403
+
404
+ const ordered = names.includes("") ? [""] : [];
405
+ while (remaining.size > 0) {
406
+ const sorted = [...remaining].toSorted((left, right) =>
407
+ left.localeCompare(right),
408
+ );
409
+ const next =
410
+ sorted.find((name) => !isDependedOnByRemaining(name)) ?? sorted[0]!;
411
+ ordered.push(next);
412
+ remaining.delete(next);
413
+ }
414
+ return ordered;
415
+ };
416
+
417
+ export const orderForReading = ({
418
+ groups,
419
+ imports,
420
+ dependsOn,
421
+ }: ReadingOrderInput): DiffReadingGroup[] => {
422
+ const byName = new Map(groups.map((group) => [group.name, group]));
423
+ const changed = new Set(groups.flatMap(({ paths }) => paths));
424
+ return orderPackages(
425
+ groups.map(({ name }) => name),
426
+ dependsOn,
427
+ ).map((name) => orderGroup(byName.get(name)!, changed, imports));
428
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The diff's syntax theme, as a family: each names a shiki theme for the light
3
+ * mode and one for the dark mode, so the diff follows the app's mode whichever
4
+ * is picked. Plain data, so a server can validate a stored setting against the
5
+ * same list the ui renders.
6
+ */
7
+ export const diffThemes = [
8
+ "github",
9
+ "github-high-contrast",
10
+ "pierre",
11
+ "vscode",
12
+ "one",
13
+ "catppuccin",
14
+ "rose-pine",
15
+ "solarized",
16
+ "gruvbox",
17
+ "everforest",
18
+ "kanagawa",
19
+ "vitesse",
20
+ "ayu",
21
+ "material",
22
+ "night-owl",
23
+ "min",
24
+ ] as const;
25
+
26
+ export type DiffTheme = (typeof diffThemes)[number];
27
+
28
+ export const isDiffTheme = (value: unknown): value is DiffTheme =>
29
+ (diffThemes as readonly unknown[]).includes(value);
30
+
31
+ interface DiffThemeFamily {
32
+ label: string;
33
+ /** shiki theme names, the pierre ones resolved by `@pierre/diffs` itself */
34
+ themes: { light: string; dark: string };
35
+ }
36
+
37
+ const families: Record<DiffTheme, DiffThemeFamily> = {
38
+ github: {
39
+ label: "GitHub",
40
+ themes: { light: "github-light-default", dark: "github-dark-default" },
41
+ },
42
+ "github-high-contrast": {
43
+ label: "GitHub high contrast",
44
+ themes: {
45
+ light: "github-light-high-contrast",
46
+ dark: "github-dark-high-contrast",
47
+ },
48
+ },
49
+ pierre: {
50
+ label: "Pierre",
51
+ themes: { light: "pierre-light", dark: "pierre-dark" },
52
+ },
53
+ vscode: {
54
+ label: "VS Code",
55
+ themes: { light: "light-plus", dark: "dark-plus" },
56
+ },
57
+ one: { label: "One", themes: { light: "one-light", dark: "one-dark-pro" } },
58
+ catppuccin: {
59
+ label: "Catppuccin",
60
+ themes: { light: "catppuccin-latte", dark: "catppuccin-mocha" },
61
+ },
62
+ "rose-pine": {
63
+ label: "Rosé Pine",
64
+ themes: { light: "rose-pine-dawn", dark: "rose-pine" },
65
+ },
66
+ solarized: {
67
+ label: "Solarized",
68
+ themes: { light: "solarized-light", dark: "solarized-dark" },
69
+ },
70
+ gruvbox: {
71
+ label: "Gruvbox",
72
+ themes: { light: "gruvbox-light-medium", dark: "gruvbox-dark-medium" },
73
+ },
74
+ everforest: {
75
+ label: "Everforest",
76
+ themes: { light: "everforest-light", dark: "everforest-dark" },
77
+ },
78
+ kanagawa: {
79
+ label: "Kanagawa",
80
+ themes: { light: "kanagawa-lotus", dark: "kanagawa-wave" },
81
+ },
82
+ vitesse: {
83
+ label: "Vitesse",
84
+ themes: { light: "vitesse-light", dark: "vitesse-dark" },
85
+ },
86
+ ayu: { label: "Ayu", themes: { light: "ayu-light", dark: "ayu-dark" } },
87
+ material: {
88
+ label: "Material",
89
+ themes: { light: "material-theme-lighter", dark: "material-theme" },
90
+ },
91
+ "night-owl": {
92
+ label: "Night Owl",
93
+ themes: { light: "night-owl-light", dark: "night-owl" },
94
+ },
95
+ min: { label: "Min", themes: { light: "min-light", dark: "min-dark" } },
96
+ };
97
+
98
+ export const diffThemeNames = (
99
+ theme: DiffTheme,
100
+ ): { light: string; dark: string } => families[theme].themes;
101
+
102
+ export const diffThemeOptions = diffThemes.map((theme) => ({
103
+ value: theme,
104
+ label: families[theme].label,
105
+ }));