figdiff 0.0.0-stage → 0.1.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +260 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +7 -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/index.d.ts +6 -0
- package/dist/index.js +6 -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/report.d.ts +7 -0
- package/dist/report.js +109 -0
- package/dist/report.js.map +1 -0
- package/dist/run.d.ts +8 -0
- package/dist/run.js +44 -0
- package/dist/run.js.map +1 -0
- package/package.json +50 -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,261 @@
|
|
|
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 (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.
|
|
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
|
+
- [The problem](#the-problem)
|
|
16
|
+
- [The idea](#the-idea)
|
|
17
|
+
- [Planned usage](#planned-usage)
|
|
18
|
+
- [How it will work](#how-it-will-work)
|
|
19
|
+
- [Who is it for](#who-is-it-for)
|
|
20
|
+
- [Roadmap](#roadmap)
|
|
21
|
+
- [Risks and open questions](#risks-and-open-questions)
|
|
22
|
+
- [Contributing](#contributing)
|
|
23
|
+
- [License](#license)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## The problem
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
Code has diffs, pull request comments, and review tools. Design files, stored in the same repositories, have none of that.
|
|
32
|
+
|
|
33
|
+
## The idea
|
|
34
|
+
|
|
35
|
+
figdiff gives design files the same review workflow as code:
|
|
36
|
+
|
|
37
|
+
- **Semantic diff.** A list of changes in plain language, for example:
|
|
38
|
+
`Frame "Login" › Button "Sign in": fill #2563EB → #1D4ED8`
|
|
39
|
+
- **Visual diff.** Before, after, and highlighted-difference images for every frame that changed.
|
|
40
|
+
- **Automatic pull request comments.** A GitHub Action that posts both of the above on every pull request that touches a design file.
|
|
41
|
+
|
|
42
|
+
## Planned usage
|
|
43
|
+
|
|
44
|
+
> These commands are the intended interface and may change.
|
|
45
|
+
|
|
46
|
+
**Compare two files locally**
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npx figdiff before.figcraft after.figcraft
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
Page "App"
|
|
54
|
+
Frame "Components"
|
|
55
|
+
- Landscape › Time=Day › Sun (ellipse)
|
|
56
|
+
~ Chip › Selected=On › Label: characters All trails → All routes
|
|
57
|
+
Frame "Home — Light"
|
|
58
|
+
~ Status bar › Indicators › Signal › Bar 1: fills #0F172A → #E6172A
|
|
59
|
+
~ Status bar › Indicators › Signal › Bar 1: width 3 → 15
|
|
60
|
+
|
|
61
|
+
4 changes in 2 frames (1 removed, 3 property changes)
|
|
62
|
+
```
|
|
63
|
+
|
|
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.
|
|
65
|
+
|
|
66
|
+
**Generate a shareable report**
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npx figdiff before.figcraft after.figcraft --html report.html
|
|
70
|
+
```
|
|
71
|
+
|
|
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.
|
|
73
|
+
|
|
74
|
+
**Compare against another Git branch**
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npx figdiff design/app.figcraft --against main
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Run on every pull request**
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
# .github/workflows/design-diff.yml
|
|
84
|
+
name: Design diff
|
|
85
|
+
on: pull_request
|
|
86
|
+
|
|
87
|
+
jobs:
|
|
88
|
+
diff:
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/checkout@v4
|
|
92
|
+
with:
|
|
93
|
+
fetch-depth: 0
|
|
94
|
+
- uses: bintangyoga/figdiff@v1
|
|
95
|
+
with:
|
|
96
|
+
files: "design/**/*.figcraft"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## How it will work
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
before.figcraft ─┐ ┌─ semantic diff (changes as text/JSON)
|
|
103
|
+
├─► parse + match ──►┤
|
|
104
|
+
after.figcraft ─┘ └─ visual diff (render + compare images)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
2. **Differ.** Matches nodes between the two versions (by id, with fallbacks) and produces a list of added, removed, moved, and modified nodes.
|
|
109
|
+
3. **Renderer.** Uses FigCraft's own headless CLI (`figcraft-cli`) to render the changed frames to images, then compares them pixel by pixel.
|
|
110
|
+
4. **Reporters.** Terminal output, JSON, a self-contained HTML report, and a GitHub pull request comment.
|
|
111
|
+
|
|
112
|
+
Design principles:
|
|
113
|
+
|
|
114
|
+
- **Independent.** A separate project. It reads the public file format and uses the public CLI; it does not modify FigCraft.
|
|
115
|
+
- **Local-first.** Runs on your machine or in your CI. No accounts, no servers, no telemetry.
|
|
116
|
+
- **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.
|
|
117
|
+
- **Useful without Git.** The CLI and HTML report work on any two files.
|
|
118
|
+
|
|
119
|
+
## Who is it for
|
|
120
|
+
|
|
121
|
+
- **Developers** who receive design files and want to know what changed since the last version.
|
|
122
|
+
- **Design engineers and designers who use Git**, and want design changes reviewed in pull requests.
|
|
123
|
+
- **Freelancers and agencies** who exchange versions with clients and need a clear "what changed since last time" report.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Roadmap
|
|
128
|
+
|
|
129
|
+
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.
|
|
130
|
+
|
|
131
|
+
| Phase | Weeks | Goal | Release |
|
|
132
|
+
| --- | --- | --- | --- |
|
|
133
|
+
| 0 | 1–2 | Research and setup | n/a |
|
|
134
|
+
| 1 | 3–6 | Semantic diff in the CLI | v0.1.0 |
|
|
135
|
+
| 2 | 7–10 | HTML report and visual diff | v0.2.0 |
|
|
136
|
+
| 3 | 11–14 | GitHub Action | v0.3.0 |
|
|
137
|
+
| 4 | 15–18 | Public launch | v1.0.0 |
|
|
138
|
+
| 5 | After v1.0 | Driven by user demand | n/a |
|
|
139
|
+
|
|
140
|
+
### Phase 0: Research and setup (weeks 1–2)
|
|
141
|
+
|
|
142
|
+
Confirm that FigCraft's file format and CLI are stable enough to build on, before writing product code.
|
|
143
|
+
|
|
144
|
+
- [x] Download sample files from FigCraft's `examples/` folder, unzip them, and study the structure of `document.json`
|
|
145
|
+
- [x] Document the node schema: id, type, name, parent, position, size, fills, strokes, text, auto layout, components, variables
|
|
146
|
+
- [ ] Verify that node ids stay stable when a file is saved again (critical for matching nodes between versions)
|
|
147
|
+
- [ ] Test `figcraft-cli render`, `export --node`, and `convert` on headless Linux
|
|
148
|
+
- [ ] Check whether `figcraft-cli` is available as a prebuilt binary or must be built with Cargo
|
|
149
|
+
- [ ] Check name availability on npm and GitHub; choose a fallback name if needed
|
|
150
|
+
- [x] Set up the repository: TypeScript, tests, CI
|
|
151
|
+
- [ ] Add linting and formatting (ESLint, Prettier)
|
|
152
|
+
|
|
153
|
+
**Deliverables:** format notes (`docs/format-notes.md`), 5–10 before/after sample file pairs, a working project skeleton.
|
|
154
|
+
|
|
155
|
+
**Exit criteria:** the `document.json` schema is understood and node ids are proven stable, or a fallback matching strategy is designed.
|
|
156
|
+
|
|
157
|
+
### Phase 1: Semantic diff in the CLI (weeks 3–6)
|
|
158
|
+
|
|
159
|
+
Deliver `figdiff a.figcraft b.figcraft` printing a correct list of changes in the terminal. No images yet.
|
|
160
|
+
|
|
161
|
+
- [x] Parser for core properties: type, name, hierarchy, position, size, fills, strokes, corner radius, text, font
|
|
162
|
+
- [ ] Node matching by id, with fallback to name and hierarchy path
|
|
163
|
+
- [x] Detect added, removed, reparented, and modified nodes
|
|
164
|
+
- [x] Group results by page and top-level frame so output is easy to read
|
|
165
|
+
- [x] Summary line (for example, "12 changes in 3 frames")
|
|
166
|
+
- [x] `--json` output for use by other tools
|
|
167
|
+
- [x] `--exit-code` flag and stable exit codes (0 ok, 1 differences, 2 error)
|
|
168
|
+
- [x] Detect changes in layer order
|
|
169
|
+
- [ ] Tests using the sample file pairs from Phase 0
|
|
170
|
+
|
|
171
|
+
**Release:** `v0.1.0` on npm, usable with `npx figdiff`.
|
|
172
|
+
|
|
173
|
+
**Exit criteria:** on every sample pair, the list of changes is complete and contains no false changes.
|
|
174
|
+
|
|
175
|
+
### Phase 2: HTML report and visual diff (weeks 7–10)
|
|
176
|
+
|
|
177
|
+
Deliver a self-contained HTML report that anyone can open, including designers who do not use Git.
|
|
178
|
+
|
|
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
|
|
182
|
+
- [ ] 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
|
|
186
|
+
|
|
187
|
+
**Release:** `v0.2.0`, with the first demo GIF in this README.
|
|
188
|
+
|
|
189
|
+
**Exit criteria:** 3–5 people outside the project open the report and understand the changes without extra explanation.
|
|
190
|
+
|
|
191
|
+
### Phase 3: GitHub Action (weeks 11–14)
|
|
192
|
+
|
|
193
|
+
Deliver an automatic comment on every pull request that changes a `.figcraft` file, set up with one workflow file.
|
|
194
|
+
|
|
195
|
+
- [ ] Detect changed `.figcraft` files in a pull request and check out both the base and head versions
|
|
196
|
+
- [ ] Install `figcraft-cli` on the runner, with caching
|
|
197
|
+
- [ ] Pull request comment with a summary, per-frame changes, and before/after images
|
|
198
|
+
- [ ] 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
|
|
201
|
+
- [ ] Public example repository that uses the Action as a live demo
|
|
202
|
+
|
|
203
|
+
**Release:** `v0.3.0` and a listing on GitHub Marketplace.
|
|
204
|
+
|
|
205
|
+
**Exit criteria:** the Action runs reliably on the example repository and on at least one real project.
|
|
206
|
+
|
|
207
|
+
### Phase 4: Public launch (weeks 15–18)
|
|
208
|
+
|
|
209
|
+
Deliver a project that other people can use and contribute to, then announce it.
|
|
210
|
+
|
|
211
|
+
- [ ] Support for advanced properties: components and instances, variants, variables and tokens, effects, prototyping, images
|
|
212
|
+
- [ ] Performance testing on large files (thousands of nodes); target a diff in a few seconds
|
|
213
|
+
- [ ] Compatibility table of tested FigCraft versions, updated with each FigCraft release
|
|
214
|
+
- [ ] Documentation: CLI guide, Action guide, contributor guide
|
|
215
|
+
- [ ] `good first issue` labels to welcome contributors
|
|
216
|
+
- [ ] Announcement: FigCraft community, Show HN, Reddit, and design communities
|
|
217
|
+
|
|
218
|
+
**Release:** `v1.0.0`, with a stable CLI and JSON output format.
|
|
219
|
+
|
|
220
|
+
### Phase 5: After v1.0
|
|
221
|
+
|
|
222
|
+
Direction will follow user demand. Candidates:
|
|
223
|
+
|
|
224
|
+
- **Three-way merge** of design files from different branches, with conflict markers
|
|
225
|
+
- **Token and consistency checks** that warn in a pull request about colors or spacing outside the design system
|
|
226
|
+
- **Other platforms:** GitLab CI, Gitea, Forgejo
|
|
227
|
+
- **MCP server**, so AI agents can ask "what changed in this design?"
|
|
228
|
+
- **Automatic design changelog** generated from Git tags
|
|
229
|
+
- **Upstream collaboration:** open a diff report directly in FigCraft, if the maintainers are interested
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Risks and open questions
|
|
234
|
+
|
|
235
|
+
| Risk | Impact | Mitigation |
|
|
236
|
+
| --- | --- | --- |
|
|
237
|
+
| The `.figcraft` format changes (FigCraft is alpha) | Parser breaks on new FigCraft releases | Isolated parser, compatibility table, tests on files from each version |
|
|
238
|
+
| Node ids are not stable between saves | Diffs full of false changes | Fallback matching by name and path, validated in Phase 0 |
|
|
239
|
+
| `figcraft-cli` is hard to install in CI | Slow or failing GitHub Action | Cache the binary; fall back to semantic diff only |
|
|
240
|
+
| FigCraft adds its own diff feature | Less reason to use figdiff | Focus on review in Git and pull requests; talk to the maintainers early |
|
|
241
|
+
| Small FigCraft user base | Slow adoption | Start from real needs; keep the core model usable for other formats later |
|
|
242
|
+
|
|
243
|
+
Open questions:
|
|
244
|
+
|
|
245
|
+
- Is the project name `figdiff` available on npm and GitHub?
|
|
246
|
+
- Does `figcraft-cli` ship as a downloadable binary for Linux, macOS, and Windows?
|
|
247
|
+
- How much of FigCraft's built-in `versions/` and `branches/` data is useful to reuse?
|
|
248
|
+
|
|
249
|
+
## Contributing
|
|
250
|
+
|
|
251
|
+
Contributions are welcome, even at this early stage. The most useful things right now:
|
|
252
|
+
|
|
253
|
+
- Feedback on the plan: open an issue with your use case
|
|
254
|
+
- Sample `.figcraft` file pairs (before/after) that you are allowed to share
|
|
255
|
+
- Notes on the file format, once Phase 0 begins
|
|
256
|
+
|
|
257
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
[MIT](LICENSE)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACpC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACpC,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Change, DesignNode } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compare two design trees and list what changed.
|
|
4
|
+
*
|
|
5
|
+
* Nodes are matched by id. Fallback matching by name and path is planned
|
|
6
|
+
* for later, once id stability across saves is verified (see docs/format-notes.md).
|
|
7
|
+
*
|
|
8
|
+
* Changes come out in document order of the old tree, followed by added nodes
|
|
9
|
+
* in document order of the new tree.
|
|
10
|
+
*/
|
|
11
|
+
export declare function diffTrees(before: DesignNode, after: DesignNode): Change[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const SEP = " › ";
|
|
2
|
+
function index(root) {
|
|
3
|
+
const map = new Map();
|
|
4
|
+
const walk = (node, trail, parentId) => {
|
|
5
|
+
map.set(node.id, { node, trail, path: [root.name, ...trail].join(SEP), parentId });
|
|
6
|
+
for (const child of node.children)
|
|
7
|
+
walk(child, [...trail, child.name], node.id);
|
|
8
|
+
};
|
|
9
|
+
walk(root, [], null);
|
|
10
|
+
return map;
|
|
11
|
+
}
|
|
12
|
+
function sameValue(a, b) {
|
|
13
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
14
|
+
}
|
|
15
|
+
function diffProps(a, b) {
|
|
16
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
17
|
+
const changes = [];
|
|
18
|
+
for (const prop of [...keys].sort()) {
|
|
19
|
+
if (!sameValue(a[prop], b[prop]))
|
|
20
|
+
changes.push({ prop, from: a[prop], to: b[prop] });
|
|
21
|
+
}
|
|
22
|
+
return changes;
|
|
23
|
+
}
|
|
24
|
+
/** True when children present in both versions appear in a different order. */
|
|
25
|
+
function childOrderChanged(old, now) {
|
|
26
|
+
const inNow = new Set(now.children.map((c) => c.id));
|
|
27
|
+
const inOld = new Set(old.children.map((c) => c.id));
|
|
28
|
+
const orderBefore = old.children.map((c) => c.id).filter((id) => inNow.has(id));
|
|
29
|
+
const orderAfter = now.children.map((c) => c.id).filter((id) => inOld.has(id));
|
|
30
|
+
return orderBefore.join(",") !== orderAfter.join(",");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compare two design trees and list what changed.
|
|
34
|
+
*
|
|
35
|
+
* Nodes are matched by id. Fallback matching by name and path is planned
|
|
36
|
+
* for later, once id stability across saves is verified (see docs/format-notes.md).
|
|
37
|
+
*
|
|
38
|
+
* Changes come out in document order of the old tree, followed by added nodes
|
|
39
|
+
* in document order of the new tree.
|
|
40
|
+
*/
|
|
41
|
+
export function diffTrees(before, after) {
|
|
42
|
+
const a = index(before);
|
|
43
|
+
const b = index(after);
|
|
44
|
+
const changes = [];
|
|
45
|
+
for (const [id, old] of a) {
|
|
46
|
+
const now = b.get(id);
|
|
47
|
+
if (!now) {
|
|
48
|
+
changes.push({ kind: "removed", id, path: old.path, trail: old.trail, type: old.node.type });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const base = { id, path: now.path, trail: now.trail };
|
|
52
|
+
if (old.parentId !== now.parentId) {
|
|
53
|
+
changes.push({ ...base, kind: "moved", fromTrail: old.trail });
|
|
54
|
+
}
|
|
55
|
+
const props = diffProps({ name: old.node.name, type: old.node.type, ...old.node.props }, { name: now.node.name, type: now.node.type, ...now.node.props });
|
|
56
|
+
if (props.length > 0)
|
|
57
|
+
changes.push({ ...base, kind: "modified", props });
|
|
58
|
+
if (childOrderChanged(old.node, now.node))
|
|
59
|
+
changes.push({ ...base, kind: "reordered" });
|
|
60
|
+
}
|
|
61
|
+
for (const [id, now] of b) {
|
|
62
|
+
if (!a.has(id)) {
|
|
63
|
+
changes.push({ kind: "added", id, path: now.path, trail: now.trail, type: now.node.type });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return changes;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/diff/diff.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Count what changed. A "frame" is a direct child of a page that contains at least one change. */
|
|
2
|
+
export function summarize(changes) {
|
|
3
|
+
const s = {
|
|
4
|
+
changes: 0,
|
|
5
|
+
frames: 0,
|
|
6
|
+
added: 0,
|
|
7
|
+
removed: 0,
|
|
8
|
+
moved: 0,
|
|
9
|
+
reordered: 0,
|
|
10
|
+
modifiedNodes: 0,
|
|
11
|
+
propertyChanges: 0,
|
|
12
|
+
};
|
|
13
|
+
const frames = new Set();
|
|
14
|
+
for (const c of changes) {
|
|
15
|
+
if (c.trail.length >= 2)
|
|
16
|
+
frames.add(`${c.trail[0]}\u0000${c.trail[1]}`);
|
|
17
|
+
switch (c.kind) {
|
|
18
|
+
case "added":
|
|
19
|
+
s.added++;
|
|
20
|
+
break;
|
|
21
|
+
case "removed":
|
|
22
|
+
s.removed++;
|
|
23
|
+
break;
|
|
24
|
+
case "moved":
|
|
25
|
+
s.moved++;
|
|
26
|
+
break;
|
|
27
|
+
case "reordered":
|
|
28
|
+
s.reordered++;
|
|
29
|
+
break;
|
|
30
|
+
case "modified":
|
|
31
|
+
s.modifiedNodes++;
|
|
32
|
+
s.propertyChanges += c.props.length;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
s.frames = frames.size;
|
|
37
|
+
s.changes = s.added + s.removed + s.moved + s.reordered + s.propertyChanges;
|
|
38
|
+
return s;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summary.js","sourceRoot":"","sources":["../../src/diff/summary.ts"],"names":[],"mappings":"AAEA,mGAAmG;AACnG,MAAM,UAAU,SAAS,CAAC,OAAiB;IACzC,MAAM,CAAC,GAAY;QACjB,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,CAAC;QACT,KAAK,EAAE,CAAC;QACR,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,CAAC;QACR,SAAS,EAAE,CAAC;QACZ,aAAa,EAAE,CAAC;QAChB,eAAe,EAAE,CAAC;KACnB,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IAEjC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACxE,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;YACf,KAAK,OAAO;gBACV,CAAC,CAAC,KAAK,EAAE,CAAC;gBACV,MAAM;YACR,KAAK,SAAS;gBACZ,CAAC,CAAC,OAAO,EAAE,CAAC;gBACZ,MAAM;YACR,KAAK,OAAO;gBACV,CAAC,CAAC,KAAK,EAAE,CAAC;gBACV,MAAM;YACR,KAAK,WAAW;gBACd,CAAC,CAAC,SAAS,EAAE,CAAC;gBACd,MAAM;YACR,KAAK,UAAU;gBACb,CAAC,CAAC,aAAa,EAAE,CAAC;gBAClB,CAAC,CAAC,eAAe,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC;gBACpC,MAAM;QACV,CAAC;IACH,CAAC;IAED,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC;IACvB,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,eAAe,CAAC;IAC5E,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* figdiff's internal, format-independent model of a design.
|
|
3
|
+
* The FigCraft-specific parsing lives in src/parser/.
|
|
4
|
+
*/
|
|
5
|
+
export interface DesignNode {
|
|
6
|
+
/** Stable identifier of the node within the file. */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Node type, for example "frame", "text", "rectangle". */
|
|
9
|
+
type: string;
|
|
10
|
+
/** Layer name shown in the editor. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Visual and layout properties: fills, strokes, size, position, and so on. */
|
|
13
|
+
props: Record<string, unknown>;
|
|
14
|
+
/** Children in layer order. */
|
|
15
|
+
children: DesignNode[];
|
|
16
|
+
}
|
|
17
|
+
export interface PropChange {
|
|
18
|
+
prop: string;
|
|
19
|
+
from: unknown;
|
|
20
|
+
to: unknown;
|
|
21
|
+
}
|
|
22
|
+
interface ChangeBase {
|
|
23
|
+
/** Id of the node the change is about. */
|
|
24
|
+
id: string;
|
|
25
|
+
/** Names from the page down to the node (the document name is not included). */
|
|
26
|
+
trail: string[];
|
|
27
|
+
/** `trail` joined with " › ", prefixed by the document name. */
|
|
28
|
+
path: string;
|
|
29
|
+
}
|
|
30
|
+
export type Change = (ChangeBase & {
|
|
31
|
+
kind: "added";
|
|
32
|
+
type: string;
|
|
33
|
+
}) | (ChangeBase & {
|
|
34
|
+
kind: "removed";
|
|
35
|
+
type: string;
|
|
36
|
+
}) | (ChangeBase & {
|
|
37
|
+
kind: "moved";
|
|
38
|
+
fromTrail: string[];
|
|
39
|
+
}) | (ChangeBase & {
|
|
40
|
+
kind: "modified";
|
|
41
|
+
props: PropChange[];
|
|
42
|
+
}) | (ChangeBase & {
|
|
43
|
+
kind: "reordered";
|
|
44
|
+
});
|
|
45
|
+
export interface Summary {
|
|
46
|
+
/** Total changes: added + removed + moved + reordered nodes + changed properties. */
|
|
47
|
+
changes: number;
|
|
48
|
+
/** Number of distinct top-level frames that contain at least one change. */
|
|
49
|
+
frames: number;
|
|
50
|
+
added: number;
|
|
51
|
+
removed: number;
|
|
52
|
+
moved: number;
|
|
53
|
+
reordered: number;
|
|
54
|
+
/** Number of nodes with at least one changed property. */
|
|
55
|
+
modifiedNodes: number;
|
|
56
|
+
/** Number of changed properties across all nodes. */
|
|
57
|
+
propertyChanges: number;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/diff/types.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { diffTrees } from "./diff/diff.js";
|
|
2
|
+
export { summarize } from "./diff/summary.js";
|
|
3
|
+
export { parseFigcraft, readDocumentJson, documentToTree } from "./parser/index.js";
|
|
4
|
+
export { formatText, formatSummary, formatValue } from "./report.js";
|
|
5
|
+
export { run } from "./run.js";
|
|
6
|
+
export type { Change, DesignNode, PropChange, Summary } from "./diff/types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { diffTrees } from "./diff/diff.js";
|
|
2
|
+
export { summarize } from "./diff/summary.js";
|
|
3
|
+
export { parseFigcraft, readDocumentJson, documentToTree } from "./parser/index.js";
|
|
4
|
+
export { formatText, formatSummary, formatValue } from "./report.js";
|
|
5
|
+
export { run } from "./run.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DesignNode } from "../diff/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Convert the contents of a `.figcraft` file's `document.json` into figdiff's tree.
|
|
4
|
+
*
|
|
5
|
+
* Format (observed in FigCraft's example files, document version 1):
|
|
6
|
+
* - `nodes` is a flat map from node id (a string key) to the node
|
|
7
|
+
* - each node lists child ids in `children`; the root of the tree is each page in `pages`
|
|
8
|
+
* - numeric ids; `next_id` is the id counter
|
|
9
|
+
*
|
|
10
|
+
* See docs/format-notes.md.
|
|
11
|
+
*/
|
|
12
|
+
export declare function documentToTree(doc: unknown): DesignNode;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Keys that describe structure rather than appearance; they are not diffed as props. */
|
|
2
|
+
const STRUCTURAL_KEYS = new Set(["id", "name", "type", "children", "parent"]);
|
|
3
|
+
/** Number of decimals kept when comparing numbers (absorbs float noise like 8.000000000000002). */
|
|
4
|
+
const PRECISION = 10_000;
|
|
5
|
+
function normalize(value) {
|
|
6
|
+
if (typeof value === "number")
|
|
7
|
+
return Math.round(value * PRECISION) / PRECISION;
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return value.map(normalize);
|
|
10
|
+
if (value !== null && typeof value === "object") {
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const [k, v] of Object.entries(value))
|
|
13
|
+
out[k] = normalize(v);
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Convert the contents of a `.figcraft` file's `document.json` into figdiff's tree.
|
|
20
|
+
*
|
|
21
|
+
* Format (observed in FigCraft's example files, document version 1):
|
|
22
|
+
* - `nodes` is a flat map from node id (a string key) to the node
|
|
23
|
+
* - each node lists child ids in `children`; the root of the tree is each page in `pages`
|
|
24
|
+
* - numeric ids; `next_id` is the id counter
|
|
25
|
+
*
|
|
26
|
+
* See docs/format-notes.md.
|
|
27
|
+
*/
|
|
28
|
+
export function documentToTree(doc) {
|
|
29
|
+
if (doc === null || typeof doc !== "object")
|
|
30
|
+
throw new Error("document.json is not an object");
|
|
31
|
+
const d = doc;
|
|
32
|
+
if (d.format !== "figcraft") {
|
|
33
|
+
throw new Error(`Not a FigCraft document (format: ${JSON.stringify(d.format)})`);
|
|
34
|
+
}
|
|
35
|
+
const nodes = d.nodes;
|
|
36
|
+
const pages = d.pages;
|
|
37
|
+
if (!nodes || typeof nodes !== "object" || !Array.isArray(pages)) {
|
|
38
|
+
throw new Error("document.json has no `nodes` map or `pages` list");
|
|
39
|
+
}
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const build = (id) => {
|
|
42
|
+
const key = String(id);
|
|
43
|
+
const raw = nodes[key];
|
|
44
|
+
if (!raw)
|
|
45
|
+
throw new Error(`Node ${key} is referenced but missing from document.json`);
|
|
46
|
+
if (seen.has(key))
|
|
47
|
+
throw new Error(`Node ${key} appears more than once in the tree`);
|
|
48
|
+
seen.add(key);
|
|
49
|
+
const props = {};
|
|
50
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
51
|
+
if (!STRUCTURAL_KEYS.has(k))
|
|
52
|
+
props[k] = normalize(v);
|
|
53
|
+
}
|
|
54
|
+
const children = Array.isArray(raw.children) ? raw.children.map(build) : [];
|
|
55
|
+
return {
|
|
56
|
+
id: key,
|
|
57
|
+
type: String(raw.type ?? "unknown"),
|
|
58
|
+
name: String(raw.name ?? ""),
|
|
59
|
+
props,
|
|
60
|
+
children,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
id: "document",
|
|
65
|
+
type: "document",
|
|
66
|
+
name: String(d.name ?? "Untitled"),
|
|
67
|
+
props: {},
|
|
68
|
+
children: pages.map(build),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.js","sourceRoot":"","sources":["../../src/parser/document.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DesignNode } from "../diff/types.js";
|
|
2
|
+
export { documentToTree } from "./document.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read a .figcraft archive and return its parsed `document.json`.
|
|
5
|
+
*
|
|
6
|
+
* The archive is a zip containing `document.json`, and optionally
|
|
7
|
+
* `thumbnail.png`, `images/`, `versions/`, `branches/` and `library/`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readDocumentJson(path: string): unknown;
|
|
10
|
+
export declare function parseFigcraft(path: string): DesignNode;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { unzipSync, strFromU8 } from "fflate";
|
|
3
|
+
import { documentToTree } from "./document.js";
|
|
4
|
+
export { documentToTree } from "./document.js";
|
|
5
|
+
/**
|
|
6
|
+
* Read a .figcraft archive and return its parsed `document.json`.
|
|
7
|
+
*
|
|
8
|
+
* The archive is a zip containing `document.json`, and optionally
|
|
9
|
+
* `thumbnail.png`, `images/`, `versions/`, `branches/` and `library/`.
|
|
10
|
+
*/
|
|
11
|
+
export function readDocumentJson(path) {
|
|
12
|
+
const files = unzipSync(new Uint8Array(readFileSync(path)));
|
|
13
|
+
const entry = files["document.json"];
|
|
14
|
+
if (!entry)
|
|
15
|
+
throw new Error(`${path}: no document.json found. Is this a .figcraft file?`);
|
|
16
|
+
return JSON.parse(strFromU8(entry));
|
|
17
|
+
}
|
|
18
|
+
export function parseFigcraft(path) {
|
|
19
|
+
return documentToTree(readDocumentJson(path));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,KAAK,CAAC,eAAe,CAAC,CAAC;IACrC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,qDAAqD,CAAC,CAAC;IAC1F,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,cAAc,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;AAChD,CAAC"}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Change, Summary } from "./diff/types.js";
|
|
2
|
+
/** Render a property value for humans: colors as hex, paints in short form, everything else as JSON. */
|
|
3
|
+
export declare function formatValue(value: unknown): string;
|
|
4
|
+
/** One line, for example "12 changes in 3 frames (2 added, 1 removed, 9 property changes)". */
|
|
5
|
+
export declare function formatSummary(s: Summary): string;
|
|
6
|
+
/** Human-readable report, grouped by page and top-level frame. */
|
|
7
|
+
export declare function formatText(changes: Change[]): string;
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { summarize } from "./diff/summary.js";
|
|
2
|
+
const hex = (n) => Math.round(Math.min(1, Math.max(0, Number(n))) * 255)
|
|
3
|
+
.toString(16)
|
|
4
|
+
.padStart(2, "0")
|
|
5
|
+
.toUpperCase();
|
|
6
|
+
function isColor(v) {
|
|
7
|
+
return typeof v === "object" && v !== null && "r" in v && "g" in v && "b" in v;
|
|
8
|
+
}
|
|
9
|
+
function colorToHex(c) {
|
|
10
|
+
const alpha = c.a === undefined ? 1 : Number(c.a);
|
|
11
|
+
return `#${hex(c.r)}${hex(c.g)}${hex(c.b)}${alpha < 1 ? hex(alpha) : ""}`;
|
|
12
|
+
}
|
|
13
|
+
function describePaint(p) {
|
|
14
|
+
if (typeof p !== "object" || p === null)
|
|
15
|
+
return undefined;
|
|
16
|
+
const paint = p;
|
|
17
|
+
const hidden = paint.visible === false ? " (hidden)" : "";
|
|
18
|
+
if (paint.type === "solid" && isColor(paint.color))
|
|
19
|
+
return colorToHex(paint.color) + hidden;
|
|
20
|
+
if (paint.type === "gradient") {
|
|
21
|
+
const stops = Array.isArray(paint.stops) ? paint.stops.length : 0;
|
|
22
|
+
return `${String(paint.kind ?? "")} gradient, ${stops} stops${hidden}`.trim();
|
|
23
|
+
}
|
|
24
|
+
if (typeof paint.type === "string")
|
|
25
|
+
return `${paint.type}${hidden}`;
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Render a property value for humans: colors as hex, paints in short form, everything else as JSON. */
|
|
29
|
+
export function formatValue(value) {
|
|
30
|
+
if (value === undefined || value === null)
|
|
31
|
+
return "(none)";
|
|
32
|
+
if (typeof value === "string")
|
|
33
|
+
return value;
|
|
34
|
+
if (isColor(value))
|
|
35
|
+
return colorToHex(value);
|
|
36
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
37
|
+
const paints = value.map(describePaint);
|
|
38
|
+
if (paints.every((p) => p !== undefined))
|
|
39
|
+
return paints.join(", ");
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(value) && value.length === 0)
|
|
42
|
+
return "(none)";
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
const SEP = " › ";
|
|
46
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
47
|
+
/** One line, for example "12 changes in 3 frames (2 added, 1 removed, 9 property changes)". */
|
|
48
|
+
export function formatSummary(s) {
|
|
49
|
+
const parts = [];
|
|
50
|
+
if (s.added)
|
|
51
|
+
parts.push(`${s.added} added`);
|
|
52
|
+
if (s.removed)
|
|
53
|
+
parts.push(`${s.removed} removed`);
|
|
54
|
+
if (s.moved)
|
|
55
|
+
parts.push(`${s.moved} moved`);
|
|
56
|
+
if (s.reordered)
|
|
57
|
+
parts.push(`${s.reordered} reordered`);
|
|
58
|
+
if (s.propertyChanges)
|
|
59
|
+
parts.push(plural(s.propertyChanges, "property change"));
|
|
60
|
+
const where = s.frames > 0 ? ` in ${plural(s.frames, "frame")}` : "";
|
|
61
|
+
return `${plural(s.changes, "change")}${where} (${parts.join(", ")})`;
|
|
62
|
+
}
|
|
63
|
+
function describe(c, label) {
|
|
64
|
+
switch (c.kind) {
|
|
65
|
+
case "added":
|
|
66
|
+
return [`+ ${label} (${c.type})`];
|
|
67
|
+
case "removed":
|
|
68
|
+
return [`- ${label} (${c.type})`];
|
|
69
|
+
case "moved":
|
|
70
|
+
return [`> ${label}: moved from ${c.fromTrail.join(SEP) || "(root)"}`];
|
|
71
|
+
case "reordered":
|
|
72
|
+
return [`~ ${label}: layer order changed`];
|
|
73
|
+
case "modified":
|
|
74
|
+
return c.props.map((p) => `~ ${label}: ${p.prop} ${formatValue(p.from)} → ${formatValue(p.to)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Human-readable report, grouped by page and top-level frame. */
|
|
78
|
+
export function formatText(changes) {
|
|
79
|
+
if (changes.length === 0)
|
|
80
|
+
return "No changes.\n";
|
|
81
|
+
// page -> frame ("" for changes on the page itself) -> changes, in first-seen order
|
|
82
|
+
const pages = new Map();
|
|
83
|
+
for (const c of changes) {
|
|
84
|
+
const page = c.trail[0] ?? "(document)";
|
|
85
|
+
const frame = c.trail[1] ?? "";
|
|
86
|
+
const frames = pages.get(page) ?? new Map();
|
|
87
|
+
frames.set(frame, [...(frames.get(frame) ?? []), c]);
|
|
88
|
+
pages.set(page, frames);
|
|
89
|
+
}
|
|
90
|
+
const lines = [];
|
|
91
|
+
for (const [page, frames] of pages) {
|
|
92
|
+
lines.push(`Page "${page}"`);
|
|
93
|
+
for (const [frame, group] of frames) {
|
|
94
|
+
if (frame === "") {
|
|
95
|
+
for (const c of group)
|
|
96
|
+
lines.push(...describe(c, "(page)").map((l) => ` ${l}`));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
lines.push(` Frame "${frame}"`);
|
|
100
|
+
for (const c of group) {
|
|
101
|
+
const label = c.trail.length > 2 ? c.trail.slice(2).join(SEP) : "(frame itself)";
|
|
102
|
+
lines.push(...describe(c, label).map((l) => ` ${l}`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push("", formatSummary(summarize(changes)));
|
|
107
|
+
return lines.join("\n") + "\n";
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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"}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const USAGE = "figdiff (pre-alpha)\n\nUsage:\n figdiff <before.figcraft> <after.figcraft> [options]\n\nOptions:\n --json Print summary and changes as JSON\n --exit-code Exit with 1 when the files differ (0 when identical)\n --help Show this help\n\nExit codes: 0 ok, 1 differences found (only with --exit-code), 2 error.\n";
|
|
2
|
+
export interface RunResult {
|
|
3
|
+
code: number;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
}
|
|
7
|
+
/** Run the CLI. Pure with respect to process state, so it can be tested. */
|
|
8
|
+
export declare function run(argv: string[]): RunResult;
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parseFigcraft } from "./parser/index.js";
|
|
2
|
+
import { diffTrees } from "./diff/diff.js";
|
|
3
|
+
import { summarize } from "./diff/summary.js";
|
|
4
|
+
import { formatText } from "./report.js";
|
|
5
|
+
export const USAGE = `figdiff (pre-alpha)
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
figdiff <before.figcraft> <after.figcraft> [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--json Print summary and changes as JSON
|
|
12
|
+
--exit-code Exit with 1 when the files differ (0 when identical)
|
|
13
|
+
--help Show this help
|
|
14
|
+
|
|
15
|
+
Exit codes: 0 ok, 1 differences found (only with --exit-code), 2 error.
|
|
16
|
+
`;
|
|
17
|
+
const KNOWN_FLAGS = new Set(["--json", "--exit-code", "--help"]);
|
|
18
|
+
/** Run the CLI. Pure with respect to process state, so it can be tested. */
|
|
19
|
+
export function run(argv) {
|
|
20
|
+
const files = argv.filter((a) => !a.startsWith("--"));
|
|
21
|
+
const flags = new Set(argv.filter((a) => a.startsWith("--")));
|
|
22
|
+
if (flags.has("--help"))
|
|
23
|
+
return { code: 0, stdout: USAGE, stderr: "" };
|
|
24
|
+
const unknown = [...flags].filter((f) => !KNOWN_FLAGS.has(f));
|
|
25
|
+
if (unknown.length > 0) {
|
|
26
|
+
return { code: 2, stdout: "", stderr: `figdiff: unknown option ${unknown.join(", ")}\n\n${USAGE}` };
|
|
27
|
+
}
|
|
28
|
+
if (files.length !== 2) {
|
|
29
|
+
return { code: 2, stdout: "", stderr: `figdiff: expected two files\n\n${USAGE}` };
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const [beforePath, afterPath] = files;
|
|
33
|
+
const changes = diffTrees(parseFigcraft(beforePath), parseFigcraft(afterPath));
|
|
34
|
+
const stdout = flags.has("--json")
|
|
35
|
+
? JSON.stringify({ summary: summarize(changes), changes }, null, 2) + "\n"
|
|
36
|
+
: formatText(changes);
|
|
37
|
+
const code = flags.has("--exit-code") && changes.length > 0 ? 1 : 0;
|
|
38
|
+
return { code, stdout, stderr: "" };
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return { code: 2, stdout: "", stderr: `figdiff: ${err instanceof Error ? err.message : String(err)}\n` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=run.js.map
|
package/dist/run.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,CAAC,MAAM,KAAK,GAAG;;;;;;;;;;;CAWpB,CAAC;AAQF,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEjE,4EAA4E;AAC5E,MAAM,UAAU,GAAG,CAAC,IAAc;IAChC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAE9D,IAAI,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAEvE,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,2BAA2B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,KAAK,EAAE,EAAE,CAAC;IACtG,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,kCAAkC,KAAK,EAAE,EAAE,CAAC;IACpF,CAAC;IAED,IAAI,CAAC;QACH,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,GAAG,KAAyB,CAAC;QAC1D,MAAM,OAAO,GAAG,SAAS,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;QAC/E,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC;YAChC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI;YAC1E,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QACxB,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3G,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figdiff",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Review FigCraft design changes like code: semantic and visual diffs for .figcraft files.",
|
|
5
|
+
"author": "bintangyoga",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"figdiff": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
23
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"provenance": true
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/bintangyoga/figdiff.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/bintangyoga/figdiff/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/bintangyoga/figdiff#readme",
|
|
37
|
+
"keywords": [
|
|
38
|
+
"figcraft",
|
|
39
|
+
"design",
|
|
40
|
+
"diff",
|
|
41
|
+
"github-action",
|
|
42
|
+
"design-systems"
|
|
43
|
+
],
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"fflate": "^0.8.2"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"typescript": "^5.6.0",
|
|
50
|
+
"vitest": "^2.1.0"
|
|
51
|
+
}
|
|
52
|
+
}
|