figdiff 0.1.0-alpha.0 → 0.1.0-alpha.1

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 (46) hide show
  1. package/README.md +67 -19
  2. package/dist/action/changed-files.d.ts +14 -0
  3. package/dist/action/changed-files.js +35 -0
  4. package/dist/action/changed-files.js.map +1 -0
  5. package/dist/action/comment.d.ts +28 -0
  6. package/dist/action/comment.js +70 -0
  7. package/dist/action/comment.js.map +1 -0
  8. package/dist/action/github.d.ts +15 -0
  9. package/dist/action/github.js +62 -0
  10. package/dist/action/github.js.map +1 -0
  11. package/dist/action/glob.d.ts +10 -0
  12. package/dist/action/glob.js +45 -0
  13. package/dist/action/glob.js.map +1 -0
  14. package/dist/action/main.d.ts +20 -0
  15. package/dist/action/main.js +111 -0
  16. package/dist/action/main.js.map +1 -0
  17. package/dist/action.d.ts +2 -0
  18. package/dist/action.js +7 -0
  19. package/dist/action.js.map +1 -0
  20. package/dist/cli.js +16 -2
  21. package/dist/cli.js.map +1 -1
  22. package/dist/git.d.ts +12 -0
  23. package/dist/git.js +32 -0
  24. package/dist/git.js.map +1 -0
  25. package/dist/html.d.ts +25 -0
  26. package/dist/html.js +205 -0
  27. package/dist/html.js.map +1 -0
  28. package/dist/index.d.ts +12 -0
  29. package/dist/index.js +6 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/render/compare.d.ts +19 -0
  32. package/dist/render/compare.js +61 -0
  33. package/dist/render/compare.js.map +1 -0
  34. package/dist/render/figcraft-cli.d.ts +21 -0
  35. package/dist/render/figcraft-cli.js +63 -0
  36. package/dist/render/figcraft-cli.js.map +1 -0
  37. package/dist/render/frames.d.ts +19 -0
  38. package/dist/render/frames.js +53 -0
  39. package/dist/render/frames.js.map +1 -0
  40. package/dist/render/png.d.ts +14 -0
  41. package/dist/render/png.js +185 -0
  42. package/dist/render/png.js.map +1 -0
  43. package/dist/run.d.ts +18 -3
  44. package/dist/run.js +117 -17
  45. package/dist/run.js.map +1 -1
  46. package/package.json +3 -2
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  `figdiff` compares two versions of a [FigCraft](https://github.com/storytold/figcraft) design file (`.figcraft`) and explains what changed, both as a readable list of changes and as before/after images.
6
6
 
7
- > **Status: early development (pre-alpha).** Phase 0 is in progress. The parser and a basic text diff exist (build with `npm run build`, then run `node dist/cli.js a.figcraft b.figcraft`); the HTML report, visual diff and GitHub Action are still planned. Commands in the usage section below are the intended interface and may change. Feedback is very welcome: please open an issue.
7
+ > **Status: early development (pre-alpha).** Phase 0 is in progress. The parser and a basic text diff exist and are published as a pre-alpha on npm (see [Try it](#try-it)); the HTML report, visual diff and GitHub Action are still planned. Commands in the usage section below are the intended interface and may change. Feedback is very welcome: please open an issue.
8
8
 
9
9
  > **Not affiliated.** figdiff is an independent community project. It is not made by, endorsed by, or connected to the FigCraft / ArtCraft authors. FigCraft is a trademark of its respective owners.
10
10
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  ## Table of contents
14
14
 
15
+ - [Try it](#try-it)
15
16
  - [The problem](#the-problem)
16
17
  - [The idea](#the-idea)
17
18
  - [Planned usage](#planned-usage)
@@ -24,6 +25,20 @@
24
25
 
25
26
  ---
26
27
 
28
+ ## Try it
29
+
30
+ The first pre-alpha is on npm under the `alpha` tag. It needs Node.js 20 or newer.
31
+
32
+ ```sh
33
+ npx figdiff@alpha before.figcraft after.figcraft
34
+ ```
35
+
36
+ To install it globally instead: `npm install -g figdiff@alpha`.
37
+
38
+ Today it compares two files and prints a grouped list of changes (`--json` and `--exit-code` are available). The HTML report, visual diff and GitHub Action are on the roadmap below. Matching of nodes relies on ids staying stable between saves in FigCraft, which is not yet verified, so treat results as preliminary.
39
+
40
+ ---
41
+
27
42
  ## The problem
28
43
 
29
44
  A `.figcraft` file is a zip archive. When it changes in a Git repository, GitHub and `git diff` can only say "binary file changed". A reviewer has no way to tell whether a designer moved one button or rebuilt a whole screen, short of opening the file in FigCraft and comparing by eye.
@@ -61,7 +76,7 @@ Page "App"
61
76
  4 changes in 2 frames (1 removed, 3 property changes)
62
77
  ```
63
78
 
64
- Useful flags: `--json` prints the summary and every change as JSON, and `--exit-code` exits with 1 when the files differ, so it can fail a CI step.
79
+ Useful flags: `--html report.html` also writes a self-contained HTML report (add `--images` for rendered before/after images), `--json` prints the summary and every change as JSON, and `--exit-code` exits with 1 when the files differ, so it can fail a CI step.
65
80
 
66
81
  **Generate a shareable report**
67
82
 
@@ -69,7 +84,7 @@ Useful flags: `--json` prints the summary and every change as JSON, and `--exit-
69
84
  npx figdiff before.figcraft after.figcraft --html report.html
70
85
  ```
71
86
 
72
- One self-contained HTML file with before/after images, a slider, and the list of changes. No server needed, so it can be sent by email or chat to a developer or client.
87
+ One self-contained HTML file with the list of changes, filters, and light and dark mode. No server needed, so it can be sent by email or chat to a developer or client. With `--images` (and FigCraft's `figcraft-cli` installed) it also embeds before/after images with a swipe slider and a difference view. This part is experimental: it has been tested with a stand-in for `figcraft-cli` but not yet with the real one.
73
88
 
74
89
  **Compare against another Git branch**
75
90
 
@@ -77,25 +92,54 @@ One self-contained HTML file with before/after images, a slider, and the list of
77
92
  npx figdiff design/app.figcraft --against main
78
93
  ```
79
94
 
95
+ `--against` accepts any git revision (a branch, tag, or commit such as `HEAD~1`). The file is compared with its committed version, so it also works for uncommitted edits: `--against HEAD` shows what you changed since the last commit.
96
+
97
+ **Use it as a `git difftool`**
98
+
99
+ ```sh
100
+ git config difftool.figdiff.cmd 'figdiff "$LOCAL" "$REMOTE"'
101
+ git difftool -y -t figdiff main -- design/app.figcraft
102
+ ```
103
+
80
104
  **Run on every pull request**
81
105
 
106
+ The GitHub Action (`action.yml`, currently in alpha) posts one comment per pull request with what changed in each `.figcraft` file, and keeps that comment up to date on every push. Full HTML reports are attached to the workflow run.
107
+
82
108
  ```yaml
83
- # .github/workflows/design-diff.yml
84
- name: Design diff
85
- on: pull_request
109
+ # .github/workflows/figdiff.yml (also in examples/figdiff.yml)
110
+ name: Design changes
111
+ on:
112
+ pull_request:
113
+ paths: ["**/*.figcraft"]
114
+
115
+ permissions:
116
+ contents: read
117
+ pull-requests: write
86
118
 
87
119
  jobs:
88
- diff:
120
+ figdiff:
89
121
  runs-on: ubuntu-latest
90
122
  steps:
91
123
  - uses: actions/checkout@v4
92
124
  with:
93
- fetch-depth: 0
94
- - uses: bintangyoga/figdiff@v1
125
+ fetch-depth: 0 # needed to read the base commit
126
+ - uses: bintangyoga/figdiff@main
95
127
  with:
96
128
  files: "design/**/*.figcraft"
97
129
  ```
98
130
 
131
+ | Input | Default | Meaning |
132
+ | --- | --- | --- |
133
+ | `files` | `**/*.figcraft` | Glob patterns (commas or new lines) of the design files to look at |
134
+ | `comment` | `true` | `false` writes the reports without commenting |
135
+ | `max-files` | `20` | Compare at most this many files per pull request |
136
+ | `upload-reports` | `true` | Upload the HTML reports as the artifact `figdiff-reports` |
137
+ | `report-dir` | `figdiff-reports` | Where the reports are written |
138
+ | `version` | `latest` | Version of the `figdiff` npm package to use |
139
+ | `token` | `github.token` | Token used to comment |
140
+
141
+ Pull requests from forks get a read-only token, so no comment is posted; the changes are printed in the job log instead. Images in the comment are not included yet (see Phase 3).
142
+
99
143
  ## How it will work
100
144
 
101
145
  ```text
@@ -176,13 +220,17 @@ Deliver `figdiff a.figcraft b.figcraft` printing a correct list of changes in th
176
220
 
177
221
  Deliver a self-contained HTML report that anyone can open, including designers who do not use Git.
178
222
 
179
- - [ ] Render changed frames from both versions with `figcraft-cli export --node` at 1x and 2x
180
- - [ ] Pixel comparison with highlighted changed areas
181
- - [ ] Per-frame views: side by side, swipe slider, and difference overlay
223
+ - [x] Self-contained HTML report of the semantic diff (`--html report.html`), with filters and light/dark mode
224
+ - [x] Before/after swipe slider and a difference view with the share of changed pixels
225
+ - [x] Pixel comparison with highlighted changed areas (own PNG decoder, no dependencies; checked against real FigCraft PNGs)
226
+ - [x] A single `report.html` with embedded images and no server
227
+ - [x] Graceful fallback when `figcraft-cli` is unavailable: the report is still written, with a warning
228
+ - [x] Frames that exist in only one version (added or removed) are shown on their own
229
+ - [ ] Verify rendering against a real `figcraft-cli` (implemented from its documentation and tested with a stand-in; the exact `--node` and `--page` behavior is unconfirmed)
230
+ - [ ] Render at 2x as well as 1x
231
+ - [x] Side-by-side view next to the swipe slider
182
232
  - [ ] Semantic change list next to the images; clicking a change highlights its location
183
- - [ ] A single `report.html` with embedded images and no server
184
- - [ ] Graceful fallback when `figcraft-cli` is unavailable: semantic diff only, with a clear message
185
- - [ ] `git difftool` integration and `--against <branch>` support
233
+ - [x] `--against <revision>` (compare a file with its version on a branch, tag or commit) and `git difftool` usage
186
234
 
187
235
  **Release:** `v0.2.0`, with the first demo GIF in this README.
188
236
 
@@ -192,12 +240,12 @@ Deliver a self-contained HTML report that anyone can open, including designers w
192
240
 
193
241
  Deliver an automatic comment on every pull request that changes a `.figcraft` file, set up with one workflow file.
194
242
 
195
- - [ ] Detect changed `.figcraft` files in a pull request and check out both the base and head versions
243
+ - [x] Detect changed `.figcraft` files in a pull request and compare each with its base version
196
244
  - [ ] Install `figcraft-cli` on the runner, with caching
197
- - [ ] Pull request comment with a summary, per-frame changes, and before/after images
245
+ - [x] Pull request comment with a summary and per-frame changes (before/after images still to do)
198
246
  - [ ] Host images via workflow artifacts or a dedicated branch (GitHub comments need image URLs)
199
- - [ ] Update a single comment on each push instead of posting new ones
200
- - [ ] Configuration options: file patterns, render scale, frame limit, comment language
247
+ - [x] Update a single comment on each push instead of posting new ones
248
+ - [x] Configuration options: file patterns, file limit, report folder (render scale and comment language still to do)
201
249
  - [ ] Public example repository that uses the Action as a live demo
202
250
 
203
251
  **Release:** `v0.3.0` and a listing on GitHub Marketplace.
@@ -0,0 +1,14 @@
1
+ export interface ChangedFile {
2
+ status: "added" | "modified" | "deleted" | "renamed";
3
+ /** Path in the new version (or the old one, for a deleted file). */
4
+ path: string;
5
+ /** Path in the base version, for a renamed file. */
6
+ oldPath?: string;
7
+ }
8
+ /**
9
+ * Parse the output of `git diff --name-status -z`. Entries are separated by NUL characters;
10
+ * renames and copies carry two paths.
11
+ */
12
+ export declare function parseNameStatus(output: string): ChangedFile[];
13
+ /** Keep the files that match one of the patterns (a rename matches on either of its two paths). */
14
+ export declare function filterByPatterns(files: ChangedFile[], patterns: string[]): ChangedFile[];
@@ -0,0 +1,35 @@
1
+ import { matchesAny } from "./glob.js";
2
+ /**
3
+ * Parse the output of `git diff --name-status -z`. Entries are separated by NUL characters;
4
+ * renames and copies carry two paths.
5
+ */
6
+ export function parseNameStatus(output) {
7
+ const tokens = output.split("\0");
8
+ const files = [];
9
+ for (let i = 0; i < tokens.length; i++) {
10
+ const code = tokens[i];
11
+ if (code === "")
12
+ continue;
13
+ const kind = code[0];
14
+ if (kind === "R") {
15
+ const oldPath = tokens[++i];
16
+ const path = tokens[++i];
17
+ files.push({ status: "renamed", path, oldPath });
18
+ }
19
+ else if (kind === "C") {
20
+ i++; // the copy source is not interesting
21
+ files.push({ status: "added", path: tokens[++i] });
22
+ }
23
+ else {
24
+ const path = tokens[++i];
25
+ const status = kind === "A" ? "added" : kind === "D" ? "deleted" : "modified";
26
+ files.push({ status, path });
27
+ }
28
+ }
29
+ return files;
30
+ }
31
+ /** Keep the files that match one of the patterns (a rename matches on either of its two paths). */
32
+ export function filterByPatterns(files, patterns) {
33
+ return files.filter((f) => matchesAny(f.path, patterns) || (f.oldPath !== undefined && matchesAny(f.oldPath, patterns)));
34
+ }
35
+ //# sourceMappingURL=changed-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changed-files.js","sourceRoot":"","sources":["../../src/action/changed-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAUvC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,KAAK,GAAkB,EAAE,CAAC;IAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAW,CAAC;QACjC,IAAI,IAAI,KAAK,EAAE;YAAE,SAAS;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjB,MAAM,OAAO,GAAG,MAAM,CAAC,EAAE,CAAC,CAAW,CAAC;YACtC,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,CAAW,CAAC;YACnC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACnD,CAAC;aAAM,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACxB,CAAC,EAAE,CAAC,CAAC,qCAAqC;YAC1C,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAW,EAAE,CAAC,CAAC;QAC/D,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,CAAW,CAAC;YACnC,MAAM,MAAM,GAAG,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC;YAC9E,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,gBAAgB,CAAC,KAAoB,EAAE,QAAkB;IACvE,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,UAAU,CAAC,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC3H,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { Summary } from "../diff/types.js";
2
+ import type { ChangedFile } from "./changed-files.js";
3
+ /** Hidden marker used to find our own comment again on later pushes. */
4
+ export declare const COMMENT_MARKER = "<!-- figdiff-comment -->";
5
+ export interface FileResult {
6
+ file: ChangedFile;
7
+ /** Present when the two versions were compared. */
8
+ summary?: Summary;
9
+ /** The text diff, as printed by `figdiff`. */
10
+ text?: string;
11
+ /** Why the comparison could not be made. */
12
+ error?: string;
13
+ /** Name of the HTML report written for this file, if any. */
14
+ reportName?: string;
15
+ }
16
+ export interface CommentContext {
17
+ /** Link to the workflow run, where the report artifact can be downloaded. */
18
+ runUrl?: string;
19
+ /** Whether the HTML reports were uploaded as an artifact. */
20
+ reportsUploaded: boolean;
21
+ version?: string;
22
+ }
23
+ /** A file counts as changed unless it was only re-saved without any design difference. */
24
+ export declare function isSubstantive(r: FileResult): boolean;
25
+ /** The pull request comment for a set of compared files. */
26
+ export declare function buildComment(results: FileResult[], ctx: CommentContext): string;
27
+ /** Replaces an earlier comment when a later push no longer changes any design file. */
28
+ export declare function buildEmptyComment(): string;
@@ -0,0 +1,70 @@
1
+ import { formatSummary } from "../report.js";
2
+ /** Hidden marker used to find our own comment again on later pushes. */
3
+ export const COMMENT_MARKER = "<!-- figdiff-comment -->";
4
+ /** GitHub rejects comments over 65,536 characters; stay well below. */
5
+ const MAX_COMMENT = 60_000;
6
+ const MAX_FILE_TEXT = 15_000;
7
+ /** A file counts as changed unless it was only re-saved without any design difference. */
8
+ export function isSubstantive(r) {
9
+ if (r.error)
10
+ return true;
11
+ if (r.file.status === "added" || r.file.status === "deleted" || r.file.status === "renamed")
12
+ return true;
13
+ return (r.summary?.changes ?? 0) > 0;
14
+ }
15
+ /** Wrap text in a code span, choosing a delimiter that cannot be broken by backticks inside. */
16
+ function code(text) {
17
+ const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((m) => m.length));
18
+ const fence = "`".repeat(longest + 1);
19
+ return `${fence}${text.startsWith("`") || text.endsWith("`") ? ` ${text} ` : text}${fence}`;
20
+ }
21
+ /** A fenced code block whose fence is longer than any backtick run inside. */
22
+ function block(text) {
23
+ const longest = Math.max(2, ...(text.match(/`+/g) ?? []).map((m) => m.length));
24
+ const fence = "`".repeat(longest + 1);
25
+ return `${fence}text\n${text.replace(/\n$/, "")}\n${fence}`;
26
+ }
27
+ function truncate(text, limit) {
28
+ if (text.length <= limit)
29
+ return text;
30
+ const cut = text.slice(0, limit);
31
+ const kept = cut.slice(0, Math.max(cut.lastIndexOf("\n"), 0));
32
+ const hidden = text.slice(kept.length).split("\n").filter(Boolean).length;
33
+ return `${kept}\n… ${hidden} more lines not shown`;
34
+ }
35
+ function section(r) {
36
+ const { file } = r;
37
+ const name = code(file.path);
38
+ switch (file.status) {
39
+ case "added":
40
+ return `**${name}**: new design file, nothing earlier to compare with.`;
41
+ case "deleted":
42
+ return `**${name}**: design file deleted.`;
43
+ default:
44
+ }
45
+ const renamed = file.status === "renamed" ? ` (renamed from ${code(file.oldPath ?? "")})` : "";
46
+ if (r.error)
47
+ return `**${name}**${renamed}: could not be compared. ${r.error}`;
48
+ if (!r.summary || r.summary.changes === 0) {
49
+ return `**${name}**${renamed}: ${file.status === "renamed" ? "renamed, no design changes." : "saved again, no design changes."}`;
50
+ }
51
+ const details = r.text
52
+ ? `\n\n<details><summary>Show changes</summary>\n\n${block(truncate(r.text, MAX_FILE_TEXT))}\n\n</details>`
53
+ : "";
54
+ return `**${name}**${renamed}: ${formatSummary(r.summary)}${details}`;
55
+ }
56
+ /** The pull request comment for a set of compared files. */
57
+ export function buildComment(results, ctx) {
58
+ const parts = [`${COMMENT_MARKER}\n## Design changes`, ...results.map(section)];
59
+ if (ctx.reportsUploaded && ctx.runUrl && results.some((r) => r.reportName)) {
60
+ parts.push(`Full HTML reports (with filters and, if enabled, images) are attached to [this workflow run](${ctx.runUrl}) as the artifact **figdiff-reports**.`);
61
+ }
62
+ parts.push(`<sub>Generated by figdiff${ctx.version ? ` ${ctx.version}` : ""}. Changes are matched by node id.</sub>`);
63
+ const body = parts.join("\n\n");
64
+ return body.length <= MAX_COMMENT ? body : `${body.slice(0, MAX_COMMENT)}\n\n… (comment shortened)`;
65
+ }
66
+ /** Replaces an earlier comment when a later push no longer changes any design file. */
67
+ export function buildEmptyComment() {
68
+ return `${COMMENT_MARKER}\n## Design changes\n\nThis pull request no longer changes any design file.`;
69
+ }
70
+ //# sourceMappingURL=comment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment.js","sourceRoot":"","sources":["../../src/action/comment.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /** An error from the GitHub API, with the HTTP status so callers can tell "no permission" apart. */
2
+ export declare class GitHubError extends Error {
3
+ readonly status: number;
4
+ constructor(message: string, status: number);
5
+ }
6
+ export interface GitHubApi {
7
+ fetch: typeof fetch;
8
+ /** For example https://api.github.com */
9
+ baseUrl: string;
10
+ token: string;
11
+ }
12
+ /** Find the comment figdiff wrote earlier on this pull request, if any. */
13
+ export declare function findComment(api: GitHubApi, repo: string, pr: number): Promise<number | undefined>;
14
+ /** Create the comment, or update the one found earlier so a pull request only ever has one. */
15
+ export declare function upsertComment(api: GitHubApi, repo: string, pr: number, body: string, existing?: number): Promise<"created" | "updated">;
@@ -0,0 +1,62 @@
1
+ import { COMMENT_MARKER } from "./comment.js";
2
+ /** An error from the GitHub API, with the HTTP status so callers can tell "no permission" apart. */
3
+ export class GitHubError extends Error {
4
+ status;
5
+ constructor(message, status) {
6
+ super(message);
7
+ this.status = status;
8
+ }
9
+ }
10
+ async function request(api, method, path, body) {
11
+ const res = await api.fetch(`${api.baseUrl}${path}`, {
12
+ method,
13
+ headers: {
14
+ authorization: `Bearer ${api.token}`,
15
+ accept: "application/vnd.github+json",
16
+ "x-github-api-version": "2022-11-28",
17
+ "user-agent": "figdiff-action",
18
+ ...(body === undefined ? {} : { "content-type": "application/json" }),
19
+ },
20
+ body: body === undefined ? undefined : JSON.stringify(body),
21
+ });
22
+ if (!res.ok) {
23
+ let detail = "";
24
+ try {
25
+ detail = (await res.json()).message ?? "";
26
+ }
27
+ catch {
28
+ // the error body was not JSON
29
+ }
30
+ throw new GitHubError(`GitHub API ${method} ${path} failed with ${res.status}${detail ? `: ${detail}` : ""}`, res.status);
31
+ }
32
+ return (res.status === 204 ? undefined : await res.json());
33
+ }
34
+ /** Find the comment figdiff wrote earlier on this pull request, if any. */
35
+ export async function findComment(api, repo, pr) {
36
+ for (let page = 1; page <= 10; page++) {
37
+ const comments = await request(api, "GET", `/repos/${repo}/issues/${pr}/comments?per_page=100&page=${page}`);
38
+ const found = comments.find((c) => c.body?.includes(COMMENT_MARKER));
39
+ if (found)
40
+ return found.id;
41
+ if (comments.length < 100)
42
+ return undefined;
43
+ }
44
+ return undefined;
45
+ }
46
+ /** Create the comment, or update the one found earlier so a pull request only ever has one. */
47
+ export async function upsertComment(api, repo, pr, body, existing) {
48
+ if (existing !== undefined) {
49
+ try {
50
+ await request(api, "PATCH", `/repos/${repo}/issues/comments/${existing}`, { body });
51
+ return "updated";
52
+ }
53
+ catch (err) {
54
+ // The old comment may have been deleted or belong to someone else: fall through and post a new one.
55
+ if (!(err instanceof GitHubError) || (err.status !== 404 && err.status !== 403))
56
+ throw err;
57
+ }
58
+ }
59
+ await request(api, "POST", `/repos/${repo}/issues/${pr}/comments`, { body });
60
+ return "created";
61
+ }
62
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/action/github.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /** Split an input such as "design/**\/*.figcraft, assets/*.figcraft" (commas or new lines) into patterns. */
2
+ export declare function parsePatterns(input: string): string[];
3
+ /**
4
+ * Convert a glob to a regular expression. Supports `*` (within a folder), `**` (across folders)
5
+ * and `?`. `**\/` also matches no folder at all, so `design/**\/*.figcraft` matches
6
+ * `design/app.figcraft`.
7
+ */
8
+ export declare function globToRegExp(pattern: string): RegExp;
9
+ /** True when the path matches at least one pattern. */
10
+ export declare function matchesAny(path: string, patterns: string[]): boolean;
@@ -0,0 +1,45 @@
1
+ /** Split an input such as "design/**\/*.figcraft, assets/*.figcraft" (commas or new lines) into patterns. */
2
+ export function parsePatterns(input) {
3
+ return input
4
+ .split(/[\n,]/)
5
+ .map((p) => p.trim().replace(/^\.\//, ""))
6
+ .filter((p) => p.length > 0);
7
+ }
8
+ /**
9
+ * Convert a glob to a regular expression. Supports `*` (within a folder), `**` (across folders)
10
+ * and `?`. `**\/` also matches no folder at all, so `design/**\/*.figcraft` matches
11
+ * `design/app.figcraft`.
12
+ */
13
+ export function globToRegExp(pattern) {
14
+ let source = "";
15
+ for (let i = 0; i < pattern.length; i++) {
16
+ const c = pattern[i];
17
+ if (c === "*") {
18
+ if (pattern[i + 1] === "*") {
19
+ i++;
20
+ if (pattern[i + 1] === "/") {
21
+ i++;
22
+ source += "(?:.*/)?";
23
+ }
24
+ else {
25
+ source += ".*";
26
+ }
27
+ }
28
+ else {
29
+ source += "[^/]*";
30
+ }
31
+ }
32
+ else if (c === "?") {
33
+ source += "[^/]";
34
+ }
35
+ else {
36
+ source += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
37
+ }
38
+ }
39
+ return new RegExp(`^${source}$`);
40
+ }
41
+ /** True when the path matches at least one pattern. */
42
+ export function matchesAny(path, patterns) {
43
+ return patterns.some((p) => globToRegExp(p).test(path));
44
+ }
45
+ //# sourceMappingURL=glob.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glob.js","sourceRoot":"","sources":["../../src/action/glob.ts"],"names":[],"mappings":"AAAA,6GAA6G;AAC7G,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAO,KAAK;SACT,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;SACzC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAW,CAAC;QAC/B,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACd,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC3B,CAAC,EAAE,CAAC;gBACJ,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBAC3B,CAAC,EAAE,CAAC;oBACJ,MAAM,IAAI,UAAU,CAAC;gBACvB,CAAC;qBAAM,CAAC;oBACN,MAAM,IAAI,IAAI,CAAC;gBACjB,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,OAAO,CAAC;YACpB,CAAC;QACH,CAAC;aAAM,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACrB,MAAM,IAAI,MAAM,CAAC;QACnB,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC;AACnC,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,QAAkB;IACzD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { run as runFigdiff } from "../run.js";
2
+ export interface ActionDeps {
3
+ git(args: string[], cwd: string): string;
4
+ readText(path: string): string;
5
+ writeText(path: string, content: string): void;
6
+ mkdirp(path: string): void;
7
+ fetch: typeof fetch;
8
+ run: typeof runFigdiff;
9
+ }
10
+ export interface ActionResult {
11
+ code: number;
12
+ log: string[];
13
+ comment?: "created" | "updated" | "skipped";
14
+ /** Number of design files with real changes. */
15
+ changed: number;
16
+ }
17
+ type Env = Record<string, string | undefined>;
18
+ /** The entry point of the GitHub Action. Everything it needs comes in through `env` and `deps`. */
19
+ export declare function runAction(env: Env, overrides?: Partial<ActionDeps>): Promise<ActionResult>;
20
+ export {};
@@ -0,0 +1,111 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import { formatText } from "../report.js";
5
+ import { run as runFigdiff } from "../run.js";
6
+ import { filterByPatterns, parseNameStatus } from "./changed-files.js";
7
+ import { buildComment, buildEmptyComment, isSubstantive } from "./comment.js";
8
+ import { parsePatterns } from "./glob.js";
9
+ import { findComment, GitHubError, upsertComment } from "./github.js";
10
+ const defaultDeps = {
11
+ git: (args, cwd) => execFileSync("git", args, { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }),
12
+ readText: (p) => readFileSync(p, "utf8"),
13
+ writeText: (p, c) => writeFileSync(p, c),
14
+ mkdirp: (p) => void mkdirSync(p, { recursive: true }),
15
+ fetch: (...a) => fetch(...a),
16
+ run: runFigdiff,
17
+ };
18
+ function input(env, name, fallback) {
19
+ const v = env[`INPUT_${name.toUpperCase().replace(/-/g, "_")}`];
20
+ return v !== undefined && v.trim() !== "" ? v.trim() : fallback;
21
+ }
22
+ /** Compare one modified file with its version at the base commit. */
23
+ function compareFile(file, root, base, reportDir, deps) {
24
+ const reportName = `${file.path.replace(/[^A-Za-z0-9._-]+/g, "_")}.html`;
25
+ const result = deps.run([join(root, file.path), "--against", base, "--json", "--html", join(reportDir, reportName)]);
26
+ if (result.code !== 0) {
27
+ const message = result.stderr.replace(/^figdiff:\s*/gm, "").trim().split("\n")[0] ?? "unknown error";
28
+ return { file, error: message };
29
+ }
30
+ for (const w of result.writes) {
31
+ deps.mkdirp(reportDir);
32
+ deps.writeText(w.path, w.content);
33
+ }
34
+ const parsed = JSON.parse(result.stdout);
35
+ return { file, summary: parsed.summary, text: formatText(parsed.changes), reportName };
36
+ }
37
+ /** The entry point of the GitHub Action. Everything it needs comes in through `env` and `deps`. */
38
+ export async function runAction(env, overrides = {}) {
39
+ const deps = { ...defaultDeps, ...overrides };
40
+ const log = [];
41
+ const done = (code, extra = {}) => ({ code, log, changed: 0, ...extra });
42
+ if (!env.GITHUB_EVENT_PATH) {
43
+ log.push("figdiff: GITHUB_EVENT_PATH is not set. Run this action in a GitHub workflow.");
44
+ return done(2);
45
+ }
46
+ const event = JSON.parse(deps.readText(env.GITHUB_EVENT_PATH));
47
+ const pr = event.pull_request;
48
+ if (!pr) {
49
+ log.push("figdiff: this action only works on pull_request events; nothing to do.");
50
+ return done(0, { comment: "skipped" });
51
+ }
52
+ const root = env.GITHUB_WORKSPACE ?? process.cwd();
53
+ const patterns = parsePatterns(input(env, "files", "**/*.figcraft"));
54
+ const reportDir = input(env, "report-dir", "figdiff-reports");
55
+ const wantComment = input(env, "comment", "true") !== "false";
56
+ const maxFiles = Math.max(1, Number.parseInt(input(env, "max-files", "20"), 10) || 20);
57
+ let listed;
58
+ try {
59
+ listed = parseNameStatus(deps.git(["diff", "--name-status", "-z", "--no-renames", `${pr.base.sha}...${pr.head.sha}`], root));
60
+ }
61
+ catch {
62
+ log.push("figdiff: could not list the changed files. Check out the repository with `fetch-depth: 0` so the base commit is available.");
63
+ return done(2);
64
+ }
65
+ const files = filterByPatterns(listed, patterns);
66
+ log.push(`figdiff: ${files.length} design file(s) changed in this pull request.`);
67
+ const results = [];
68
+ for (const file of files.slice(0, maxFiles)) {
69
+ results.push(file.status === "modified" ? compareFile(file, root, pr.base.sha, reportDir, deps) : { file });
70
+ }
71
+ if (files.length > maxFiles)
72
+ log.push(`figdiff: only the first ${maxFiles} files were compared (max-files).`);
73
+ const changed = results.filter(isSubstantive);
74
+ for (const r of results)
75
+ log.push(`figdiff: ${r.file.path}: ${r.error ?? r.summary?.changes ?? r.file.status}`);
76
+ if (!wantComment)
77
+ return done(0, { comment: "skipped", changed: changed.length });
78
+ const token = env.GITHUB_TOKEN ?? env.INPUT_TOKEN;
79
+ const repo = env.GITHUB_REPOSITORY;
80
+ if (!token || !repo) {
81
+ log.push("figdiff: no token or repository available, so no comment was posted.");
82
+ return done(0, { comment: "skipped", changed: changed.length });
83
+ }
84
+ const api = { fetch: deps.fetch, baseUrl: env.GITHUB_API_URL ?? "https://api.github.com", token };
85
+ try {
86
+ const existing = await findComment(api, repo, pr.number);
87
+ if (changed.length === 0 && existing === undefined) {
88
+ log.push("figdiff: no design changes and no earlier comment; staying quiet.");
89
+ return done(0, { comment: "skipped" });
90
+ }
91
+ const runUrl = env.GITHUB_SERVER_URL && env.GITHUB_RUN_ID ? `${env.GITHUB_SERVER_URL}/${repo}/actions/runs/${env.GITHUB_RUN_ID}` : undefined;
92
+ const body = changed.length === 0
93
+ ? buildEmptyComment()
94
+ : buildComment(changed, { runUrl, reportsUploaded: input(env, "upload-reports", "true") !== "false", version: env.FIGDIFF_VERSION });
95
+ const outcome = await upsertComment(api, repo, pr.number, body, existing);
96
+ log.push(`figdiff: comment ${outcome}.`);
97
+ return done(0, { comment: outcome, changed: changed.length });
98
+ }
99
+ catch (err) {
100
+ if (err instanceof GitHubError && err.status === 403) {
101
+ // Pull requests from forks get a read-only token: report instead of failing the build.
102
+ log.push("figdiff: not allowed to comment (403). On pull requests from forks the token is read-only; the changes are in the job log.");
103
+ for (const r of changed)
104
+ log.push(`${basename(r.file.path)}:\n${r.text ?? r.error ?? r.file.status}`);
105
+ return done(0, { comment: "skipped", changed: changed.length });
106
+ }
107
+ log.push(`figdiff: ${err instanceof Error ? err.message : String(err)}`);
108
+ return done(2, { changed: changed.length });
109
+ }
110
+ }
111
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/action/main.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/action.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runAction } from "./action/main.js";
3
+ const result = await runAction(process.env);
4
+ for (const line of result.log)
5
+ console.log(line);
6
+ process.exitCode = result.code;
7
+ //# sourceMappingURL=action.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action.js","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAC5C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,GAAG;IAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACjD,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC"}