Add tree-shaking analysis scripts and bundle size reporting#7032
Add tree-shaking analysis scripts and bundle size reporting#7032PavelVanecek merged 7 commits intomainfrom
Conversation
Co-authored-by: PavelVanecek <1100170+PavelVanecek@users.noreply.github.com>
…queness Co-authored-by: PavelVanecek <1100170+PavelVanecek@users.noreply.github.com>
Bundle ReportChanges will increase total bundle size by 2.07MB (71.44%) ⬆️
Affected Assets, Files, and Routes:view changes for bundle: recharts/bundle-cjsAssets Changed:
view changes for bundle: recharts/bundle-umdAssets Changed:
view changes for bundle: recharts/bundle-es6Assets Changed:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7032 +/- ##
=======================================
Coverage 90.12% 90.12%
=======================================
Files 526 526
Lines 39183 39242 +59
Branches 5422 5438 +16
=======================================
+ Hits 35312 35368 +56
- Misses 3862 3865 +3
Partials 9 9 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
Co-authored-by: PavelVanecek <1100170+PavelVanecek@users.noreply.github.com>
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
|
@copilot codecov is now reporting only one bundle. We expect that it should output the previous 3 bundles + all the new ones (cartesian bundle, polar bundle, treemap sankey sunburst bundles). Why is that? See the output below: Bundle ReportChanges will increase total bundle size by 1.47MB (50.64%) ⬆️ Bundle Report Changes will increase total bundle size by 1.47MB (50.64%) ⬆️ |
This is expected Codecov behavior — the PR bundle diff only shows bundles that have changed vs the base branch. Since this PR adds only tooling scripts without touching library source code, the All 4 bundles are still uploaded by the script — you can verify by clicking the full Codecov bundle report link rather than just the PR diff view. |
|
Okay @copilot that's fair and you are correct in the details I see 4 bundles. However what we want to see are 8 bundles:
|
Co-authored-by: PavelVanecek <1100170+PavelVanecek@users.noreply.github.com>
Fixed in 3f91be8 — each bundle now gets its own unique Codecov name, so all 8 will appear separately:
|
|
@copilot question: do the new bundles include comments, or exclude? |
The new bundles exclude comments. Rollup strips all comments from its output as part of normal bundle generation — only the actual executable code is in the generated files. I verified this: a |
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
Important Review skippedBot user detected. To trigger a single review, invoke the You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR introduces a comprehensive tree-shaking testing and bundle analysis infrastructure for Recharts. It adds Rollup-based utilities to measure bundle sizes, detect component tree-shaking behavior, includes test coverage for bundle integrity, and extends the bundle upload pipeline with tree-shaking bundle generation for various component groups. Changes
Sequence Diagram(s)sequenceDiagram
participant Test as Test Suite
participant Treeshake as treeshake()
participant Rollup as Rollup
participant Output as Output Analyzer
participant Assert as Assertions
Test->>Treeshake: treeshake(componentNames)
Treeshake->>Treeshake: Create temp entry file
Treeshake->>Rollup: Bundle with config<br/>(external deps, no side-effects)
Rollup->>Rollup: Resolve & transform code
Rollup-->>Treeshake: OutputChunk[]
Treeshake->>Output: findComponentsInBundle(output)
Output->>Output: Scan bundle code<br/>for definitions
Output-->>Treeshake: Found components[]
Treeshake->>Output: getBundleSize(output)
Output-->>Treeshake: Size in bytes
Treeshake-->>Test: Results
Test->>Assert: Verify expected<br/>components present
Test->>Assert: Verify bundle > 0 bytes
Test->>Assert: Mark as todo if<br/>knownIssue exists
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (8)
scripts/treeshaking.ts (2)
32-63: Minor:bundle.close()skipped ifgenerate()throws.If
bundle.generate()on Line 58 throws,bundle.close()on Line 59 is never called. Consider wrapping the bundle lifecycle in its own try/finally to ensure cleanup.♻️ Proposed fix
try { const bundle = await rollup({ input: tmpFile, external: getExternals(), treeshake: { moduleSideEffects: false, }, logLevel: 'silent', plugins: [ { name: 'resolve-directory-index', resolveId(moduleId: string, importer: string | undefined) { if (importer != null && moduleId.startsWith('.')) { const resolved = path.resolve(path.dirname(importer), moduleId); if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) { const indexFile = path.join(resolved, 'index.js'); if (fs.existsSync(indexFile)) { return indexFile; } } } return null; }, }, ], }); - const output = await bundle.generate({ format: 'esm' }); - await bundle.close(); - return output.output; + try { + const output = await bundle.generate({ format: 'esm' }); + return output.output; + } finally { + await bundle.close(); + } } finally { fs.unlinkSync(tmpFile); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/treeshaking.ts` around lines 32 - 63, The rollup bundle is not guaranteed to be closed if bundle.generate() throws; wrap the rollup lifecycle so bundle.close() always runs: after creating bundle = await rollup(...) keep the outer finally that unlinks tmpFile, but introduce an inner try/finally around await bundle.generate({ format: 'esm' }) that always calls await bundle.close() in the finally block (only call close if bundle is defined), ensuring bundle.generate, bundle.close, and rollup are referenced correctly and tmpFile cleanup remains in the existing outer finally.
9-57: Significant code duplication withscripts/upload-bundle-analysis.js.The
getExternals()function,es6EntryandpackageRootconstants, temp file naming approach, rollup configuration, and theresolve-directory-indexplugin are duplicated across both files. The implementations are functionally identical, thoughgetExternals()has minor stylistic differences (treeshaking.ts uses aspecialDepsconstant while the upload script inlines the dependency). If either copy is updated independently, the other will silently drift.Consider extracting these utilities into a shared
.mjsmodule that both scripts can import, or convert the upload script to TypeScript to share the module directly. This prevents maintenance burden and ensures consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/treeshaking.ts` around lines 9 - 57, Duplicate utilities (getExternals, es6Entry, packageRoot, tmp file naming, rollup config and the resolve-directory-index plugin) are repeated between scripts/treeshaking.ts and scripts/upload-bundle-analysis.js; extract them into a single shared module and import it from both scripts to avoid drift. Create a shared module that exports getExternals, es6Entry, packageRoot, a helper for creating tmp filenames, and a function or object for the common rollup plugin/configuration, then replace the local definitions in treeshaking.ts (symbols: getExternals, es6Entry, packageRoot, tmpFile creation, and the inline plugin named "resolve-directory-index") and in upload-bundle-analysis.js to import and use the shared exports so both scripts use the exact same implementation.scripts/treeshaking.test.ts (3)
50-98: Bundle size tests are functional; minor DRY opportunity.The six bundle-size tests follow an identical pattern (treeshake → getBundleSize → assert > 0 → log). This is readable as-is, but if the list of bundles grows, consider a table-driven approach:
♻️ Optional: parameterize bundle size tests
const bundleSizeCases: [string, string | string[]][] = [ ['Cartesian components', [...CARTESIAN_COMPONENT_NAMES]], ['Polar components', [...POLAR_COMPONENT_NAMES]], ['Treemap', 'Treemap'], ['SunburstChart', 'SunburstChart'], ['Sankey', 'Sankey'], ['Chart components', [...CHART_COMPONENT_NAMES]], ]; for (const [label, components] of bundleSizeCases) { it(`${label} bundle size`, async () => { const output = await treeshake(components); const size = getBundleSize(output); expect(size).toBeGreaterThan(0); console.log(`${label} bundle size: ${size} bytes`); }, 60_000); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/treeshaking.test.ts` around lines 50 - 98, The tests repeat the pattern treeshake → getBundleSize → expect(...) → console.log, so refactor to a table-driven loop to remove duplication: create an array (e.g., bundleSizeCases) of tuples pairing the human label with the components (use CARTESIAN_COMPONENT_NAMES, POLAR_COMPONENT_NAMES, CHART_COMPONENT_NAMES or string names like 'Treemap'), then iterate over that array to call treeshake(components), compute getBundleSize(output), assert expect(size).toBeGreaterThan(0) and log the label; keep the individual timeout (60_000) for each generated it block and preserve test names by using the label when creating the it(...) description.
18-20:knownExpectedBundleis mapped but never used inside the test body.The
expectedInBundledata from each known issue is stored inknownIssuesByComponentand read intoknownExpectedBundle(line 24), but the test function (lines 26-34) never references it — it checks for all other tracked components, not specifically the expected ones.This is fine for the
it.fails()gating logic (you only need to know whether a known issue exists, not which components it documents). But then you could simplify to just checking for the component name's presence rather than storing the fullexpectedInBundlevalue:♻️ Optional simplification
-const knownIssuesByComponent = Object.fromEntries( - knownTreeshakingIssues.map(issue => [issue.component, issue.expectedInBundle]), -); +const knownIssueComponents = new Set( + knownTreeshakingIssues.map(issue => issue.component), +);Then on line 24/36:
- const knownExpectedBundle = knownIssuesByComponent[componentName]; ... - if (knownExpectedBundle != null) { + if (knownIssueComponents.has(componentName)) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/treeshaking.test.ts` around lines 18 - 20, The test builds knownIssuesByComponent from knownTreeshakingIssues but then reads knownExpectedBundle (from knownIssuesByComponent) and never uses it in the test body; either use the expectedInBundle value in the assertions inside the test function (reference knownExpectedBundle and assert the bundle content matches expectedInBundle for that component) or simplify the setup to only track component names (e.g., a Set of component strings) and change the test to check presence by component name instead of storing expectedInBundle; update either the mapping creation (knownIssuesByComponent / knownTreeshakingIssues) and the test assertion logic accordingly so the stored data is actually used or removed.
22-48: Well-designed dynamic test generation withit.fails()for known issues.The pattern of using
it.fails()to document known tree-shaking problems while keeping CI green — and having it break when the issue is fixed (prompting removal of the known-issue entry) — is a clean approach.One minor observation: the test verifies that importing component X doesn't include other tracked components, but it doesn't verify that component X itself is present in the bundle. If tree-shaking were overly aggressive and removed the target component entirely, this test would still pass. Consider adding a positive assertion:
💡 Optional: verify the target component is present
const testFn = async () => { const output = await treeshake(componentName); + const selfInBundle = findComponentsInBundle(output, [componentName]); + expect(selfInBundle, `${componentName} should be present in its own bundle`).toEqual([componentName]); const otherComponents = ALL_TRACKED_COMPONENT_NAMES.filter(n => n !== componentName); const unexpectedComponents = findComponentsInBundle(output, otherComponents);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/treeshaking.test.ts` around lines 22 - 48, The test currently only asserts that importing componentName does NOT pull in other tracked components; add a positive assertion that the target component is actually present in the bundle to catch over-aggressive tree-shaking: after calling treeshake(componentName) (in testFn) use findComponentsInBundle(output, [componentName]) and assert that the returned list includes componentName (or is non-empty) before asserting unexpectedComponents is empty; keep this check in the same testFn so both the presence of the target and absence of others are validated for functions like treeshake, findComponentsInBundle, and the loop over ALL_TRACKED_COMPONENT_NAMES (including handling knownExpectedBundle / it.fails).scripts/upload-bundle-analysis.js (3)
119-140: Consider converting the.then()chain toasync/awaitfor readability.The sequential chain of eight
.then()calls is verbose. An async IIFE or top-level async function would be cleaner and easier to extend:♻️ Optional refactor
(async () => { await uploadBundleAnalysis('lib', 'recharts/bundle-cjs'); await uploadBundleAnalysis('es6', 'recharts/bundle-es6'); await uploadBundleAnalysis('umd', 'recharts/bundle-umd'); await generateAndUploadTreeshakingBundle(CARTESIAN_COMPONENTS, 'recharts/bundle-treeshaking-cartesian'); await generateAndUploadTreeshakingBundle(POLAR_COMPONENTS, 'recharts/bundle-treeshaking-polar'); await generateAndUploadTreeshakingBundle(['Treemap'], 'recharts/bundle-treeshaking-treemap'); await generateAndUploadTreeshakingBundle(['SunburstChart'], 'recharts/bundle-treeshaking-sunburst'); await generateAndUploadTreeshakingBundle(['Sankey'], 'recharts/bundle-treeshaking-sankey'); })().catch(error => { console.error('Bundle analysis failed:', error); process.exitCode = 1; });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/upload-bundle-analysis.js` around lines 119 - 140, Replace the long Promise chain with an async flow: wrap the sequential calls to uploadBundleAnalysis and generateAndUploadTreeshakingBundle (references: uploadBundleAnalysis, generateAndUploadTreeshakingBundle, CARTESIAN_COMPONENTS, POLAR_COMPONENTS) in an async function or async IIFE and await each call in order, and add a single catch at the end to log errors and set a non-zero process.exitCode; this simplifies control flow and preserves sequential execution while making it easier to extend.
39-86: Significant code duplication withscripts/treeshaking.ts.
generateTreeshakingBundleduplicates the rollup config (resolve-directory-index plugin, treeshake options, temp file pattern) fromtreeshake()intreeshaking.ts. Additionally,CARTESIAN_COMPONENTSandPOLAR_COMPONENTSduplicateCARTESIAN_COMPONENT_NAMESandPOLAR_COMPONENT_NAMES.This creates a maintenance risk: changes to component lists, externals, or rollup config in one file but not the other will produce silently inconsistent results between CI bundle reports and test assertions.
If cross-language import is impractical, consider at minimum extracting the component name arrays to a shared JSON or
.jsfile that both can consume.Also applies to: 88-107
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/upload-bundle-analysis.js` around lines 39 - 86, The generateTreeshakingBundle function duplicates rollup config, the custom resolve-directory-index plugin, temp-entry filename generation, and component lists (CARTESIAN_COMPONENTS, POLAR_COMPONENTS) already defined alongside treeshake() and CARTESIAN_COMPONENT_NAMES/POLAR_COMPONENT_NAMES in scripts/treeshaking.ts; extract the shared pieces into a single small module (e.g., export the component name arrays, getExternals(), a helper to create the temp entry filename, and the resolve-directory-index plugin or a function that returns the plugins array) and have both generateTreeshakingBundle and treeshake() import and reuse those exports so there is one source of truth for component lists, externals, and rollup configuration to avoid drift.
10-15:getExternals()is duplicated fromscripts/treeshaking.ts.This function is nearly identical to the one in
treeshaking.ts. If the externals list diverges between the two files, bundle analysis results will be inconsistent with what the tests measure.Since this file is
.jsandtreeshaking.tsis TypeScript, direct import may not be straightforward. Consider extracting a shared.jsmodule (or compiling the TS utilities) so both files reference a single source of truth for externals.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/upload-bundle-analysis.js` around lines 10 - 15, Duplicate getExternals() logic should be centralized: extract the function from scripts/upload-bundle-analysis.js into a shared JS module (e.g., scripts/getExternals.js) that exports getExternals, then replace the inline implementation in upload-bundle-analysis.js and the one in treeshaking.ts to import/require that shared module (or compile treeshaking.ts utilities to JS and import them) so both use the same single source of truth; update references to the unique symbol getExternals (and preserve behavior: reading package.json, merging dependencies and peerDependencies, and the vendor regex entries) and run tests to ensure bundle analysis and treeshaking use identical externals.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@scripts/upload-bundle-analysis.js`:
- Around line 119-140: The promise chain starting with uploadBundleAnalysis(...)
and subsequent generateAndUploadTreeshakingBundle(...) calls lacks a terminal
.catch(), so failures in generateAndUploadTreeshakingBundle or later steps can
cause unhandled rejections; append a final .catch handler to the chain that logs
the error (including error.stack) and exits non-zero (e.g., process.exit(1)) to
ensure CI fails clearly; target the chain that begins with uploadBundleAnalysis
and include logging via console.error or the existing logger so all errors from
uploadBundleAnalysis and generateAndUploadTreeshakingBundle (and data like
CARTESIAN_COMPONENTS, POLAR_COMPONENTS) are surfaced.
- Line 41: The generated ES module import specifier uses es6Entry (built with
path.join) which yields backslashes on Windows and breaks the import string
assigned to source; fix by normalizing es6Entry to use forward slashes before
interpolating into source (e.g., replace backslashes with '/' or build the path
with path.posix.join) and apply the same normalization where treeshaking.ts
constructs ES module specifiers; update the code that computes es6Entry (and the
equivalent in treeshaking.ts) so the interpolated import uses the normalized
path.
---
Nitpick comments:
In `@scripts/treeshaking.test.ts`:
- Around line 50-98: The tests repeat the pattern treeshake → getBundleSize →
expect(...) → console.log, so refactor to a table-driven loop to remove
duplication: create an array (e.g., bundleSizeCases) of tuples pairing the human
label with the components (use CARTESIAN_COMPONENT_NAMES, POLAR_COMPONENT_NAMES,
CHART_COMPONENT_NAMES or string names like 'Treemap'), then iterate over that
array to call treeshake(components), compute getBundleSize(output), assert
expect(size).toBeGreaterThan(0) and log the label; keep the individual timeout
(60_000) for each generated it block and preserve test names by using the label
when creating the it(...) description.
- Around line 18-20: The test builds knownIssuesByComponent from
knownTreeshakingIssues but then reads knownExpectedBundle (from
knownIssuesByComponent) and never uses it in the test body; either use the
expectedInBundle value in the assertions inside the test function (reference
knownExpectedBundle and assert the bundle content matches expectedInBundle for
that component) or simplify the setup to only track component names (e.g., a Set
of component strings) and change the test to check presence by component name
instead of storing expectedInBundle; update either the mapping creation
(knownIssuesByComponent / knownTreeshakingIssues) and the test assertion logic
accordingly so the stored data is actually used or removed.
- Around line 22-48: The test currently only asserts that importing
componentName does NOT pull in other tracked components; add a positive
assertion that the target component is actually present in the bundle to catch
over-aggressive tree-shaking: after calling treeshake(componentName) (in testFn)
use findComponentsInBundle(output, [componentName]) and assert that the returned
list includes componentName (or is non-empty) before asserting
unexpectedComponents is empty; keep this check in the same testFn so both the
presence of the target and absence of others are validated for functions like
treeshake, findComponentsInBundle, and the loop over ALL_TRACKED_COMPONENT_NAMES
(including handling knownExpectedBundle / it.fails).
In `@scripts/treeshaking.ts`:
- Around line 32-63: The rollup bundle is not guaranteed to be closed if
bundle.generate() throws; wrap the rollup lifecycle so bundle.close() always
runs: after creating bundle = await rollup(...) keep the outer finally that
unlinks tmpFile, but introduce an inner try/finally around await
bundle.generate({ format: 'esm' }) that always calls await bundle.close() in the
finally block (only call close if bundle is defined), ensuring bundle.generate,
bundle.close, and rollup are referenced correctly and tmpFile cleanup remains in
the existing outer finally.
- Around line 9-57: Duplicate utilities (getExternals, es6Entry, packageRoot,
tmp file naming, rollup config and the resolve-directory-index plugin) are
repeated between scripts/treeshaking.ts and scripts/upload-bundle-analysis.js;
extract them into a single shared module and import it from both scripts to
avoid drift. Create a shared module that exports getExternals, es6Entry,
packageRoot, a helper for creating tmp filenames, and a function or object for
the common rollup plugin/configuration, then replace the local definitions in
treeshaking.ts (symbols: getExternals, es6Entry, packageRoot, tmpFile creation,
and the inline plugin named "resolve-directory-index") and in
upload-bundle-analysis.js to import and use the shared exports so both scripts
use the exact same implementation.
In `@scripts/upload-bundle-analysis.js`:
- Around line 119-140: Replace the long Promise chain with an async flow: wrap
the sequential calls to uploadBundleAnalysis and
generateAndUploadTreeshakingBundle (references: uploadBundleAnalysis,
generateAndUploadTreeshakingBundle, CARTESIAN_COMPONENTS, POLAR_COMPONENTS) in
an async function or async IIFE and await each call in order, and add a single
catch at the end to log errors and set a non-zero process.exitCode; this
simplifies control flow and preserves sequential execution while making it
easier to extend.
- Around line 39-86: The generateTreeshakingBundle function duplicates rollup
config, the custom resolve-directory-index plugin, temp-entry filename
generation, and component lists (CARTESIAN_COMPONENTS, POLAR_COMPONENTS) already
defined alongside treeshake() and
CARTESIAN_COMPONENT_NAMES/POLAR_COMPONENT_NAMES in scripts/treeshaking.ts;
extract the shared pieces into a single small module (e.g., export the component
name arrays, getExternals(), a helper to create the temp entry filename, and the
resolve-directory-index plugin or a function that returns the plugins array) and
have both generateTreeshakingBundle and treeshake() import and reuse those
exports so there is one source of truth for component lists, externals, and
rollup configuration to avoid drift.
- Around line 10-15: Duplicate getExternals() logic should be centralized:
extract the function from scripts/upload-bundle-analysis.js into a shared JS
module (e.g., scripts/getExternals.js) that exports getExternals, then replace
the inline implementation in upload-bundle-analysis.js and the one in
treeshaking.ts to import/require that shared module (or compile treeshaking.ts
utilities to JS and import them) so both use the same single source of truth;
update references to the unique symbol getExternals (and preserve behavior:
reading package.json, merging dependencies and peerDependencies, and the vendor
regex entries) and run tests to ensure bundle analysis and treeshaking use
identical externals.
|
|
||
| async function generateTreeshakingBundle(components, outputFile) { | ||
| const importList = components.join(', '); | ||
| const source = `import { ${importList} } from '${es6Entry}';\nexport const used = { ${importList} };\n`; |
There was a problem hiding this comment.
Path separator issue in generated ES module source on Windows.
es6Entry is built with path.join (line 8), which uses backslashes on Windows. When interpolated into the import statement template, this produces an invalid ES module specifier like import { ... } from 'C:\...\es6\index.js'. The same pattern exists in treeshaking.ts.
If this only runs in Linux CI, this is low risk. Otherwise, normalize to forward slashes:
🛠️ Proposed fix
- const source = `import { ${importList} } from '${es6Entry}';\nexport const used = { ${importList} };\n`;
+ const normalizedEntry = es6Entry.split(path.sep).join('/');
+ const source = `import { ${importList} } from '${normalizedEntry}';\nexport const used = { ${importList} };\n`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const source = `import { ${importList} } from '${es6Entry}';\nexport const used = { ${importList} };\n`; | |
| const normalizedEntry = es6Entry.split(path.sep).join('/'); | |
| const source = `import { ${importList} } from '${normalizedEntry}';\nexport const used = { ${importList} };\n`; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/upload-bundle-analysis.js` at line 41, The generated ES module import
specifier uses es6Entry (built with path.join) which yields backslashes on
Windows and breaks the import string assigned to source; fix by normalizing
es6Entry to use forward slashes before interpolating into source (e.g., replace
backslashes with '/' or build the path with path.posix.join) and apply the same
normalization where treeshaking.ts constructs ES module specifiers; update the
code that computes es6Entry (and the equivalent in treeshaking.ts) so the
interpolated import uses the normalized path.
| uploadBundleAnalysis('lib', 'recharts/bundle-cjs') | ||
| .then(() => { | ||
| return uploadBundleAnalysis('es6', 'recharts/bundle-es6'); | ||
| }) | ||
| .then(() => { | ||
| return uploadBundleAnalysis('umd', 'recharts/bundle-umd'); | ||
| }) | ||
| .then(() => { | ||
| return generateAndUploadTreeshakingBundle(CARTESIAN_COMPONENTS, 'recharts/bundle-treeshaking-cartesian'); | ||
| }) | ||
| .then(() => { | ||
| return generateAndUploadTreeshakingBundle(POLAR_COMPONENTS, 'recharts/bundle-treeshaking-polar'); | ||
| }) | ||
| .then(() => { | ||
| return generateAndUploadTreeshakingBundle(['Treemap'], 'recharts/bundle-treeshaking-treemap'); | ||
| }) | ||
| .then(() => { | ||
| return generateAndUploadTreeshakingBundle(['SunburstChart'], 'recharts/bundle-treeshaking-sunburst'); | ||
| }) | ||
| .then(() => { | ||
| return generateAndUploadTreeshakingBundle(['Sankey'], 'recharts/bundle-treeshaking-sankey'); | ||
| }); |
There was a problem hiding this comment.
No terminal .catch() on the promise chain — unhandled rejections possible.
The original three-step chain relied on uploadBundleAnalysis swallowing its own errors (line 36). The new generateAndUploadTreeshakingBundle steps don't have internal .catch() handling, so a rollup failure (e.g., missing es6 build, invalid component name) would propagate as an unhandled promise rejection, potentially crashing the CI step without a clear error message.
🛠️ Proposed fix: add a terminal catch
.then(() => {
return generateAndUploadTreeshakingBundle(['Sankey'], 'recharts/bundle-treeshaking-sankey');
- });
+ })
+ .catch(error => {
+ console.error('Bundle analysis failed:', error);
+ process.exitCode = 1;
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| uploadBundleAnalysis('lib', 'recharts/bundle-cjs') | |
| .then(() => { | |
| return uploadBundleAnalysis('es6', 'recharts/bundle-es6'); | |
| }) | |
| .then(() => { | |
| return uploadBundleAnalysis('umd', 'recharts/bundle-umd'); | |
| }) | |
| .then(() => { | |
| return generateAndUploadTreeshakingBundle(CARTESIAN_COMPONENTS, 'recharts/bundle-treeshaking-cartesian'); | |
| }) | |
| .then(() => { | |
| return generateAndUploadTreeshakingBundle(POLAR_COMPONENTS, 'recharts/bundle-treeshaking-polar'); | |
| }) | |
| .then(() => { | |
| return generateAndUploadTreeshakingBundle(['Treemap'], 'recharts/bundle-treeshaking-treemap'); | |
| }) | |
| .then(() => { | |
| return generateAndUploadTreeshakingBundle(['SunburstChart'], 'recharts/bundle-treeshaking-sunburst'); | |
| }) | |
| .then(() => { | |
| return generateAndUploadTreeshakingBundle(['Sankey'], 'recharts/bundle-treeshaking-sankey'); | |
| }); | |
| uploadBundleAnalysis('lib', 'recharts/bundle-cjs') | |
| .then(() => { | |
| return uploadBundleAnalysis('es6', 'recharts/bundle-es6'); | |
| }) | |
| .then(() => { | |
| return uploadBundleAnalysis('umd', 'recharts/bundle-umd'); | |
| }) | |
| .then(() => { | |
| return generateAndUploadTreeshakingBundle(CARTESIAN_COMPONENTS, 'recharts/bundle-treeshaking-cartesian'); | |
| }) | |
| .then(() => { | |
| return generateAndUploadTreeshakingBundle(POLAR_COMPONENTS, 'recharts/bundle-treeshaking-polar'); | |
| }) | |
| .then(() => { | |
| return generateAndUploadTreeshakingBundle(['Treemap'], 'recharts/bundle-treeshaking-treemap'); | |
| }) | |
| .then(() => { | |
| return generateAndUploadTreeshakingBundle(['SunburstChart'], 'recharts/bundle-treeshaking-sunburst'); | |
| }) | |
| .then(() => { | |
| return generateAndUploadTreeshakingBundle(['Sankey'], 'recharts/bundle-treeshaking-sankey'); | |
| }) | |
| .catch(error => { | |
| console.error('Bundle analysis failed:', error); | |
| process.exitCode = 1; | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/upload-bundle-analysis.js` around lines 119 - 140, The promise chain
starting with uploadBundleAnalysis(...) and subsequent
generateAndUploadTreeshakingBundle(...) calls lacks a terminal .catch(), so
failures in generateAndUploadTreeshakingBundle or later steps can cause
unhandled rejections; append a final .catch handler to the chain that logs the
error (including error.stack) and exits non-zero (e.g., process.exit(1)) to
ensure CI fails clearly; target the chain that begins with uploadBundleAnalysis
and include logging via console.error or the existing logger so all errors from
uploadBundleAnalysis and generateAndUploadTreeshakingBundle (and data like
CARTESIAN_COMPONENTS, POLAR_COMPONENTS) are surfaced.
|
@copilot I have updated the name matching in |
…ed matching Co-authored-by: PavelVanecek <1100170+PavelVanecek@users.noreply.github.com>
Done in 983924a — added individual known-issue files for all 30 newly-detected failures (the updated |
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
Integrates BundleWatch to track bundle sizes on every CI run, with PR
comments showing size deltas against `main`.
## Description
**`.bundlewatch.config.json`** — new config tracking 6 bundles:
- `umd/Recharts.js` (minified UMD, 600kB limit)
-
`bundlewatch-bundles/treeshaking-{cartesian,polar,treemap,sunburst,sankey}.js`
(300kB limit each)
**`scripts/upload-bundle-analysis.js`** —
`generateAndUploadTreeshakingBundle` now accepts an optional
`bundlewatchOutputFile`; when provided, copies the bundle to
`bundlewatch-bundles/` before the temp dir is cleaned up. All 5
tree-shaking calls pass their respective output paths so BundleWatch has
persistent files to read.
**`.github/workflows/ci.yml`** — `BundleWatch` step added after the
Codecov bundle analysis step (which now populates
`bundlewatch-bundles/`). Passes `BUNDLEWATCH_GITHUB_TOKEN` from secrets
and sets the required CI env vars (`CI_COMMIT_SHA` uses the PR head SHA
for pull requests, `CI_BRANCH_BASE` falls back to `main` for direct
pushes).
**`.gitignore`** — `bundlewatch-bundles/` excluded.
**`package.json`** — `bundlewatch@^0.4.1` added to devDependencies;
`bundlewatch` npm script added as `bundlewatch --config
.bundlewatch.config.json` (BundleWatch does not auto-discover
`.bundlewatch.config.json` by name — without an explicit `--config` flag
it looks in `package.json` and fails with "Configuration missing").
## Related Issue
Tracks the same 5 tree-shaking bundles plus the UMD artifact introduced
in #7032.
## Motivation and Context
Without BundleWatch, there is no automated per-PR signal on whether
bundle sizes are growing. BundleWatch posts a PR comment summarising
size changes vs the `main` baseline, giving reviewers actionable data
before merging.
## How Has This Been Tested?
CI workflow updated; will validate on first run against this PR. The
`upload-bundle-analysis.js` change is additive (file copy before
temp-dir cleanup) and does not affect the existing Codecov upload path.
## Screenshots (if appropriate):
## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
## Checklist:
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [ ] I have added a storybook story or VR test, or extended an existing
story or VR test to show my changes
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
>
> ----
>
> *This section details on the original issue you should resolve*
>
> <issue_title>Onboard to BundleWatch</issue_title>
> <issue_description>Onboard this repository to BundleWatch so that we
can track bundle sizes. Follow
https://bundlewatch.io/guide/getting-started. Make it track the same
bundles as codecov is tracking. Note that we have added new bundles in
#7032.
>
> I have already configured BUNDLEWATCH_GITHUB_TOKEN action environment
secret.
>
> We want to track bundle sizes in main branch, then each PR, and have a
comment on each PR that shows how much each bundle size will change if
we were to merge that PR.</issue_description>
>
> ## Comments on the Issue (you are @copilot in this section)
>
> <comments>
> </comments>
>
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes #7038
<!-- START COPILOT CODING AGENT TIPS -->
---
💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Chores**
* Added bundle size monitoring to the continuous integration pipeline to
automatically track and enforce library size limits across distribution
formats.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: PavelVanecek <1100170+PavelVanecek@users.noreply.github.com>
… visualization (#7037) ## Description Adds two on-demand developer tools for understanding and debugging tree-shaking and bundle composition: **`bundle-viz/` — standalone Vite + React app** - Tab-toggles between **Treemap** (rectangles sized by rendered bytes, coloured by source directory) and **Sunburst** (same data as concentric rings) - Stats header: total size, module count, directory count, component list - Directory colour legend - Aliases `recharts → ../src` via Vite so no separate build step is required **`scripts/generate-bundle-data.ts`** — data pipeline feeding the app - Runs rollup tree-shaking, groups modules by source directory path, computes per-node byte totals recursively, writes hierarchical JSON to `bundle-viz/src/bundle-data.json` **`scripts/treeshaking.ts`** — extended with `getModuleGraph()` (previous PR) supporting `generate-bundle-data` ### Usage ```sh npm run build-es6 npm run generate-bundle-data # defaults to all tracked components npm run generate-bundle-data -- Area # or a specific set cd bundle-viz && npm install && npm run dev # http://localhost:3001 ``` ## Related Issue Bundle size visualization tooling follow-up. ## Motivation and Context The existing tree-shaking tests document *what* ends up co-bundled but give no visual sense of *how much* each file contributes. This app answers that question at a glance, and makes it easy to spot unexpectedly large modules or directories. ## How Has This Been Tested? - Generated real data for all 34 tracked components and verified both chart views render correctly - Verified app builds cleanly with both the placeholder and real data - All 40 existing treeshaking tests pass unchanged ## Screenshots (if appropriate): **Treemap view**  **Sunburst view**  ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist: - [ ] My change requires a change to the documentation. - [ ] I have updated the documentation accordingly. - [ ] I have added tests to cover my changes. - [ ] I have added a storybook story or VR test, or extended an existing story or VR test to show my changes > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `storybook.js.org` > - Triggering command: `/opt/hostedtoolcache/node/24.13.0/x64/bin/node node /home/REDACTED/work/recharts/recharts/node_modules/.bin/vitest run scripts/treeshaking.test.ts /pro�� d" | sort | uniq3001 0/x64/bin/node h` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/recharts/recharts/settings/copilot/coding_agent) (admins only) > > </details> <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Bundle size visualization</issue_title> > <issue_description>In #7032 we have added a script that creates a bundle with tree-shaking enabled. We have also added tests that show which components are pulled together, which highlights some problems we have with tree-shaking. Next task is to debug 1. why these components are pulled together and 2. if it is a problem > > The task is: > 1. Add a visualization tool that shows which file contributes how much kB. > 2. Add a debug trace tool that shows why does component A include component B. For example, if I import `Area` component, the final bundle has `Pie` in it. Why? > > This visualization and trace can run only on-demand, it does not need to be integrated with CI. If you need to add any dependencies make sure they are dev-only, and none of this ends up in the npm package.</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #7036 <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added bundle visualization tool to analyze package size contributions via interactive Treemap and Sunburst views. * Added new npm scripts for bundle analysis: `visualize-bundle`, `trace-bundle`, and `generate-bundle-data`. * **Chores** * Enhanced CI workflow with Node.js initialization. * Updated build tooling configurations and ignores for bundle analysis artifacts. * Improved treeshaking validation with dynamic component tracking. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: PavelVanecek <1100170+PavelVanecek@users.noreply.github.com> Co-authored-by: Pavel Vanecek <corkscreewe@gmail.com>
Adds tooling to observe and enforce tree-shaking quality across recharts components — detecting when importing one component unexpectedly pulls in others.
Description
scripts/treeshaking.ts— core utilitytreeshake(components)— rollup-bundles specific components from thees6build withmoduleSideEffects: falsegetBundleSize(output)— returns total byte size of rollup outputfindComponentsInBundle(output, names)— detects which component names appear in the bundle output usingcode.includes(name)CHART_COMPONENT_NAMES,CARTESIAN_COMPONENT_NAMES,POLAR_COMPONENT_NAMES,ALL_TRACKED_COMPONENT_NAMESdir/→dir/index.js(rollup's ESM build skips this Node.js resolution behavior)scripts/treeshaking.test.ts— vitest test suite (runs inbuild-outputproject)Treemap,SunburstChart, andSankeyes6/build is absentscripts/treeshaking-known-issues/— per-component files for known failurescomponent(the imported component name) andexpectedInBundle(all components expected to appear in the bundle, including unintentionally bundled ones)index.tscollects all known issues for use in the test suiteit.fails()to unexpectedly pass, surfacing the improvementscripts/upload-bundle-analysis.js— extendedTreemap,SunburstChart, andSankeyrecharts/bundle-cjs(existing)recharts/bundle-es6(existing)recharts/bundle-umd(existing)recharts/bundle-treeshaking-cartesian(new)recharts/bundle-treeshaking-polar(new)recharts/bundle-treeshaking-treemap(new)recharts/bundle-treeshaking-sunburst(new)recharts/bundle-treeshaking-sankey(new)Other
rollupadded as an explicitdevDependency(was only transitive)eslint.config.mjs:scripts/*.tsadded to the scripts override (relaxesno-console/no-require-imports)Related Issue
Motivation and Context
Without this tooling there is no automated signal when a code change worsens tree-shaking (e.g. a shared utility import that silently pulls in an unrelated chart). This makes it impossible to track whether recharts is improving or regressing on bundle efficiency for end-users who only use a subset of the library.
How Has This Been Tested?
npm run test-build-output— all 40 tests pass.34 pre-existing tree-shaking problems surfaced and documented with
it.fails()— all tracked components currently have at least one known issue. Each known failure has its own file inscripts/treeshaking-known-issues/.it.fails()means CI stays green while the issue is documented. When a problem is fixed the corresponding file must be removed, confirming the fix.Screenshots (if appropriate):
Types of changes
Checklist:
Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.
Summary by CodeRabbit
Chores
Tests
Documentation