diff --git a/rfc-160-using-es6-modules-in-govuk_publishing_components.md b/rfc-160-using-es6-modules-in-govuk_publishing_components.md new file mode 100644 index 00000000..c7ff6aaa --- /dev/null +++ b/rfc-160-using-es6-modules-in-govuk_publishing_components.md @@ -0,0 +1,428 @@ +# Use ES6 Modules in govuk_publishing_components + +## Summary + +The approach to authoring JavaScript in [govuk_publishing_components](https://github.com/alphagov/govuk_publishing_components) should change from adopting the [ES5 standard](https://www.w3schools.com/js/js_es5.asp) to instead utilising the modular JavaScript introduced with [ES6](https://www.w3schools.com/js/js_es6.asp). This would mean govuk_publishing_components could use contemporary JavaScript, now supported for several years by the most popular browsers. Eventually, we should move the JavaScript over to a separate npm module. As intermediary step, we will integrate the bundler [Rollup](https://rollupjs.org/) using [jsbundling-rails](https://github.com/rails/jsbundling-rails). This would allow us to deliver separate Javascript files to our frontend applications, enabling [individual component javascript loading](https://github.com/alphagov/govuk-rfcs/blob/main/rfc-149-switch-to-per-page-asset-loading.md). Individual component javascript loading will allows to check browser support of different files as well as provide performance benefits to all of GOV.UK. In addition we can use Rollup to transpile ES6 Javascript to ES5 which could be used to enable analytics features to continue working with Internet Explorer, which has limited support for ES6. + +This RFC builds upon ideas from: +- [RFC #149: Serve component CSS and Javascript as individual files](https://github.com/alphagov/govuk-rfcs/blob/main/rfc-149-switch-to-per-page-asset-loading.md) +- [This gem is coupled to sprockets/asset pipeline](https://github.com/alphagov/govuk_publishing_components/issues/505) + +(Thanks to Kevin for providing lots of insight with Rails, govuk_publishing_components and being very helpful throughout the research process!!) + +## Problem + +### The future of IE11 support on GOV.UK + +To support Internet Explorer 11 (IE11) all Javascript in govuk_publishing_components is written in the ES5 standard. This is because IE11 does not support ES6. However lately in the GOV.UK Frontend Community there have been calls to drop JS support for IE11. As of June 2022, [Microsoft no longer supports IE11](https://learn.microsoft.com/en-us/lifecycle/products/internet-explorer-11). From 24th of April to the 21st of May, the number of users that accessed GOV.UK using IE11 was 26,205. This is 0.067% of all users (38,821,598) who accessed GOV.UK during that time. The concensus in the community is that we should eventually drop support for IE11 but the timeline for this has yet to be established. Part of the complexity is that we would still want analytics to work for the not insigificant number of IE11 users. + +### The next release of govuk-frontend + +The next release of [govuk-frontend](https://github.com/alphagov/govuk-frontend) (which is used by components in govuk_publishing_components) is going to drop support for ES5. Instead it will only be available in ES6 and will target browsers that support `type="module"` script tags. This means to recieve future updates from govuk-frontend, govuk_publishing_components will need to support ES6. In addition, this change in govuk-frontend means that the components in govuk_publishing_components that depend on govuk-frontend will also now target browsers that support `type="module"`. This means that components will no longer run on older browsers (i.e. IE11) but we would like to still recieve analytics for browsers that don't support `type="module"`. This means we need to be able to provide Javascript to applications in a way that allows browser support checking for different Javascript files. + +### Inability to load separate Javascript files + +In each application on GOV.UK, Javascript is all imported in a single application.js file. This includes all the Javascript for every component used in an application. This application.js file is used on every page of the application, even if the page being visited uses a subset of all the components in an application. [RFC #149](https://github.com/alphagov/govuk-rfcs/blob/main/rfc-149-switch-to-per-page-asset-loading.md) proposed serving assets as individual files and while we have implemented serving CSS separately we are yet to implement serving JS separately. + +### AssetPipeline is constrictive + +Currently frontend rendering applications on GOV.UK install the gem as a dependency. In order to use the javascript in govuk_publishing_components, you have to use sprockets and the asset pipeline. It is not compatible with alternative methods of delivery. It means that the Javascript from govuk_publishing_components cannot be imported into other Javascript files in an application for extension. + +### The future of AssetPipeline and Sprockets + +The future of Javascript delivery in Rails seems to be one without AssetPipeline or Sprockets. In [this blogpost from 2021](https://world.hey.com/dhh/rails-7-will-have-three-great-answers-to-javascript-in-2021-8d68191b), the founder of Rails states that from Rails 7 there are 3 intended methods to include Javascript in your application (jsbundling-rails, import maps or moving rendering out of your Rails application entirely). None of these methods utilize AssetPipeline or Sprockets which suggests that while they are being used currently, there will come a time when they stop being maintained and we will have to move away from them. + +## Proposal + +There's one significant change we should make to how we deliver Javascript in govuk_publishing_components but we can make an intermediary change which will put govuk_publishing_components in a better place for the significant change and mean that govuk_publishing_components will be able to use the ES6 modules from govuk-frontend: + +- Intermediary change: use jsbundling-rails to integrate rollup in govuk_publishing_components and start delivering individual javascript files +- Significant change: move all Javascript to a separate npm module + +### Intermediary change: use jsbundling-rails to integrate rollup in govuk_publishing_components and start delivering individual javascript files + +Of the [methods proposed in the blogpost by the Rails founder](https://world.hey.com/dhh/rails-7-will-have-three-great-answers-to-javascript-in-2021-8d68191b), the one that makes the most sense for an intermediary change would be using the [jsbundling-rails gem](https://github.com/rails/jsbundling-rails) to integrate a bundler into govuk_publishing_components. The bundler would be able to transpile ES6 into ES5 and output individual files. This means govuk_publishing_components could use ES6, support the latest version of govuk-frontend, enable the invidual loading of Javascript in frontend applications and ensure analytics still works on older browsers. + +[Rollup](https://rollupjs.org/) is a Javascript bundler that is used in govuk-frontend. It supports plugins and accepts a configuration file with handlers for specific events (such as when the transformed Javascript is being written to a file). Using js-bundling rails I was able to integrate Rollup into govuk_publishing_components. + +#### Implementation (of the intermediary change) + +I implemented a [prototype of govuk_publishing_components using jsbundling-rails](https://github.com/alphagov/govuk_publishing_components/compare/main...try-to-use-rollup) so I could see how it would integrate into our frontend applications. Additionally it meant that I could run performance testing to see how loading individual javascript files compared to our current delivery mechanism. What follows is my method of implementation. + +##### 1. Install jsbundling-rails + +The [jsbundling-rails gem](https://github.com/rails/jsbundling-rails) has to be added as dependency to govuk_publishing_components. It is a simple gem that adds rake tasks (for building and clobbering) and installs a specific bundler (esbuild, Rollup or Webpack). I determined that Rollup was best for the task at hand. This was based on my past experience with Webpack ([powerful but complex and difficult to maintain and integrate](https://blixtdev.com/you-dont-need-webpack-3-better-alternatives-for-building-your-javascript#what%E2%80%99s-wrong-with-webpack)) and trying out [esbuild](https://github.com/evanw/esbuild) (fast and lightweight but [limited options to customise ES5 output](https://github.com/evanw/esbuild/issues/297)). Rollup is still lightweight (in comparison to Webpack) but easily configurable due to its library of plugins and event handlers during build. It is also already used in govuk-frontend for building Javascript. + +##### 2. Converting the Javascript to ES6 + +In govuk_publishing_components we use a ['module'-like implementation](https://docs.publishing.service.gov.uk/repos/govuk_publishing_components/javascript-modules.html) in which the Javascript of each component is self-contained in a function. This function is then assigned to the GOVUK window object and then the components are initialised using an "initAll" method which iterates through the components and runs each initialisation method. + +As this 'module'-like pattern has already been implemented, the actual conversion to ES6 is straight forward. It would consist of wrapping the existing code in a `Class` and removing the `prototype` syntax. + +``` +(function (Modules) { + function ModuleName ($module) { + this.$module = $module; + } + + ModuleName.prototype.utilityFunction = function () {} + + ModuleName.prototype.init = function () {} +})(window.GOVUK.Modules) +``` +to + +``` +Class ModuleName { + constructor($module) { + this.$module = $module; + } + + utilityFunction() {} + + init() { + this.utilityFunction(); + // rest of the init code... + } +} + +export default ModuleName; +``` + +In govuk_publishing_components, Javascript is used to add extra functionality (usually analytics) to components implemented in govuk-frontend. Currently the approach is to first initialise the govuk-frontend Javascript module of a component and then initialise the govuk_publishing_components Javascript module to add - for instance - extra analytics when a user clicks specific areas of the component. + +``` +window.GOVUK.Component.init(exampleElement); +window.GOVUK.GemComponent.init(exampleElement); +``` + +With jsbundling we can simply this initialisation by using composition: + +``` +import { Component } from 'govuk-frontend'; + +Class GemComponent { + constructor($module) { + this.govukFrontendComponent = new Component($module); // assign a variable to an instance of the component + this.classNameOfElement = "class-name-of-element"; + // specific constructor code for the gem component + } + + init() { + this.govukFrontendComponent.init(); // initialise the instance of the component + // specific init code for the gem component + } +} + +export default GemComponent; +``` + +``` +window.GOVUK.GemComponent.init(exampleElement); +``` + +[I also considered using inheritance](#inheritance-vs-composition). + +##### 3. Creating a Rollup configuration file + +We now need to create a configuration file for Rollup: + +``` +const defaultConfig = input => ({ + input, + output: { + file: `app/assets/javascripts/govuk_publishing_components/components/${input.match(/(\w|-)+(?=\.js)/g)[0]}.js`, + format: "umd", + name: `GOVUK.Modules.${NameOfTheComponent}`, + sourcemap: true, + }, + plugins: [ + resolve(), + buble(), + ], +}); + +export default glob("app/assets/javascripts/govuk_publishing_components/components/src/*.js") + .then(files => files.map(file => defaultConfig(file))) +``` + +With Rollup we can utilise a transpiler ([I used Bublé](https://buble.surge.sh/)) to convert our ES6 code into ES5. This means we can still have analytics run on IE11 and use [Uglify to minify our Javascript](https://github.com/privatenumber/minification-benchmarks#-results). Setting the output of the Javascript to [UMD](https://github.com/umdjs/umd/blob/master/templates/commonjsStrict.js) with a name of 'GOVUK.Modules.ComponentName' results in the component being [assigned to the window object that components are currently assigned to](https://docs.publishing.service.gov.uk/repos/govuk_publishing_components/javascript-modules.html#module-structure). [I also considered an alternative method in the appendix](#alternate-method-of-initialising-components). + +The config uses the [glob package](https://github.com/isaacs/node-glob) to create an array of the paths of all the Javascript files we want to transpile. It then loops through the array for each Javascript file, transpiles the Javascript to ES5 using Buble and writes out the transpiled Javascript to individual files. + +If we were to move the Javascript to be transpiled to `app/assets/javascripts/govuk_publishing_components/components/src/` and build the output in `app/assets/javascripts/govuk_publishing_components/components/` (as shown in the config above) it would mean we could make this change to govuk_publishing_components without having to update paths in frontend rendering applications. This is a [similar folder structure to what is used in govuk-frontend](https://github.com/alphagov/govuk-frontend/tree/main/packages/govuk-frontend/src). If analytics were also to be built by the bundler then a similar change to folder structure should be made (and those paths could be added to a [globStream](https://github.com/isaacs/node-glob#globstreampattern-string--string-options-globoptions--minipassstring--path)) although I would argue that [analytics does not necessarily need to be updated](#loading-analytics-code-individually). This script would be run on gem release so that an application could then include the generated Javascript files (similar to how node_modules are included). + +##### 4. Update frontend applications to individually load Javascript + +In order to load the JS for each component separately, we need to add the path of each component we are going to use to the manifest of a frontend application. Then we can add a script tag to the pages were the components are going to be used: + +``` +//= link govuk_publishing_components/path/to/component-name +``` + +then + +``` + <%= javascript_include_tag "govuk_publishing_components/path/to/component-name" %> +``` + +If we were to follow this implementation method then we could augment AssetHelper instead of manually adding script tags to partials and paths to the manifest. AssetHelper already has this functionality for stylesheets, where the `add_component_stylesheet` function can update the manifest and add stylesheet link tags. We could add a new function for adding javascript to a partial: + +`add_component_javascript("component-name")` + +which would add + +`