-
Notifications
You must be signed in to change notification settings - Fork 200
chore: add compare-examples-size skill #1074
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| --- | ||
| name: compare-examples-size | ||
| description: Compare maxGraph example bundle sizes between two git references (commit SHA, branch, or tag) and emit a markdown table with deltas in kB and %. Use when the user asks to compare bundle sizes, measure the size impact of a change or release, diff example sizes between branches/tags/commits, or invokes the /compare-examples-size slash command with two refs. | ||
| --- | ||
|
|
||
| # Compare Examples Size | ||
|
|
||
| ## Overview | ||
|
|
||
| Builds the maxGraph examples at two git references and produces a markdown table comparing per-example bundle sizes. Useful to assess the size impact of a PR, refactor, or release. | ||
|
|
||
| ## When to use | ||
|
|
||
| Trigger this skill on requests like: | ||
| - "compare bundle sizes between `main` and my branch" | ||
| - "what's the size impact of this branch?" | ||
| - "compare example sizes between `v1.2.0` and `main`" | ||
| - `/compare-examples-size <from> <to>` | ||
|
|
||
| ## Inputs | ||
|
|
||
| Two git references — `<from>` and `<to>`. Each can be: | ||
| - a commit SHA (full or short) | ||
| - a branch name (HEAD of the branch is used) | ||
| - a tag name | ||
|
|
||
| If the user only provides one ref, ask for the second. Do not assume `HEAD` silently. If the user provides none, ask for both. | ||
|
|
||
| ## Workflow | ||
|
|
||
| Run the bundled script with the two refs: | ||
|
|
||
| ```bash | ||
| .claude/skills/compare-examples-size/scripts/compare-examples-size.bash <from> <to> | ||
| ``` | ||
|
|
||
| The script: | ||
| 1. Verifies the working tree is strictly clean (`git status --porcelain` empty). Aborts with an error if not. | ||
| 2. Resolves both refs via `git rev-parse --verify`. Aborts on unknown refs. | ||
| 3. Normalizes the column order: the **older** commit (by committer timestamp) becomes column 1, the **newer** becomes column 2 — regardless of the order the user passed them on the CLI. A note is printed to stderr if a swap happened. Δ is therefore always `newer − older` (positive Δ = size grew over time). | ||
| 4. Saves the current branch (or SHA if detached) and installs a `trap` so the original ref is restored on any exit — including build failures or Ctrl-C. | ||
| 5. For each ref, in order older then newer: | ||
| - `git checkout <ref>` | ||
| - `npm ci` if `package-lock.json` differs from the previous ref (always runs on first ref to guarantee fresh deps). `npm ci` is used instead of `npm install` so the lock file is never rewritten — keeping the working tree clean across the checkout. | ||
| - `npm run build -w packages/core` | ||
| - `./scripts/build-all-examples.bash`, capturing its trailing CSV (header line + values line) into a temp file. | ||
| 6. Parses both CSVs, joins by example name, and prints a markdown table to stdout. | ||
| 7. Restores the original ref and removes the temp dir. | ||
|
|
||
| Build logs go to stderr; only the final markdown table goes to stdout, so it can be redirected to a file or piped directly. | ||
|
|
||
| ## Output format | ||
|
|
||
| GitHub-flavored markdown table on stdout: | ||
|
|
||
| ``` | ||
| | Example | <older-label> <older-short-sha> (kB) | <newer-label> <newer-short-sha> (kB) | Δ kB | Δ % | | ||
| ``` | ||
|
|
||
| - Column 1 is always the **older** commit, column 2 the **newer** — even if the user passed them in the reverse order on the CLI. | ||
| - `<label>` is the branch or tag name the user supplied. If the user supplied a raw SHA, the label is omitted and only the short SHA appears. | ||
| - `Δ kB` is signed (`+` or `-`) and equals `newer − older`. | ||
| - `Δ %` is signed and uses the older size as the denominator. | ||
| - Rows are sorted alphabetically by example name. | ||
| - `N/A` is used when an example exists at one ref but not the other. | ||
|
|
||
| No commentary on causes of variation — just the numbers. | ||
|
|
||
| ## Prerequisites and constraints | ||
|
|
||
| - The script must run from inside the maxGraph repository (it uses `git rev-parse --show-toplevel`). | ||
| - The active Node.js version should match `.nvmrc`. If `nvm` is available, the user should run `nvm use` before invoking the skill. | ||
| - The working tree must be clean. Do **not** auto-stash. If the user has uncommitted work, surface the error and ask them how to proceed. | ||
| - The script does two full `npm run build -w packages/core` + full examples build, so it takes several minutes. Warn the user before launching if this is the first run in a session. | ||
|
|
||
| ## Failure handling | ||
|
|
||
| - **Dirty working tree**: report the offending files (`git status --short`) and stop. | ||
| - **Unknown ref**: report which ref failed to resolve and stop. | ||
| - **Build failure on a ref**: the `trap` restores the original ref before exiting. Report which ref failed and at which step (`npm ci`, core build, or examples build). Do not retry automatically. | ||
| - **Original ref restoration fails on exit**: surface the warning printed by the script and tell the user to `git checkout <original-ref>` manually. | ||
| - **Forced kill (SIGKILL) mid-run**: bash traps cannot run on SIGKILL, so the user can be left in detached HEAD. To detect this, the script writes a lock file at `.git/compare-examples-size.lock` containing the original ref before any checkout; the `trap` removes it on graceful exit. If the script aborts via SIGKILL, the lock survives. The next invocation will refuse to start and print the original ref to restore. After running `git checkout <that-ref>`, the user must remove the lock file as instructed. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,225 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||||||||||||||||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Compare maxGraph example bundle sizes between two git references. | ||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Usage: compare-examples-size.bash <from-ref> <to-ref> | ||||||||||||||||||||||||||||||||||||||||||||||||
| # <from-ref> / <to-ref>: commit SHA, branch name, or tag. | ||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Output: a markdown table on stdout with per-example sizes for both refs and the delta. | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Build logs are printed to stderr so stdout stays clean. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| usage() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Usage: $0 <from-ref> <to-ref>" >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo " Each ref can be a commit SHA, branch name, or tag." >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ $# -ne 2 ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| usage | ||||||||||||||||||||||||||||||||||||||||||||||||
| exit 2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| FROM_RAW="$1" | ||||||||||||||||||||||||||||||||||||||||||||||||
| TO_RAW="$2" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ -z "$REPO_ROOT" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Error: not inside a git repository." >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| cd "$REPO_ROOT" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # 1. Recovery lock check — must run FIRST. If a previous invocation was killed forcibly | ||||||||||||||||||||||||||||||||||||||||||||||||
| # (SIGKILL bypasses the trap), the lock survives and points to the ref the user was on. | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Surface that before any other check so the recovery instructions are the first thing | ||||||||||||||||||||||||||||||||||||||||||||||||
| # the user sees. | ||||||||||||||||||||||||||||||||||||||||||||||||
| LOCK_FILE="$(git rev-parse --git-common-dir)/compare-examples-size.lock" | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ -f "$LOCK_FILE" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| prev_ref=$(cat "$LOCK_FILE" 2>/dev/null || echo "<unknown>") | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Error: a previous run did not clean up (lock file present)." >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "It was started on ref: $prev_ref" >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "If you are not on that ref now, run: git checkout $prev_ref" >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Then remove the lock: rm $LOCK_FILE" >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # 2. Strict clean working tree (including untracked files in tracked dirs) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ -n "$(git status --porcelain)" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Error: working tree is not clean. Commit, stash, or discard changes before running." >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| git status --short >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # 3. Resolve refs (verify they exist; capture commit SHA + label). | ||||||||||||||||||||||||||||||||||||||||||||||||
| # The `^{commit}` peel forces dereferencing through annotated tags so we end up with the | ||||||||||||||||||||||||||||||||||||||||||||||||
| # underlying commit SHA, not the tag object SHA. Without it, `git rev-parse --verify v0.23.0` | ||||||||||||||||||||||||||||||||||||||||||||||||
| # returns the tag object's SHA, which is unfindable via `git log` and confuses readers of the | ||||||||||||||||||||||||||||||||||||||||||||||||
| # output table. | ||||||||||||||||||||||||||||||||||||||||||||||||
| resolve_ref() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| local raw="$1" | ||||||||||||||||||||||||||||||||||||||||||||||||
| local sha | ||||||||||||||||||||||||||||||||||||||||||||||||
| if ! sha=$(git rev-parse --verify "$raw^{commit}" 2>/dev/null); then | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Error: cannot resolve git ref '$raw' to a commit." >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "$sha" | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| FROM_SHA=$(resolve_ref "$FROM_RAW") | ||||||||||||||||||||||||||||||||||||||||||||||||
| TO_SHA=$(resolve_ref "$TO_RAW") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Normalize order: older commit first (column 1), newer commit second (column 2), | ||||||||||||||||||||||||||||||||||||||||||||||||
| # regardless of CLI argument order. "Older" is determined by committer timestamp, | ||||||||||||||||||||||||||||||||||||||||||||||||
| # which works even when the two refs are not in an ancestor relationship. | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Δ semantics become "newer minus older" → positive Δ means size grew over time. | ||||||||||||||||||||||||||||||||||||||||||||||||
| FROM_TS=$(git show -s --format=%ct "$FROM_SHA") | ||||||||||||||||||||||||||||||||||||||||||||||||
| TO_TS=$(git show -s --format=%ct "$TO_SHA") | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (( FROM_TS > TO_TS )); then | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Note: swapping args so the older commit is in column 1 (was: '$FROM_RAW' newer than '$TO_RAW')." >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| tmp_raw="$FROM_RAW"; FROM_RAW="$TO_RAW"; TO_RAW="$tmp_raw" | ||||||||||||||||||||||||||||||||||||||||||||||||
| tmp_sha="$FROM_SHA"; FROM_SHA="$TO_SHA"; TO_SHA="$tmp_sha" | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| FROM_SHORT="${FROM_SHA:0:7}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| TO_SHORT="${TO_SHA:0:7}" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Decide the label shown in the markdown header: empty unless user provided a branch/tag name. | ||||||||||||||||||||||||||||||||||||||||||||||||
| # A raw 40-char (or shorter) SHA passed in is treated as a SHA-only column. | ||||||||||||||||||||||||||||||||||||||||||||||||
| ref_label() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| local raw="$1" | ||||||||||||||||||||||||||||||||||||||||||||||||
| local sha="$2" | ||||||||||||||||||||||||||||||||||||||||||||||||
| # If raw looks like a SHA (hex, 7-40 chars) AND matches the start of sha, treat as SHA-only. | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ "$raw" =~ ^[0-9a-fA-F]{7,40}$ ]] && [[ "$sha" == "$raw"* ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "" | ||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "$raw" | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| FROM_LABEL=$(ref_label "$FROM_RAW" "$FROM_SHA") | ||||||||||||||||||||||||||||||||||||||||||||||||
| TO_LABEL=$(ref_label "$TO_RAW" "$TO_SHA") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # 4. Save current ref so we can restore it on exit (branch name preferred, fallback to SHA), | ||||||||||||||||||||||||||||||||||||||||||||||||
| # then write the lock so a forced kill leaves a recovery breadcrumb (see step 1). | ||||||||||||||||||||||||||||||||||||||||||||||||
| ORIGINAL_REF=$(git symbolic-ref --quiet --short HEAD 2>/dev/null || git rev-parse HEAD) | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "$ORIGINAL_REF" > "$LOCK_FILE" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Temp files for CSV captures | ||||||||||||||||||||||||||||||||||||||||||||||||
| TMP_DIR=$(mktemp -d -t maxgraph-sizes-XXXXXX) | ||||||||||||||||||||||||||||||||||||||||||||||||
| FROM_CSV="$TMP_DIR/from.csv" | ||||||||||||||||||||||||||||||||||||||||||||||||
| TO_CSV="$TMP_DIR/to.csv" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| cleanup() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| local status=$? | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Restoring original ref: $ORIGINAL_REF" >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| git checkout --quiet "$ORIGINAL_REF" 2>/dev/null || \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Warning: failed to restore $ORIGINAL_REF. Run 'git checkout $ORIGINAL_REF' manually." >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| rm -rf "$TMP_DIR" | ||||||||||||||||||||||||||||||||||||||||||||||||
| rm -f "$LOCK_FILE" | ||||||||||||||||||||||||||||||||||||||||||||||||
| exit $status | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| trap cleanup EXIT INT TERM | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+111
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With ♻️ Proposed fix cleanup() {
local status=$?
+ trap - EXIT INT TERM
echo >&2
echo "Restoring original ref: $ORIGINAL_REF" >&2📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| PREV_LOCK_HASH="" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # 4. Build at each ref and capture CSV | ||||||||||||||||||||||||||||||||||||||||||||||||
| build_and_capture() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| local sha="$1" | ||||||||||||||||||||||||||||||||||||||||||||||||
| local out_csv="$2" | ||||||||||||||||||||||||||||||||||||||||||||||||
| local raw="$3" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| echo >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "=== Checking out $raw ($sha) ===" >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| git checkout --quiet "$sha" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| local current_lock_hash="" | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ -f package-lock.json ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| current_lock_hash=$(git hash-object package-lock.json) | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ -z "$PREV_LOCK_HASH" || "$PREV_LOCK_HASH" != "$current_lock_hash" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Use `npm ci` rather than `npm install` so the working tree stays clean: | ||||||||||||||||||||||||||||||||||||||||||||||||
| # `npm install` can rewrite package-lock.json and contaminate the tree across the checkout. | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "--- npm ci (package-lock.json changed or first run) ---" >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| npm ci >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "--- npm ci skipped (package-lock.json unchanged) ---" >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| PREV_LOCK_HASH="$current_lock_hash" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| echo "--- Building core ---" >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| npm run build -w packages/core >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| echo "--- Building examples and capturing sizes ---" >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| # build-all-examples.bash emits a CSV at the end: a header line and a values line. | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Stream to stderr for the user while teeing to a tmp file we parse afterwards. | ||||||||||||||||||||||||||||||||||||||||||||||||
| local raw_out | ||||||||||||||||||||||||||||||||||||||||||||||||
| raw_out=$(mktemp -t maxgraph-raw-XXXXXX) | ||||||||||||||||||||||||||||||||||||||||||||||||
| ./scripts/build-all-examples.bash 2>&1 | tee /dev/stderr > "$raw_out" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Extract the last two non-empty lines, which are the CSV header and CSV values. | ||||||||||||||||||||||||||||||||||||||||||||||||
| local last_two | ||||||||||||||||||||||||||||||||||||||||||||||||
| last_two=$(grep -v '^$' "$raw_out" | tail -n 2) | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "$last_two" > "$out_csv" | ||||||||||||||||||||||||||||||||||||||||||||||||
| rm -f "$raw_out" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ $(wc -l < "$out_csv") -lt 2 ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Error: failed to capture CSV output from build-all-examples.bash." >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| build_and_capture "$FROM_SHA" "$FROM_CSV" "$FROM_RAW" | ||||||||||||||||||||||||||||||||||||||||||||||||
| build_and_capture "$TO_SHA" "$TO_CSV" "$TO_RAW" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # 5. Parse CSVs and emit markdown table. | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Each CSV file has exactly 2 lines: header (example names) and values (kB sizes). | ||||||||||||||||||||||||||||||||||||||||||||||||
| python3 - "$FROM_CSV" "$TO_CSV" "$FROM_LABEL" "$FROM_SHORT" "$TO_LABEL" "$TO_SHORT" <<'PY' | ||||||||||||||||||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| from_csv, to_csv, from_label, from_short, to_label, to_short = sys.argv[1:] | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def load(path): | ||||||||||||||||||||||||||||||||||||||||||||||||
| with open(path) as f: | ||||||||||||||||||||||||||||||||||||||||||||||||
| lines = [ln.rstrip("\n") for ln in f if ln.strip()] | ||||||||||||||||||||||||||||||||||||||||||||||||
| header = lines[0].split(",") | ||||||||||||||||||||||||||||||||||||||||||||||||
| values = lines[1].split(",") | ||||||||||||||||||||||||||||||||||||||||||||||||
| return dict(zip(header, values)) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+182
to
+187
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent column mismatch in CSV parsing.
🛡️ Proposed fix def load(path):
with open(path) as f:
lines = [ln.rstrip("\n") for ln in f if ln.strip()]
header = lines[0].split(",")
values = lines[1].split(",")
+ if len(header) != len(values):
+ sys.exit(
+ f"Error: CSV header/values column count mismatch in {path} "
+ f"({len(header)} vs {len(values)})."
+ )
return dict(zip(header, values))📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| from_sizes = load(from_csv) | ||||||||||||||||||||||||||||||||||||||||||||||||
| to_sizes = load(to_csv) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def to_float(v): | ||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| return float(v) | ||||||||||||||||||||||||||||||||||||||||||||||||
| except (TypeError, ValueError): | ||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def col_header(label, short): | ||||||||||||||||||||||||||||||||||||||||||||||||
| return f"{label} {short}".strip() if label else short | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| from_col = col_header(from_label, from_short) | ||||||||||||||||||||||||||||||||||||||||||||||||
| to_col = col_header(to_label, to_short) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| examples = sorted(set(from_sizes) | set(to_sizes)) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| print() | ||||||||||||||||||||||||||||||||||||||||||||||||
| print(f"| Example | {from_col} (kB) | {to_col} (kB) | Δ kB | Δ % |") | ||||||||||||||||||||||||||||||||||||||||||||||||
| print("| --- | ---: | ---: | ---: | ---: |") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| for name in examples: | ||||||||||||||||||||||||||||||||||||||||||||||||
| fv = to_float(from_sizes.get(name)) | ||||||||||||||||||||||||||||||||||||||||||||||||
| tv = to_float(to_sizes.get(name)) | ||||||||||||||||||||||||||||||||||||||||||||||||
| fv_s = f"{fv:.2f}" if fv is not None else "N/A" | ||||||||||||||||||||||||||||||||||||||||||||||||
| tv_s = f"{tv:.2f}" if tv is not None else "N/A" | ||||||||||||||||||||||||||||||||||||||||||||||||
| if fv is not None and tv is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| delta = tv - fv | ||||||||||||||||||||||||||||||||||||||||||||||||
| pct = (delta / fv * 100) if fv != 0 else 0.0 | ||||||||||||||||||||||||||||||||||||||||||||||||
| delta_s = f"{delta:+.2f}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| pct_s = f"{pct:+.2f}%" | ||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||
| delta_s = "N/A" | ||||||||||||||||||||||||||||||||||||||||||||||||
| pct_s = "N/A" | ||||||||||||||||||||||||||||||||||||||||||||||||
| print(f"| {name} | {fv_s} | {tv_s} | {delta_s} | {pct_s} |") | ||||||||||||||||||||||||||||||||||||||||||||||||
| print() | ||||||||||||||||||||||||||||||||||||||||||||||||
| PY | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| --- | ||
| description: Internal tools to help during maxGraph development. | ||
| --- | ||
|
|
||
| # Development tools | ||
|
|
||
| This page documents internal scripts and helpers used during maxGraph development. They are not part of the public API of the library — they are intended for maintainers and contributors who need to measure, inspect, or compare the impact of their changes on the project. | ||
|
|
||
| ## Build all examples | ||
|
|
||
| The script `scripts/build-all-examples.bash` builds every example shipped in the repository (`packages/ts-example*` and `packages/js-example*`) and reports the resulting bundle sizes. | ||
|
|
||
| It is useful to: | ||
|
|
||
| - give the size of the `maxGraph` chunk for all examples; | ||
| - visualize the tree-shaking effect of the examples, and observe how much each example reduces the size of the `maxGraph` chunk. | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| The `@maxgraph/core` package must be built first so that the examples pick up the latest changes: | ||
|
|
||
| ```bash | ||
| npm run build -w packages/core | ||
| ``` | ||
|
|
||
| ### Usage | ||
|
|
||
| Run from the repository root: | ||
|
|
||
| ```bash | ||
| ./scripts/build-all-examples.bash | ||
| ``` | ||
|
|
||
| The script prints, for each example: | ||
|
|
||
| - the size of every `*.js` file produced under `dist/`; | ||
| - a Markdown summary table with the bundle sizes of every example (with an empty "before" column intended to be filled in manually when comparing snapshots); | ||
| - a CSV summary (one header line listing example names, one value line listing sizes in kB) — convenient to copy into a spreadsheet. | ||
|
|
||
| To skip the build step and only display the sizes from an existing `dist/` output: | ||
|
|
||
| ```bash | ||
| ./scripts/build-all-examples.bash --list-size-only | ||
| ``` | ||
|
|
||
| ## Comparing the size of the maxGraph chunk between git revisions | ||
|
|
||
| Comparing the size of the `maxGraph` chunk between git revisions helps track the evolution of the codebase, the reduction of the size of `maxGraph`, and the impact of tree-shaking improvements over time. | ||
|
|
||
| It is also useful to: | ||
|
|
||
| - enrich pull request descriptions with a concrete measurement of the impact of a code change on the bundle size; | ||
| - support release notes, where the resulting table can demonstrate positive changes or warn readers about negative impacts on the bundle size. | ||
|
|
||
| Two interchangeable entry points are provided: | ||
|
|
||
| - a **Claude Code skill** named `compare-examples-size`, intended for users of Claude Code. It produces a Markdown table comparing the size of the `maxGraph` chunk for all examples between two git revisions (commit SHAs, branch names, or tag names). | ||
| - the underlying **bash script**, available for direct use at `.claude/skills/compare-examples-size/scripts/compare-examples-size.bash`. The skill is a thin wrapper around this script, so contributors who do not use Claude Code can still run the comparison from any shell. | ||
|
|
||
| ### Usage | ||
|
|
||
| Run from the repository root, passing two git references in any order: | ||
|
|
||
| ```bash | ||
| .claude/skills/compare-examples-size/scripts/compare-examples-size.bash <ref-1> <ref-2> | ||
| ``` | ||
|
|
||
| For example, to compare the current `main` branch against the `v0.23.0` release tag: | ||
|
|
||
| ```bash | ||
| .claude/skills/compare-examples-size/scripts/compare-examples-size.bash main v0.23.0 | ||
| ``` | ||
|
|
||
| For each of the two references, the script checks out the revision, runs `npm ci`, builds `@maxgraph/core`, and runs `scripts/build-all-examples.bash` to capture the bundle sizes. It then prints a Markdown table to stdout — build logs go to stderr — with one row per example and the following columns: example name, bundle size at the older revision, bundle size at the newer revision, delta in kB, and delta in %. | ||
|
|
||
| The column order is normalized by the commit date so that the older revision is always in column 1 and the newer one in column 2, regardless of the argument order. The delta is therefore always `newer − older`: a positive delta means the bundle grew between the two revisions. | ||
|
|
||
| The working tree must be clean before running. The original branch is restored automatically at the end of the run, including on Ctrl-C or build failure. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Install the cleanup trap before creating side effects.
The lock file (line 104) and
TMP_DIR(line 107) are created beforetrap cleanup EXIT INT TERMis installed on line 121. IfSIGINT/SIGTERMarrives in that window (e.g., duringmktemp), the shell takes the default action and exits without runningcleanup, leaving an orphaned lock file and a temp dir behind — and on next run the user is told a previous run was killed even though nothing was checked out. Move the trap installation to immediately afterORIGINAL_REFis captured, then write the lock and create the temp dir.🛡️ Proposed reordering
📝 Committable suggestion