govuk-frontend: Cannot import standard ES6 modules from npm

What

At the moment if you try to import using our documentation using a standard ES6 bundler such as Rollup it will fail.

This is because we ship our bundles as UMD, which is great for browser and CommonJs support but not ES6 Modules (better for build pipelines).

This means our current guidance around rollup is misleading and only works when compiling from source.

We could consider adding more real world examples that import the package directly to ensure what we recommend is tested.

I’m not sure if we should be publishing these as .mjs files, which seems to be what Node.js is going towards.

Why

Some users rely on getting modules directly from /src, which we don’t recommend and don’t want to encourage. Many of these users say they’re running outdated versions. We should make it easier for them to import as standard ES6 modules.

Done when

  • Test including modules directly from /src
  • Research into type: module in the package.json
  • Test in some dummy apps/services
  • Automated testing
  • Implement decided approach
  • Do a pre-release and share with the community to make sure this solves the problems (try and reach out to those we know are going directly to /src)
  • Documentation
  • Raised a PR for using this in the Design System

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 3
  • Comments: 26 (24 by maintainers)

Commits related to this issue

Most upvoted comments

In our project we use very few govuk-frontend js modules: Button, Radios, Checkboxes, ErrorSummary, that’s about it. So to save on the bundle size I made a custom all.js file that only imports the components I need. The other reason we do this is that we have some custom components which use the same polyfills (and one that reuses Button) which can also be added to the same all.js file. The tree shaking will avoid bundling the same polyfills or components twice, resulting in a single small js bundle.

Unforuntatley the only way to achieve this is to add this Github repo as a npm depency, as the unbundled source is not included in the npm package and using the individually bundled components does not take advantage of tree shaking, bloating out the total bundle size.

This also presents us with some problems:

  • The version of the Github repo is not always accurately reported by the ‘out of date’ check in our CI pipeline so it’s always yellow with a warning
  • The Github repo package.json requires specifically node version 12 preventing us from updating to current LTS version 14 (the npm install fails)
  • govuk-frontend is already a subdepency of a DWP package meaing we’re effectively installing everything twice, with the nunjucks macros being the loaded from the subdepency but the client side js bundled from the source repo. Keeping these versions the same is a faff
  • This is clearly a messy hack

So in an ideal world the unbundled component js would be included in the govuk-frontend npm package in a way in which it could be imported as an ES6 module. This could also allow serving the ES6 modules directly over HTTP2 in the future, falling back to the bundle.

I’d be happy to help on the implementation of this if an approach could be agreed.

Thanks!

@domoscargin and I have been testing this approach with the following bundlers: Webpack; Parcel; Rollup; esbuild.

Setting sideEffects

The biggest change we’ve made since my last comment is setting sideEffects in the govuk-frontend package/package.json. I originally set this to false when first prototyping approaches to this card, for unknown reasons (most likely copied from an example without giving it much thought).

However, @domoscargin noticed when he was testing that polyfills were being ignored and weren’t being bundled. Having read https://sgom.es/posts/2020-06-15-everything-you-never-wanted-to-know-about-side-effects/, I came to the conclusion that we needed to set sideEffects to true because of the way we import polyfills - we import them and rely on them being there without having to ‘call’ them, which means our code has ‘side effects’. A bundler might otherwise exclude these because it thinks the imported code isn’t being referenced and therefore isn’t needed. I’ve gone one step further and actually set sideEffects to "govuk-esm/vendor/**” because I think these are the only JavaScript files that we use in this way.

I’ve updated the pre-release with this change and tested with all the bundlers listed above. All output JavaScript now seems to correctly include polyfills and only includes them once even if they’re imported by multiple components (tree-shaking).

common.mjs

As discussed on Slack, we’ve decided not to export the methods included in common.mjs in our all.mjs file. These are internal methods which were never intended to be made ‘public’, despite people importing them at the moment in the way we currently ship our JS as UMD probably because it’s convenient and there’s nothing to stop them from doing that. As this is a new feature, we’re not going to include the functions in that file (nodeListForEach and generateUniqueID) as an export in our ES modules.

Stats

Original UMD bundle in package/govuk: 96.01 kB Rollup: 55.98 kB Webpack: 76.05 kB Parcel: 36.14 kB (minified) esbuild: 42.76 kB

Note: variations in file size should be down to different config setups, there might be config tweaks/changes our users can make to bring that file size down even further.

Draft govuk-frontend PR

Updated pre-release branch

Test app for Webpack; Rollup; Parcel; esbuild Note: the commit history hasn’t been updated with the latest pre-release yet. Run npm install --save "alphagov/govuk-frontend#202c4c6f82b13dd6a06183380096c0e78933d5e0 before testing with that app.

Design System branch

Prototype Kit branch (to test backwards compatibility)

Hey! Thanks so much for this work, once released this should allow me install the govuk-frontend module properly again rather than include a tagged point in the repo to get the original source. I’m gonna make a branch on our frontend project to test it out and see if these changes will fit neatly into my build. Thanks

@36degrees Good shout on nodeListForEach - I’d noted down our decision, but forgotten to update that branch. Should be updated now in this commit.

For the other point, importing from govuk-frontend is how I’d expect someone to import. We’re not defining anything new called ‘govuk-frontend-esm’. Instead, the fact that we’re using an import statement should be enough for the govuk-frontend package.json to “point us” to the govuk-esm directory because we’ve made these changes to the package/package.json file.

Does that make sense?

ok thanks @36degrees I’ll try that first and see what happens. (Probably this weekend)

ESM Write-Up

I picked this work up last week and carried on where @domoscargin left off - looking into the 3 options detailed in the comment above in more detail to see which is most feasible.

Option 1: using rollup

I’ve almost immediately discarded this option because of the downsides written up by @domoscargin.

Option 2: copying src files into /package alongside UMD

It seems difficult to get this working properly (i.e: separating out .mjs and .js and making sure you’re only importing either ESM or UMD). I changed the package/package.json to give 2 entry points for UMD or ESM, e.g:

  "name": "govuk-frontend",
  "description": "GOV.UK Frontend contains the code you need to start building a user interface for government platforms and services.",
  "version": "4.0.1",
  "main": "govuk/all.js",
  "module": "govuk/all.mjs",
   …

However, even though the initial entry point works correctly, the JavaScript that all.mjs imports seems to still target .js (UMD) files and there’s no way of telling it to import .mjs instead.

I didn’t dig into setting ”type”: “module” in the package/package.json which might change that default behaviour - I’m not sure. That might become a breaking change, where ESM becomes the default, which I think we want to avoid.

Option 3: copying src files into a new folder for ESM

This is the option which I’ve tested the most as it seems the most feasible option.

➡️ See WIP govuk-frontend branch

I’ve added a new copysrc task which copies the JavaScript files in src, renames them to have the .mjs file extension and saves them into a new package/govuk-esm folder.

Note: This only copies JavaScript into the package/govuk-esm folder. The package/govuk-esm folder is not a complete copy of what’s currently in package/govuk. I think our JavaScript is separate enough from our sass, nunjucks etc that we can pull it out into a separate folder like this, but happy to be corrected on that.

➡️ See WIP govuk-design-system branch

I tested this by editing the govuk-frontend.js in the Design System repo and replacing it with:

import { initAll } from ‘govuk-frontend’
initAll()

The only thing which needed adjusting to get this to work were the imports of our common helpers.

Next steps

Things I was hoping to do, but unfortunately didn’t get enough time before annual leave:

  • Test this WIP solution with more services/apps/examples other than just the design system
  • Figure out the interaction with bundlers and if this works
  • Figure out what additional documentation/changes to existing documentation we might need
  • Share with other developers and get opinions on if this is the right direction to be heading in

direction most packages are moving to is to specify type: module in package.json Sindre Sorhus’s FAQ. for more details alternatively ship additional govuk-frontend-esm npm package

Yeah I believe that’s the only way currently to achieve what you’d get for free with tree shaking.