feat(build-cli): bundle size collection and comparison tooling#27151
Conversation
Note: includes changes to the pnpm-lock and pnpm-workspace that need to be reverted before this change is reviewed or checked in.
|
Hey! You look nice today! Want me to review this PR? Based on the diff (1476 lines, 6 files), I've queued these reviewers:
Toggle checkboxes to adjust, then reply yes to start — or ask me anything! |
Co-authored-by: Copilot <copilot@github.com>
There was a problem hiding this comment.
Pull request overview
Adds a local developer workflow for collecting webpack bundle stats from a “base” git ref and comparing them against the current ref, including per-asset parsed + gzip deltas. This lives under examples/utils/bundle-size-tests and is intended to speed up iterative bundle-structure investigations (not CI).
Changes:
- Add
collectBundles.ts,compareBundles.ts, andcollectAndCompareBundles.tsscripts plus npm script entry points. - Persist collected stats/build outputs under an OS temp directory to survive
git checkoutand repo cleans. - Extend lint/tsconfig setup so the new
scripts/TypeScript files are type-checked and linted; add@msgpack/msgpackfor decoding stats.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
pnpm-lock.yaml |
Adds @msgpack/msgpack and updates lock resolutions (includes an unrelated peer-resolution change flagged in comments). |
examples/utils/bundle-size-tests/tsconfig.scripts.json |
New TS config to include scripts/**/*.ts in type-aware tooling. |
examples/utils/bundle-size-tests/scripts/compareBundles.ts |
New comparison/report generator (text + JSON), including gzip computation for changed assets. |
examples/utils/bundle-size-tests/scripts/collectBundles.ts |
New collector that stashes changes, checks out refs, builds, saves stats, and restores git state. |
examples/utils/bundle-size-tests/scripts/collectAndCompareBundles.ts |
New wrapper to run collection then comparison via node --import jiti/register. |
examples/utils/bundle-size-tests/package.json |
Adds collect:* / compare:* scripts, lints scripts/, and adds @msgpack/msgpack. |
examples/utils/bundle-size-tests/eslint.config.mts |
Ensures typed linting covers script TS files via explicit tsconfig list + script-specific rule relaxations. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
examples/utils/bundle-size-tests/scripts/compareBundles.ts:490
- This script calls
main(process.argv)instead ofprocess.argv.slice(2), unlike the other scripts in this folder. While option parsing still mostly works, it’s easy to introduce subtle parsing bugs/inconsistencies (e.g., if later you add positional args). Consider switching toprocess.argv.slice(2)for consistency with the other CLIs here.
main(process.argv);
… the report Merge bucketPackageSizes and the inline rowsForAsset memoization closure into a single comparePackages(baseNodes, currentNodes) that returns both the composition buckets and the full per-package breakdown. The fluidFrameworkAll breakdown now reuses the rows already memoized during the bucket pass instead of walking that asset's module tree a second time. Also: - Rename computeComparison -> computeBundleComparison for clarity. - Filter the gzip asset table on its own gzip diff rather than on the parsed-size change set, so gzip-only changes are no longer hidden and parsed-only changes no longer show up as noise rows. - Document the report's tables, measurement units (parsed vs gzip), and attribution rules in the bundle-size-tests README. Behavior-preserving: bundle compare output is byte-identical.
packageFromModulePath and canonicalModuleKey both stripped the concatenation wrapper and then ran the same "last node_modules/, else first packages/" anchoring logic. Pull that shared step into a single moduleAnchor helper that returns the matched marker and index, so each function reduces to consuming the anchor it needs. Also rename the "(app/entry)" concept from "harness" to "synthetic entrypoint" across the comments and README (the data label is unchanged), and rename moduleAnchor's parameter to modulePath for clarity. No behavior change: bundle compare output is byte-identical.
Document, in the CollectBundleOptions.mode JSDoc, the --mode flag help, and the local-mode branch + captureLocalPatch doc, that local collection builds the outer enlistment exactly as it sits on disk: its git state (working tree, branch, revision) is never modified, and the captured staged-changes patch is only a reproducibility record that is never applied.
Relocate the conceptual `flub bundle` documentation (how the commands fit together and how to read the comparison report) out of the bundle-size-tests package README and into build-cli's docs/bundleDetails.md, following the same *Details.md convention as typetestDetails.md. Link to it from each bundle command's description so it is discoverable via --help and the generated bundle.md, and trim the package README to its package-specific wrapper plus a pointer to the new doc.
…vision mode Revision-mode bundle collection previously created the inner repo by shallow-cloning the outer repo's origin remote and fetching the requested commit by SHA, which required the commit to exist on origin and depended on network access plus uploadpack.allowReachableSHA1InWant. Clone the inner repo directly from the outer enlistment on disk instead. No remote or network access is involved, and every object the outer repo already has (including merge-base SHAs that aren't branch tips) is available without a fetch; the requested revision is checked out detached. The commit now only needs to exist locally. Removes the now-unused getOuterOriginUrl helper and updates bundleDetails.md.
#27531) ## Description Enable `importHelpers` (with a `tslib` dependency) in `@fluidframework/tree`. With this on, the TypeScript compiler emits `import { ... } from "tslib"` instead of inlining the runtime helpers (e.g. `__assign`, `__awaiter`, `__spreadArray`) into every module that uses them. Webpack can then dedupe those helpers down to a single shared copy, shrinking the bundle. Measured impact (parsed bytes), comparing against the parent revision: | Entrypoint | Base | Current | Diff | % | |---|--:|--:|--:|--:| | `sharedTree.js` | 401,086 | 392,105 | **−8,981** | **-2.2%** | | `fluidFrameworkAll.js` | 1,326,045 | 1,317,064 | **-8,981** | **-0.7%** | Gzipped: `sharedTree.js` -1,948 B, `fluidFrameworkAll.js` -1,949 B. The mechanism is visible in the per-package breakdown: inlined helpers drop out of `@fluidframework/fluid-framework` (-12,499 B) while a single shared `tslib` module is added (+3,902 B), for a net ~-8,981 B per affected entrypoint. All other entrypoints are unchanged. Changes: - Add `"importHelpers": true` to `packages/dds/tree/tsconfig.json`. - Add `tslib` as a dependency in `packages/dds/tree/package.json` (and corresponding lockfile update). ## Reviewer Guidance - `tslib` is added as a runtime dependency. This is not a widely-used dependency in our repo (client-logger/fluid-telemetry uses it, so does the devtools browser extension). Are there any potential issues if tree depends on it? - The bundle numbers above were produced with `flub bundle collect-and-compare` against the commit's parent revision. See #27151 for the version of the tools used.
ChumpChief
left a comment
There was a problem hiding this comment.
We talked about a couple of these offline already but wanted to get you this set of comments.
I think eventually there's a lot of opportunity to share/reuse with the pipeline bundle comparison tools (i.e. merge for the union of their functionalities), but I think starting with a checkin of a standalone tool like you're doing here is the right move to retain agility as we iterate even if there's some duplicated functionality short-term.
Relocate the longer explanatory inline comments in the bundle size scripts (collectBundle, collectAndCompareBundles, compareBundles) into @remarks blocks on the owning functions' doc comments, decluttering the function bodies while preserving the rationale. Short structural one-liners (e.g. section labels in renderAsText, the leaf-node marker, and the compareJsonReports shared-primitive note) are left inline. Comment-only change: no behavior, signatures, or public API affected.
In bundle revision mode the inner repo is a clone of the outer repo, so a branch from the outer repo exists in the clone only as a remote-tracking ref (origin/<branch>). A bare branch name therefore failed to resolve under 'git checkout --detach', and relative committishes such as HEAD~2 resolved against the inner clone's HEAD (the wrong commit). ensureInnerRepoAtRevision now selects the checkout target explicitly: an exact branch is checked out by its qualified origin/<branch> name (readable, never turned into a SHA), while anything else - a commit SHA, tag, or committish - is resolved to a commit SHA in the outer repo (the source of truth for HEAD and relative refs) and that SHA, which the full clone already contains, is checked out. Branch detection uses a non-throwing 'git for-each-ref' probe. Also broadens the --revision flag help to mention committishes. Validated end-to-end by collecting a branch (tbrosman/eslint-config-13), HEAD~2, and a bare SHA.
Bundle size comparisonBase commit: Notable changesNo bundles changed by ≥ 500 bytes parsed. Per-bundle deltas
|
… and simplify collect flags Move revision resolution (merge-base/exact), base-report caching via a revision.txt sidecar, and default label generation (timestamped current_<epoch>) out of the collect-and-compare orchestrator and into collectBundle, which now returns the label it saved under. collectAndCompareBundles is reduced to sequencing collect/collect/compare and deleting the scratch inner repo after the comparison. Redesign the collect command around committish-driven flags, replacing the --mode/--revision/--exact-base trio: - --revision <committish>: build that committish resolved as-is - --merge-base <committish>: build the merge-base of HEAD and the committish The two are mutually exclusive; omitting both collects the local working tree. CollectBundleOptions.exactBase is replaced with resolution: 'exact' | 'merge-base'. Also remove the unused --skip-compare flag from collect-and-compare, simplify ensureInnerRepoAtRevision to check out an already-resolved SHA, and regenerate docs/bundle.md.
…lectAndCompare Rename the command file to camelCase to match the repo's command-naming convention (e.g. `flub generate compatLayerGeneration`). This changes the invoked command from `flub bundle collect-and-compare` to `flub bundle collectAndCompare`, consistent with its siblings collect/compare. Update the bundle-size-tests compare:bundles script, the pnpm-workspace.yaml comment, and bundleDetails.md to the new name, and regenerate docs/bundle.md.
…undle Replace CollectBundleOptions.resolution with a dedicated mergeBaseOf committish: revision is always built as-is, mergeBaseOf builds the merge-base with HEAD, so specifying a revision no longer silently resolves to the fork point. Update the collect command (--merge-base) and the collect-and-compare orchestrator to match. Restructure collectBundle into clear phases (validate, resolve+cache, prepare env, build, save) via resolveBuildRevision/prepareRevisionBuild helpers, and tighten the helper doc comments.
Group the helpers into labeled sections (utilities, git resolution, build steps, environment prep, output, orchestrator) so related functions sit together. Merge buildWorkspace/buildBundles into a single buildPackage, make resolveMergeBase throw on failure (symmetric with resolveSha), simplify resolveBuildRevision to (committish, useMergeBase) with no cast, and hoist the analyzer.json / revision.txt filenames into shared constants. No behavior change.
Remove the "Named entrypoint total asset sizes" table from the bundle comparison (compareBundles) and its bundleDetails.md docs, plus the now-unused entrypoint helpers — those sizes are already captured in the per-asset table. In collectAndCompare, delete the scratch inner repo only after the report is written and the completion banner is shown, so the user sees results before the slow cleanup. Also rename the analyzer/revision filename constants to camelCase to match the bundleSize module and trim an overly verbose doc comment.
Move the default-base resolution into collectBundle: when revision mode is requested without an explicit revision/mergeBaseOf, it now defaults to the 'main' branch of the freshest remote pointing at microsoft/FluidFramework (via pickFreshestRemote) at its merge-base with HEAD, rather than relying on the local 'main' which may be stale or absent in worktree setups. collectAndCompare no longer resolves the base itself; its --base-revision flag has no default and is passed through as undefined when omitted, so an explicit value (including 'main') is always honored as given. Updates docs and regenerates docs/bundle.md.
Retopic and rename the bundle-analysis commands so they live under the standardized generate/check topics: - 'bundle collect' -> 'generate bundleAnalysisRepo' - 'bundle compare' -> 'check bundleAnalysisReposComparison' - 'bundle collectAndCompare' -> 'generate bundleAnalysisReposWithComparison' Removes the now-empty 'bundle' oclif topic, renames the companion doc to bundleAnalysisRepoDetails.md (a durable, easy-to-find name), updates all command cross-references (compare:bundles script, bundle-size-tests README, pnpm-workspace comment), and regenerates the oclif manifest and docs. The underlying library functions are unchanged.
ChumpChief
left a comment
There was a problem hiding this comment.
I think the tool seems fine to go in - I'm sure we'll iterate on it some over time but it seems helpful as-is.
I do think we should probably drop the fluidFrameworkAll bundle for now though, just to minimize disruption to our existing bundle telemetry. I can try to prioritize fixing AB#75724 to unblock it, but I think that should happen first so we can have as few "telemetry reset" events as possible.
- eslint.config.mts: drop the leftover projectService override block, orphaned after the bundle scripts (and tsconfig.scripts.json) moved out of the package. - pnpm-lock.yaml: revert incidental semver 7.7.3 -> 7.7.4 drift under @arethetypeswrong/core (a transitive dep nothing in this PR touches). - package.json: remove the orphaned 'compile' fluidBuild task whose build:scripts dependency was already deleted. - bundleSize/index.ts: stop re-exporting compareJsonReports, which is only used via direct import and consumed by no command through the index. - README.md: correct the Outputs section to the actual compareBundlesOutput/ paths (analysis/<label>/, root reports, base-repo/).
Remove the fluidFrameworkAll aggregate entrypoint and everything that fed off it: delete examples/utils/bundle-size-tests/src/fluidFrameworkAll.ts, its webpack entry, and the now-unneeded extensionAlias resolver config. In compareBundles.ts, re-scope the composition buckets and the per-package breakdown to the sharedTree entrypoint, drop the two Fluid Framework buckets, and update bundleAnalysisRepoDetails.md to match. Clarify the bucketDefinitions docstring with a plain-language description of the (app/entry) exclusion and fix the malformed '@ remarks' tag. Restore the jiti devDependency (and its lockfile entry) to bundle-size-tests. jiti is required for eslint 9 to load the TypeScript eslint.config.mts and was over-removed alongside the script-only deps; this realigns the package with the other 173 client packages that declare it.
Remove the --analysis-dir, --output-dir, --force-clean-build, and --label flags from the bundle-analysis commands (generate bundleAnalysisRepo, generate bundleAnalysisReposWithComparison, check bundleAnalysisReposComparison). Each command now derives its paths from --package-dir and the fixed compareBundlesOutput convention (per-label analyzer stats under compareBundlesOutput/analysis/<label>/, reports at compareBundlesOutput/), always builds without the workspace clean, and lets labels be auto-generated. Keep --base-label/--current-label on the compare command, since pairing specific labels is needed for other workflows. Define the 'label' concept in bundleAnalysisRepoDetails.md (the subdirectory each collected bundle's analyzer.json lands in) and make the collect command's --revision/--merge-base help self-explanatory now that --label is gone. Fix a stale --force-clean-build example and regenerate the command docs.
|
🔗 No broken links found! ✅ Your attention to detail is admirable. linkcheck output |
Adds a local dev workflow for collecting webpack bundle stats from a base
git ref and comparing them against the current ref - per-asset parsed/gzip
deltas, per-entrypoint totals, and per-package size breakdown. Checks out an
isolated shallow clone at the base revision so the outer repo is never
touched. Intended for inner-loop bundle investigations, not CI.
Description
Core implementation (src/library/bundleSize):
an inner enlistment at a given ref) and saves its analyzer.json.
and per-package tables (text + JSON), tolerating differing asset/package sets.
the merge-base of HEAD by default, or used as-is with exactBase. Caches the
base report by SHA to skip rebuilds.
flub commands (src/commands/generate, src/commands/check + docs):
generate bundleAnalysisRepo,check bundleAnalysisReposComparison, andgenerate bundleAnalysisReposWithComparisonwrap the above. Regenerateddocs/generate.md and docs/check.md, with the companion deep-dive in
docs/bundleAnalysisRepoDetails.md.
Client code (non-shipping):
Todo
@remarksfor verbose "why" comments.