figdiff 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +308 -2
- package/dist/action/changed-files.d.ts +14 -0
- package/dist/action/changed-files.js +35 -0
- package/dist/action/changed-files.js.map +1 -0
- package/dist/action/comment.d.ts +28 -0
- package/dist/action/comment.js +70 -0
- package/dist/action/comment.js.map +1 -0
- package/dist/action/github.d.ts +15 -0
- package/dist/action/github.js +62 -0
- package/dist/action/github.js.map +1 -0
- package/dist/action/glob.d.ts +10 -0
- package/dist/action/glob.js +45 -0
- package/dist/action/glob.js.map +1 -0
- package/dist/action/main.d.ts +20 -0
- package/dist/action/main.js +111 -0
- package/dist/action/main.js.map +1 -0
- package/dist/action.d.ts +2 -0
- package/dist/action.js +7 -0
- package/dist/action.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +21 -0
- package/dist/cli.js.map +1 -0
- package/dist/diff/diff.d.ts +11 -0
- package/dist/diff/diff.js +68 -0
- package/dist/diff/diff.js.map +1 -0
- package/dist/diff/summary.d.ts +3 -0
- package/dist/diff/summary.js +40 -0
- package/dist/diff/summary.js.map +1 -0
- package/dist/diff/types.d.ts +59 -0
- package/dist/diff/types.js +2 -0
- package/dist/diff/types.js.map +1 -0
- package/dist/git.d.ts +12 -0
- package/dist/git.js +32 -0
- package/dist/git.js.map +1 -0
- package/dist/html.d.ts +25 -0
- package/dist/html.js +205 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/document.d.ts +12 -0
- package/dist/parser/document.js +71 -0
- package/dist/parser/document.js.map +1 -0
- package/dist/parser/index.d.ts +10 -0
- package/dist/parser/index.js +21 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/render/compare.d.ts +19 -0
- package/dist/render/compare.js +61 -0
- package/dist/render/compare.js.map +1 -0
- package/dist/render/figcraft-cli.d.ts +21 -0
- package/dist/render/figcraft-cli.js +63 -0
- package/dist/render/figcraft-cli.js.map +1 -0
- package/dist/render/frames.d.ts +19 -0
- package/dist/render/frames.js +53 -0
- package/dist/render/frames.js.map +1 -0
- package/dist/render/png.d.ts +14 -0
- package/dist/render/png.js +185 -0
- package/dist/render/png.js.map +1 -0
- package/dist/report.d.ts +7 -0
- package/dist/report.js +109 -0
- package/dist/report.js.map +1 -0
- package/dist/run.d.ts +23 -0
- package/dist/run.js +144 -0
- package/dist/run.js.map +1 -0
- package/package.json +51 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 figdiff contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,309 @@
|
|
|
1
|
-
#
|
|
1
|
+
# figdiff
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Review design changes the way you review code.**
|
|
4
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of contents
|
|
14
|
+
|
|
15
|
+
- [Try it](#try-it)
|
|
16
|
+
- [The problem](#the-problem)
|
|
17
|
+
- [The idea](#the-idea)
|
|
18
|
+
- [Planned usage](#planned-usage)
|
|
19
|
+
- [How it will work](#how-it-will-work)
|
|
20
|
+
- [Who is it for](#who-is-it-for)
|
|
21
|
+
- [Roadmap](#roadmap)
|
|
22
|
+
- [Risks and open questions](#risks-and-open-questions)
|
|
23
|
+
- [Contributing](#contributing)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
|
|
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
|
+
|
|
42
|
+
## The problem
|
|
43
|
+
|
|
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.
|
|
45
|
+
|
|
46
|
+
Code has diffs, pull request comments, and review tools. Design files, stored in the same repositories, have none of that.
|
|
47
|
+
|
|
48
|
+
## The idea
|
|
49
|
+
|
|
50
|
+
figdiff gives design files the same review workflow as code:
|
|
51
|
+
|
|
52
|
+
- **Semantic diff.** A list of changes in plain language, for example:
|
|
53
|
+
`Frame "Login" › Button "Sign in": fill #2563EB → #1D4ED8`
|
|
54
|
+
- **Visual diff.** Before, after, and highlighted-difference images for every frame that changed.
|
|
55
|
+
- **Automatic pull request comments.** A GitHub Action that posts both of the above on every pull request that touches a design file.
|
|
56
|
+
|
|
57
|
+
## Planned usage
|
|
58
|
+
|
|
59
|
+
> These commands are the intended interface and may change.
|
|
60
|
+
|
|
61
|
+
**Compare two files locally**
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npx figdiff before.figcraft after.figcraft
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
Page "App"
|
|
69
|
+
Frame "Components"
|
|
70
|
+
- Landscape › Time=Day › Sun (ellipse)
|
|
71
|
+
~ Chip › Selected=On › Label: characters All trails → All routes
|
|
72
|
+
Frame "Home — Light"
|
|
73
|
+
~ Status bar › Indicators › Signal › Bar 1: fills #0F172A → #E6172A
|
|
74
|
+
~ Status bar › Indicators › Signal › Bar 1: width 3 → 15
|
|
75
|
+
|
|
76
|
+
4 changes in 2 frames (1 removed, 3 property changes)
|
|
77
|
+
```
|
|
78
|
+
|
|
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.
|
|
80
|
+
|
|
81
|
+
**Generate a shareable report**
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
npx figdiff before.figcraft after.figcraft --html report.html
|
|
85
|
+
```
|
|
86
|
+
|
|
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.
|
|
88
|
+
|
|
89
|
+
**Compare against another Git branch**
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
npx figdiff design/app.figcraft --against main
|
|
93
|
+
```
|
|
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
|
+
|
|
104
|
+
**Run on every pull request**
|
|
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
|
+
|
|
108
|
+
```yaml
|
|
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
|
|
118
|
+
|
|
119
|
+
jobs:
|
|
120
|
+
figdiff:
|
|
121
|
+
runs-on: ubuntu-latest
|
|
122
|
+
steps:
|
|
123
|
+
- uses: actions/checkout@v4
|
|
124
|
+
with:
|
|
125
|
+
fetch-depth: 0 # needed to read the base commit
|
|
126
|
+
- uses: bintangyoga/figdiff@main
|
|
127
|
+
with:
|
|
128
|
+
files: "design/**/*.figcraft"
|
|
129
|
+
```
|
|
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
|
+
|
|
143
|
+
## How it will work
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
before.figcraft ─┐ ┌─ semantic diff (changes as text/JSON)
|
|
147
|
+
├─► parse + match ──►┤
|
|
148
|
+
after.figcraft ─┘ └─ visual diff (render + compare images)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
1. **Parser.** Opens the `.figcraft` archive and reads its `document.json` into an internal model. This is the only part that depends on the file format, so it is isolated and versioned.
|
|
152
|
+
2. **Differ.** Matches nodes between the two versions (by id, with fallbacks) and produces a list of added, removed, moved, and modified nodes.
|
|
153
|
+
3. **Renderer.** Uses FigCraft's own headless CLI (`figcraft-cli`) to render the changed frames to images, then compares them pixel by pixel.
|
|
154
|
+
4. **Reporters.** Terminal output, JSON, a self-contained HTML report, and a GitHub pull request comment.
|
|
155
|
+
|
|
156
|
+
Design principles:
|
|
157
|
+
|
|
158
|
+
- **Independent.** A separate project. It reads the public file format and uses the public CLI; it does not modify FigCraft.
|
|
159
|
+
- **Local-first.** Runs on your machine or in your CI. No accounts, no servers, no telemetry.
|
|
160
|
+
- **Resilient to format changes.** FigCraft is in alpha and its format may change. The parser is kept separate from the diff logic and tested against files from multiple FigCraft versions.
|
|
161
|
+
- **Useful without Git.** The CLI and HTML report work on any two files.
|
|
162
|
+
|
|
163
|
+
## Who is it for
|
|
164
|
+
|
|
165
|
+
- **Developers** who receive design files and want to know what changed since the last version.
|
|
166
|
+
- **Design engineers and designers who use Git**, and want design changes reviewed in pull requests.
|
|
167
|
+
- **Freelancers and agencies** who exchange versions with clients and need a clear "what changed since last time" report.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Roadmap
|
|
172
|
+
|
|
173
|
+
Estimated timeline: about 18 weeks to v1.0, assuming one maintainer at roughly 10 hours per week. Each phase ends with a usable release, and the next phase starts only after the exit criteria are met.
|
|
174
|
+
|
|
175
|
+
| Phase | Weeks | Goal | Release |
|
|
176
|
+
| --- | --- | --- | --- |
|
|
177
|
+
| 0 | 1–2 | Research and setup | n/a |
|
|
178
|
+
| 1 | 3–6 | Semantic diff in the CLI | v0.1.0 |
|
|
179
|
+
| 2 | 7–10 | HTML report and visual diff | v0.2.0 |
|
|
180
|
+
| 3 | 11–14 | GitHub Action | v0.3.0 |
|
|
181
|
+
| 4 | 15–18 | Public launch | v1.0.0 |
|
|
182
|
+
| 5 | After v1.0 | Driven by user demand | n/a |
|
|
183
|
+
|
|
184
|
+
### Phase 0: Research and setup (weeks 1–2)
|
|
185
|
+
|
|
186
|
+
Confirm that FigCraft's file format and CLI are stable enough to build on, before writing product code.
|
|
187
|
+
|
|
188
|
+
- [x] Download sample files from FigCraft's `examples/` folder, unzip them, and study the structure of `document.json`
|
|
189
|
+
- [x] Document the node schema: id, type, name, parent, position, size, fills, strokes, text, auto layout, components, variables
|
|
190
|
+
- [ ] Verify that node ids stay stable when a file is saved again (critical for matching nodes between versions)
|
|
191
|
+
- [ ] Test `figcraft-cli render`, `export --node`, and `convert` on headless Linux
|
|
192
|
+
- [ ] Check whether `figcraft-cli` is available as a prebuilt binary or must be built with Cargo
|
|
193
|
+
- [ ] Check name availability on npm and GitHub; choose a fallback name if needed
|
|
194
|
+
- [x] Set up the repository: TypeScript, tests, CI
|
|
195
|
+
- [ ] Add linting and formatting (ESLint, Prettier)
|
|
196
|
+
|
|
197
|
+
**Deliverables:** format notes (`docs/format-notes.md`), 5–10 before/after sample file pairs, a working project skeleton.
|
|
198
|
+
|
|
199
|
+
**Exit criteria:** the `document.json` schema is understood and node ids are proven stable, or a fallback matching strategy is designed.
|
|
200
|
+
|
|
201
|
+
### Phase 1: Semantic diff in the CLI (weeks 3–6)
|
|
202
|
+
|
|
203
|
+
Deliver `figdiff a.figcraft b.figcraft` printing a correct list of changes in the terminal. No images yet.
|
|
204
|
+
|
|
205
|
+
- [x] Parser for core properties: type, name, hierarchy, position, size, fills, strokes, corner radius, text, font
|
|
206
|
+
- [ ] Node matching by id, with fallback to name and hierarchy path
|
|
207
|
+
- [x] Detect added, removed, reparented, and modified nodes
|
|
208
|
+
- [x] Group results by page and top-level frame so output is easy to read
|
|
209
|
+
- [x] Summary line (for example, "12 changes in 3 frames")
|
|
210
|
+
- [x] `--json` output for use by other tools
|
|
211
|
+
- [x] `--exit-code` flag and stable exit codes (0 ok, 1 differences, 2 error)
|
|
212
|
+
- [x] Detect changes in layer order
|
|
213
|
+
- [ ] Tests using the sample file pairs from Phase 0
|
|
214
|
+
|
|
215
|
+
**Release:** `v0.1.0` on npm, usable with `npx figdiff`.
|
|
216
|
+
|
|
217
|
+
**Exit criteria:** on every sample pair, the list of changes is complete and contains no false changes.
|
|
218
|
+
|
|
219
|
+
### Phase 2: HTML report and visual diff (weeks 7–10)
|
|
220
|
+
|
|
221
|
+
Deliver a self-contained HTML report that anyone can open, including designers who do not use Git.
|
|
222
|
+
|
|
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
|
|
232
|
+
- [ ] Semantic change list next to the images; clicking a change highlights its location
|
|
233
|
+
- [x] `--against <revision>` (compare a file with its version on a branch, tag or commit) and `git difftool` usage
|
|
234
|
+
|
|
235
|
+
**Release:** `v0.2.0`, with the first demo GIF in this README.
|
|
236
|
+
|
|
237
|
+
**Exit criteria:** 3–5 people outside the project open the report and understand the changes without extra explanation.
|
|
238
|
+
|
|
239
|
+
### Phase 3: GitHub Action (weeks 11–14)
|
|
240
|
+
|
|
241
|
+
Deliver an automatic comment on every pull request that changes a `.figcraft` file, set up with one workflow file.
|
|
242
|
+
|
|
243
|
+
- [x] Detect changed `.figcraft` files in a pull request and compare each with its base version
|
|
244
|
+
- [ ] Install `figcraft-cli` on the runner, with caching
|
|
245
|
+
- [x] Pull request comment with a summary and per-frame changes (before/after images still to do)
|
|
246
|
+
- [ ] Host images via workflow artifacts or a dedicated branch (GitHub comments need image URLs)
|
|
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)
|
|
249
|
+
- [ ] Public example repository that uses the Action as a live demo
|
|
250
|
+
|
|
251
|
+
**Release:** `v0.3.0` and a listing on GitHub Marketplace.
|
|
252
|
+
|
|
253
|
+
**Exit criteria:** the Action runs reliably on the example repository and on at least one real project.
|
|
254
|
+
|
|
255
|
+
### Phase 4: Public launch (weeks 15–18)
|
|
256
|
+
|
|
257
|
+
Deliver a project that other people can use and contribute to, then announce it.
|
|
258
|
+
|
|
259
|
+
- [ ] Support for advanced properties: components and instances, variants, variables and tokens, effects, prototyping, images
|
|
260
|
+
- [ ] Performance testing on large files (thousands of nodes); target a diff in a few seconds
|
|
261
|
+
- [ ] Compatibility table of tested FigCraft versions, updated with each FigCraft release
|
|
262
|
+
- [ ] Documentation: CLI guide, Action guide, contributor guide
|
|
263
|
+
- [ ] `good first issue` labels to welcome contributors
|
|
264
|
+
- [ ] Announcement: FigCraft community, Show HN, Reddit, and design communities
|
|
265
|
+
|
|
266
|
+
**Release:** `v1.0.0`, with a stable CLI and JSON output format.
|
|
267
|
+
|
|
268
|
+
### Phase 5: After v1.0
|
|
269
|
+
|
|
270
|
+
Direction will follow user demand. Candidates:
|
|
271
|
+
|
|
272
|
+
- **Three-way merge** of design files from different branches, with conflict markers
|
|
273
|
+
- **Token and consistency checks** that warn in a pull request about colors or spacing outside the design system
|
|
274
|
+
- **Other platforms:** GitLab CI, Gitea, Forgejo
|
|
275
|
+
- **MCP server**, so AI agents can ask "what changed in this design?"
|
|
276
|
+
- **Automatic design changelog** generated from Git tags
|
|
277
|
+
- **Upstream collaboration:** open a diff report directly in FigCraft, if the maintainers are interested
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Risks and open questions
|
|
282
|
+
|
|
283
|
+
| Risk | Impact | Mitigation |
|
|
284
|
+
| --- | --- | --- |
|
|
285
|
+
| The `.figcraft` format changes (FigCraft is alpha) | Parser breaks on new FigCraft releases | Isolated parser, compatibility table, tests on files from each version |
|
|
286
|
+
| Node ids are not stable between saves | Diffs full of false changes | Fallback matching by name and path, validated in Phase 0 |
|
|
287
|
+
| `figcraft-cli` is hard to install in CI | Slow or failing GitHub Action | Cache the binary; fall back to semantic diff only |
|
|
288
|
+
| FigCraft adds its own diff feature | Less reason to use figdiff | Focus on review in Git and pull requests; talk to the maintainers early |
|
|
289
|
+
| Small FigCraft user base | Slow adoption | Start from real needs; keep the core model usable for other formats later |
|
|
290
|
+
|
|
291
|
+
Open questions:
|
|
292
|
+
|
|
293
|
+
- Is the project name `figdiff` available on npm and GitHub?
|
|
294
|
+
- Does `figcraft-cli` ship as a downloadable binary for Linux, macOS, and Windows?
|
|
295
|
+
- How much of FigCraft's built-in `versions/` and `branches/` data is useful to reuse?
|
|
296
|
+
|
|
297
|
+
## Contributing
|
|
298
|
+
|
|
299
|
+
Contributions are welcome, even at this early stage. The most useful things right now:
|
|
300
|
+
|
|
301
|
+
- Feedback on the plan: open an issue with your use case
|
|
302
|
+
- Sample `.figcraft` file pairs (before/after) that you are allowed to share
|
|
303
|
+
- Notes on the file format, once Phase 0 begins
|
|
304
|
+
|
|
305
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
[MIT](LICENSE)
|
|
@@ -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"}
|