Skip to content
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

Fix tree-shaking when importing govuk-frontend #4961

Merged
merged 10 commits into from
May 3, 2024
Merged

Conversation

romaricpascal
Copy link
Member

@romaricpascal romaricpascal commented Apr 30, 2024

This PR fixes the ability for our package to be tree-shaken by bundlers without extra configuration on the users part, allowing code exported by govuk-frontend but not imported by the user's code to be appropriately dropped from their compiled file.

The fix

The fix consists of adding "sideEffects": true to our package.json file. This announces to bundlers that the files of our project are free of side effects (code that modifies JavaScript globals or the DOM) on import. In turn, this lets them know that it's safe to remove code that wouldn't be imported by the user's project.

While not a field officially documented by npm, this is what Webpack and Rollup look at when deciding whether they can drop code from within node_modules.

Tests

Unit tests

The PR adds a package.json.unit.test.mjs to verify the presence of that field.

Integration tests

This work highlighted the need for us to at least have a little check that our library works OK with major bundlers. Without setting up a complex configuration, this PR adds integration tests with Rollup, Webpack and Vite to verify that tree-shaking works as expected with them.

This required the creation of a new npm workspace in the project: @govuk-frontend/bundler-integrations (in .github/workflows/bundler-integrations as this check should primarily happen on CI). This use of a workspace is necessary so that govuk-frontend is consumed as an external module by the bundlers. If we were to just run the bundlers in the govuk-frontend workspace, bundlers would have no trouble tree-shaking unused code as it would all be code from the same project.

That workspace contains light configurations for each of the bundlers (mostly to ensure either tree-shaking is enabled when it needs explicit enabling like for Webpack and/or that minifying is disabled so we can look up the names of our components in the output and not mangled variables).

This workspace is then used on CI by the bundlers-integration.yml workflow, automatically called as a last step by our test.yml (so we benefit from the caching of the dependencies and build).

Future considerations

Freedom from side effect

The package is currently free from side effects thanks to our code only affecting the global scope from inside functions or classes. If functions are not called or classes not instanciated, nothing happens from only importing our files. This may change if we start including polyfills. At that point, we'll need to be cautious of either:

  • remaining side-effect free by using 'pure' polyfills, themselves free of side-effects (like 'core-js-pure')
  • adjusting our 'sideEffects' field accordingly (you can list files that do have side-effects).

Tree-shaking in file stats

While investigating this issue, we noticed that the scripts computing the stats imports from each component's file rather than from govuk-frontend. We may want to expand the stats to compare the file sizes when importing a component from its file and from govuk-frontend (which may highlight that some unnecessary code doesn't get tree-shaken).


Closes #4957

@romaricpascal romaricpascal changed the title Update config of webpack example for investigation Fix tree-shaking when importing govuk-frontend Apr 30, 2024
Copy link

github-actions bot commented Apr 30, 2024

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 113.24 KiB
dist/govuk-frontend-development.min.js 42.21 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 87.21 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 81.95 KiB
packages/govuk-frontend/dist/govuk/all.mjs 4.17 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs 359 B
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 113.23 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 42.2 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.55 KiB

Modules

File Size (bundled) Size (minified)
all.mjs 77.67 KiB 40.19 KiB
accordion.mjs 22.71 KiB 12.85 KiB
button.mjs 5.98 KiB 2.69 KiB
character-count.mjs 22.4 KiB 9.92 KiB
checkboxes.mjs 5.83 KiB 2.83 KiB
error-summary.mjs 7.89 KiB 3.46 KiB
exit-this-page.mjs 17.1 KiB 9.26 KiB
header.mjs 4.46 KiB 2.6 KiB
notification-banner.mjs 6.26 KiB 2.62 KiB
password-input.mjs 15.15 KiB 7.25 KiB
radios.mjs 4.83 KiB 2.38 KiB
skip-link.mjs 4.39 KiB 2.18 KiB
tabs.mjs 10.13 KiB 6.11 KiB

View stats and visualisations on the review app


Action run for 9ed92a5

Any side effect affecting the global scope is encapsulated either in the components classes
or initAll. By marking our package as free of side effects, WebPack drops any unused export
appropriately
Ensures the documentation for running 'npm run clean' without specifying a workspace remains true
Called `build:all`, not `build` so it doesn't get run by `npm run build --workspaces` on CI (in `.github/workflows/actions/build/action.yml`). If called `build` it would run before `govuk-frontend` gets build and the build would fail.
@@ -0,0 +1,6 @@
import resolve from '@rollup/plugin-node-resolve'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment Rollup needs a plugin to resolve package from node_modules.

Comment on lines +5 to +6
outDir: 'dist/vite',
assetsDir: '.',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment By default, Vite would output in dist and the compiled JavaScript in dist/assets. This makes it match the dist/<BUNDLER_NAME> convention from the other bundlers.

@@ -151,6 +151,7 @@ describe('packages/govuk-frontend/dist/', () => {
'govuk-prototype-kit.config.json',
'gulpfile.mjs',
'package.json',
'package.json.unit.test.mjs',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment Something for another PR, but there are a couple of .mjs files that end up in the final package when they shouldn't (see the couple of .test.mjs and .config.mjs files after this line). There's a *.test.* entry in .npmignore, but it doesn't seem to do much.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note We only publish a subset of what's in package/govuk-frontend to npm because we're filtering it using the files field of packages/govuk-frontend/package.json:

"files": [
"dist",
"govuk-prototype-kit.config.json",
"package.json",
"README.md"
],

Agree it's a little confusing as we have a combination of exclusions at build time, exclusions using .npmignore and a list of inclusions in that files field that all intersect with each other, but I think what we're publishing is broadly 'correct'.

@romaricpascal romaricpascal marked this pull request as ready for review April 30, 2024 17:10
@romaricpascal romaricpascal requested a review from a team as a code owner April 30, 2024 17:10
@owenatgov
Copy link
Contributor

Not one we need to figure out in this PR necessarily but I think we should have a think as a dev team about which bundler examples to include and maybe establish some criteria. The inclusion of vite makes sense due to it's skyrocketing popularity and rollup is useful because we use it on the website, but should we also include examples for esbuild and parcel (I appreciate the example for parcel would hypothetically be nothing)?

My sources for what's popular at the moment:

@owenatgov
Copy link
Contributor

I've adjusted the bundler integration tests to also account for importing via initAll. Specifically what I've done is:

  • added an initAll.mjs entry point to src
  • renamed index.mjs to default.mjs so that it's no longer the 'index' of the entry point
  • adjusted the bundler scripts and configs so that they all output both a default.js and an initAll.js in their respective dist folders
  • adjusted the test itself to also test for a successful grep of 'Accordion' in initAll.js

I've added this so that we are sure that this change, and any further changes to how we do imports, don't negatively impact users who are using initAll to import instead of importing specific components.

The leftover question for me are firstly, how comprehensive we want this testing to be? Right now both tests are just greping for 'Accordion' and reporting the success. Should we also grep for 'Button' in default? Should we grep for all components in initAll? In theory it wouldn't negatively impact the testing time too significantly but it's also more test code to maintain eg: if we release a new component we ahve to remember to add it to this.

Secondly, should we get rid of the webpack example in docs/examples/webpack? From a skim of this repo's docs, we don't reference it anywhere but we do now reference the bundler testing in our docs with this PR, which could act as a set of examples.

Copy link
Contributor

@36degrees 36degrees left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is looking great – @owenatgov thanks for picking this up and adding the tests for initAll 🙌🏻

I only have a couple of suggestions, neither of which are blocking.

.github/workflows/bundler-integrations.yml Outdated Show resolved Hide resolved
@@ -151,6 +151,7 @@ describe('packages/govuk-frontend/dist/', () => {
'govuk-prototype-kit.config.json',
'gulpfile.mjs',
'package.json',
'package.json.unit.test.mjs',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note We only publish a subset of what's in package/govuk-frontend to npm because we're filtering it using the files field of packages/govuk-frontend/package.json:

"files": [
"dist",
"govuk-prototype-kit.config.json",
"package.json",
"README.md"
],

Agree it's a little confusing as we have a combination of exclusions at build time, exclusions using .npmignore and a list of inclusions in that files field that all intersect with each other, but I think what we're publishing is broadly 'correct'.

.github/workflows/bundler-integrations/src/default.mjs Outdated Show resolved Hide resolved
This ensures that any changes we make to our importing process don't negatively impact users who're importing via `initAll`
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4961 May 3, 2024 08:31 Inactive
@owenatgov owenatgov merged commit 8729af4 into main May 3, 2024
48 checks passed
@owenatgov owenatgov deleted the fix-tree-shaking branch May 3, 2024 09:59
36degrees pushed a commit that referenced this pull request May 15, 2024
Fix tree-shaking when importing `govuk-frontend`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Fix tree-shaking when importing from 'govuk-frontend'
4 participants