docs(bundle-size): correct the action pin, package inventory and guard coverage

The Tier 3 section shipped in this PR made claims the code contradicts:

- The action pin read v2.9.1; static_bundle_size.yml pins 2.10.0 (by SHA).
- assert-headless-purity.mjs asserts four targets — dist/v2/headless.{mjs,cjs}
  AND dist/v2/context.{mjs,cjs} — not just the headless pair.
- The package inventory enumerated 9 packages while the workflow glob covers 10:
  react-native was neither listed nor classified, and the "no size script" claim
  was wrong for it (it ships size:headless).
- "Both directions of the #4893 regression" overclaimed. The purity script is a
  substring scan of four emitted files with @copilotkit/core, @copilotkit/shared,
  @ag-ui/*, rxjs, zod and uuid external, so it cannot see a heavy dep arriving
  through an external edge; the RN test never resolves bare specifiers, so it
  cannot enter node_modules. Both limits are now stated, along with the gap
  neither guard covers.
- The heading said "two tiers" while three are documented.

Docs only — no change to the script or the workflow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim
2026-08-10 21:35:30 +02:00
parent ec42161670
commit 2158a6f382
+54 -15
View File
@@ -1,10 +1,10 @@
# Bundle Size Tracking
## How it works — two tiers
## How it works — three tiers
### Tier 1: CI (compressed-size-action)
`static_bundle_size.yml` runs on every PR via `preactjs/compressed-size-action@v2.9.1`. It scans a glob (`packages/{...}/dist/**/*.{mjs,js,cjs}`), computes the gzip size of each matched file (the action's default compression; the workflow sets no `compression` input), and posts a PR comment showing per-file diffs. It has **no hard-fail** (Phase 1).
`static_bundle_size.yml` runs on every PR via `preactjs/compressed-size-action` (pinned by commit SHA, currently `2.10.0`). It scans a glob (`packages/{...}/dist/**/*.{mjs,js,cjs}`), computes the gzip size of each matched file (the action's default compression; the workflow sets no `compression` input), and posts a PR comment showing per-file diffs. It has **no hard-fail** (Phase 1).
> **Fork PRs:** `pull_request` runs triggered from a fork receive a read-only `GITHUB_TOKEN`, so `compressed-size-action` cannot post or update the PR comment — it prints the size report to the job logs instead. The measurement still runs; only the comment is unavailable. This is an accepted Phase 1 limitation (the report is informational and there is no hard-fail). If the PR comment ever becomes a required signal, switch to a `pull_request_target` + `workflow_run` relay pattern so the comment is posted from a trusted context without exposing write tokens to fork code.
@@ -14,10 +14,15 @@ Key facts:
- The action runs `build-script: build` (the root `build` script — `nx run-many -t build` over all `packages/**`) on both the PR branch and the base branch, then measures only the files matched by the `pattern` glob. The root `build` script is used (rather than a bundle-size-specific one) because the action must build the base branch too, and `build` exists on every branch. No separate build step is needed before the workflow triggers — the action handles both builds.
- PR comments show paths like `packages/react-core/dist/index.mjs (+1.2 kB gzip)`.
`react-native` joined the glob in the render-tool convergence (2026-08-06); its
`dist/` was previously unmeasured. `pnpm --filter @copilotkit/react-native
size:headless` gives a per-PR gzip signal for the headless entry, printed to the
job summary. No `limit` fields — see Phase 2.
`react-native` joined the glob in the render-tool convergence (2026-08-06),
bringing the glob to **10** packages; its `dist/` was previously unmeasured.
Separately, `pnpm --filter @copilotkit/react-native size:headless`
(`packages/react-native/scripts/measure-headless.mjs`, run as the last step of the
`copilotchat-import-size` job) esbuild-bundles the lean import surface of
`@copilotkit/react-native/headless` — deps and all, with `react`/`react-native`
external — and writes the gzipped total to the job summary. Like `size:headline`
it is a cross-PR relative signal, not a Metro figure, and it does not hard-fail.
No `limit` fields — see Phase 2.
### The CopilotChat regression signal (job summary, not the PR comment)
@@ -51,7 +56,10 @@ The four **bundled** packages (`core`, `react-core`, `react-ui`, `react-textarea
pnpm --filter <pkg> size
```
The five unbundled packages (`shared`, `runtime-client-gql`, `web-inspector`, `voice`, `a2ui-renderer`) have no `.size-limit.json` and no `size` script — their sizes are tracked by the CI glob only.
The other **six** packages in the CI glob have no `.size-limit.json` and no `size` script (4 + 6 is the 10 packages the workflow's `pattern` covers):
- `shared`, `runtime-client-gql`, `web-inspector`, `voice`, `a2ui-renderer` — unbundled (they emit re-export barrels with separate chunk files); tracked by the CI glob only.
- `react-native` — multi-entry with every runtime dep external, so the glob measures each entry plus its shared chunks. It has no size-limit config either, but it does ship a bespoke `size:headless` script (`scripts/measure-headless.mjs`, an esbuild signal rather than size-limit — see Tier 1 above), run in CI and locally via `pnpm --filter @copilotkit/react-native size:headless`.
> **Node version requirement:** `size-limit@12.1.0` requires Node 20, 22, or 24+ (`^20 || ^22 || >=24`). Running `pnpm --filter <pkg> size` on Node 18 will produce an `EBADENGINE` error.
@@ -60,15 +68,46 @@ The five unbundled packages (`shared`, `runtime-client-gql`, `web-inspector`, `v
Two checks hard-fail because they assert _structure_, not a byte threshold — no
baseline to maintain, and no conflict with the Phase 2 freeze on `limit` fields:
1. `pnpm --filter @copilotkit/react-core size:assert-headless` — fails if the
built `dist/v2/headless.mjs` / `.cjs` reference `shiki`, `mermaid`,
`cytoscape`, `katex` or `streamdown`. Runs in `static_bundle_size.yml`.
1. `pnpm --filter @copilotkit/react-core size:assert-headless`
(`packages/react-core/scripts/assert-headless-purity.mjs`) — reads the **four**
built React-Native-reachable entry files (`dist/v2/headless.mjs` / `.cjs` and
`dist/v2/context.mjs` / `.cjs`) and fails if the text of any of them contains
`shiki`, `mermaid`, `cytoscape`, `katex` or `streamdown`. Both entries are
guarded because `@copilotkit/react-native` imports both. Runs in
`static_bundle_size.yml` — the step there is named after `/v2/headless` only,
but the script covers `/v2/context` as well.
2. `packages/react-native/src/__tests__/headless-entry-surface.test.ts` — walks
the RN import graph and fails if any module imports a react-core entry other
than `/v2/headless` or `/v2/context`. Runs in the normal test job.
the relative-import graph of this package's own `src/`, from both
`src/headless.ts` and `src/index.ts`, and fails if a reached module imports a
react-core entry other than `/v2/headless` or `/v2/context`, imports the heavy
render stack directly, or (headless entry only) pulls the optional native
chat/attachment peer deps. Runs in the normal test job.
Together they cover both directions of the #4893 regression: react-native
importing the fat entry, and the lean entry growing heavy.
**What they cover, and what they don't.** Between them the two checks catch the
two shapes of the #4893 regression that are cheap to detect statically:
react-native importing the fat `@copilotkit/react-core/v2` entry, and the heavy
render stack being _inlined_ into the lean react-core entries. Neither check
follows a dependency edge past the files it reads:
- The purity script is a **substring scan of four emitted files**. It sees only
what rolldown inlined into them. `headless` and `context` are built with
`react`, `@copilotkit/core`, `@copilotkit/shared`, `@ag-ui/*`, `rxjs`, `zod` and
`uuid` **external** (`packages/react-core/tsdown.config.ts`), so a heavy dep
arriving transitively through one of those would not appear in the scanned text
and would pass. The one external edge that _is_ covered is
`@copilotkit/react-core/v2/context` — and only because `context.mjs` / `.cjs`
are enumerated as targets in their own right, not because the scan traverses.
Being a plain substring match, it also only checks the `.mjs` / `.cjs` outputs
(not the UMD builds or the declarations), and a forbidden name in a comment or
an unrelated identifier fails it.
- The RN test reads only `.ts` / `.tsx` files under `packages/react-native/src/`.
It records bare specifiers but never resolves them, so it cannot see anything
inside `node_modules`.
A regression that reaches an RN bundle through a transitive dependency of an
externalized package is therefore caught by neither. The `size:headless` esbuild
signal (Tier 1) is what makes such a regression's _magnitude_ visible, after the
fact and without hard-failing.
## Where configuration lives
@@ -76,7 +115,7 @@ importing the fat entry, and the lean entry growing heavy.
## Adding a new measurement
Only bundled packages support local size tracking. For unbundled packages, CI covers all chunk files via the glob; no local config is needed.
Only bundled packages support local size tracking **via size-limit**. For the other six packages in the glob, CI covers all chunk files; no local config is needed. Where a specific consumer-facing import needs a number, the pattern is a bespoke esbuild script rather than a `.size-limit.json` — `react-core`'s `size:headline` and `react-native`'s `size:headless` are the two existing examples.
To add a measurement to a bundled package: