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
Replace Browserify with Rollup #3810
Conversation
Prod bundle size before:
Where the sidebar app consists of the sidebar, sentry, katex and showdown bundles (340 + 113 + 260 + 76 = 789k) Prod bundle size after:
Where the sidebar app is contained in only one bundle. |
I've seen this error sometimes happen on a clean build:
This is harmless because the task gets re-run automatically after the file is generated. But we should still fix it. |
One small quality of life improvement in this PR is that the
It would still be good to further prune the output in future and reduce the amount of noise. |
Codecov Report
@@ Coverage Diff @@
## master #3810 +/- ##
=======================================
Coverage 99.02% 99.02%
=======================================
Files 211 211
Lines 7797 7797
Branches 1757 1757
=======================================
Hits 7721 7721
Misses 76 76
Continue to review full report at Codecov.
|
@@ -215,23 +157,8 @@ gulp.task( | |||
}) | |||
); | |||
|
|||
const imageFiles = 'src/images/**/*'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The build-images
task has been removed because all of the images are SVGs embedded in the JS bundles as strings. This was already the case with the Browserify build. I'm just removing obsolete logic.
// should always have been generated first. | ||
if (usingDevServer && !existsSync(bootBundle)) { | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might have been technically possible for this issue to have occurred previously with Browserify, but I never observed it in practice when running make clean; make dev
. I did however see it sometimes with Rollup.
"prettier": { | ||
"arrowParens": "avoid", | ||
"singleQuote": true | ||
}, | ||
"browser": { | ||
"fetch-mock": "./node_modules/fetch-mock/cjs/client.js" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need this any more because Rollup will use the ES module version of fetch-mock
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
I did some experiments with adding caching at various points in the build process. What I learned is that there are worthwhile wins to be had from minimizing or caching:
Of the above, the Babel transform looks like it will be simplest to do. Not sure about the others yet. Also we could look at what we did with Browserify where large vendor dependencies were bundled separately in development. In production building vendor dependenceis + app code together maximizes the effectiveness of tree-shaking. I will look at these after the initial conversion lands. |
This is almost ready for review / testing now. Remaining things to do:
|
I haven't yet added the polyfill bundles to the Rollup config. It turns out that we don't currently need them, as none of our supported browsers need any polyfills. The most recent thing we use is |
@robertknight I've read the description and thread here but have not yet reviewed code or tested (will do shortly). One thing that jumped out at me:
A related comment:
Our current failure mode in unsupported browsers could be described as "soft." IIRC, we actually render a nice message telling the user that their browser is out of date and provide a link to help docs. What's the failure mode going forward for unsupported browsers? |
That's the same as before. The only code that runs in unsupported browsers is the boot script, and that's compiled to a classic (non ES-module) script. The current behavior in the client is just to log a console warning. The LMS app does display a UI message and we should be able to keep that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this stage, mostly lots of questions! I tried to do some background reading where I could.
Main observations:
- In general, rollup config is straightforward to read and understand, and their docs seem solid. I appreciate that they manage their own core plugins, as that makes documentation consistent.
- My biggest challenge with this set of changes is the test-build configuration. I'm having trouble following it (more specific questions inline).
@@ -17,7 +17,6 @@ | |||
] | |||
], | |||
"plugins": ["inject-args"], | |||
"ignore": ["**/vendor/*"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you help me understand this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We used to have some third-party dependencies in a local vendor
directory in the source tree. This no longer exists.
// Use `preact/compat/jsx-dev-runtime` which is an alias for `preact/jsx-runtime`. | ||
// See https://github.com/preactjs/preact/issues/2974. | ||
"importSource": "preact/compat" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading the linked bug description, sounds like this workaround was because of a shortcoming in Browserify so it makes sense we wouldn't need it anymore...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct, it is not needed anymore.
@@ -16,5 +16,12 @@ | |||
|
|||
// Replaced by TypeScript's static checking. | |||
"react/prop-types": "off" | |||
} | |||
}, | |||
"overrides": [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL: ESLINT overrides. For when you need to provide different (potentially conflicting) settings to a particular glob (subset) of files.
Is this set of overrides potentially common/reusable for modules that run in a Node.js context vs. the browser? Or is there something particularly special about rollup
configs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rollup configs are written as ES modules, but currently ESLint expects is configured to expect files outside of the src/
dir to be non-ES modules (eg. CommonJS). If we make everything an ES module we could remove this.
const gulp = require('gulp'); | ||
const replace = require('gulp-replace'); | ||
const rename = require('gulp-rename'); | ||
const rollup = require('rollup'); | ||
const loadConfigFile = require('rollup/dist/loadConfigFile'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh. Is this a typical pattern with rollup/part of the documented API?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rollup-tests.config.js
Outdated
string({ | ||
include: '**/*.{html,svg}', | ||
}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blurb from this plugin is that it "converts text files to modules." Can you help me grok what it does for us in terms of building bundles for test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This enables import someIcon from 'path/to/icon.svg'
statements to work. Same for .html
files.
// Eliminate debug-only imports. | ||
prodPlugins.push( | ||
virtual({ | ||
'preact/debug': '', | ||
}) | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would be become aware of (additional) debug-only imports in the future?
updated: I see a helpful comment in annotator/index
related to this.
nodeResolve(), | ||
commonjs(), | ||
string({ | ||
include: '**/*.svg', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No .html
here vs. in test config...curious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only some HTML fixtures in the tests need to import .html
files as strings.
if (process.env.NODE_ENV !== 'production') { | ||
require('preact/debug'); | ||
} | ||
// Enable debug checks for Preact. Removed in prod builds by Rollup config. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Helpful comment for future, thanks!
<svg className={classnames('Menu__arrow', className)} width={15} height={8}> | ||
<path d="M0 8 L7 0 L15 8" stroke="currentColor" strokeWidth="2" /> | ||
</svg> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Orthogonal, but is there a counter-argument to eventually replacing this SVG with an SVGIcon component?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit simpler and faster, but not so easy to update as an SVGIcon
. We can change if needed in future.
gulpfile.js
Outdated
); | ||
// | ||
// Some (eg. a11y) tests rely on CSS bundles. We assume that JS will always take | ||
// londer to build than CSS, so build in parallel. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// londer to build than CSS, so build in parallel. | |
// longer to build than CSS, so build in parallel. |
Replace Browserify with Rollup as a module bundler. This brings several advantages: - It is a more modern bundler that has better support in the ecosystem, more active maintenence, fewer dependencies and is easier to write plugins for. The core team also maintain most of the plugins that we need. - It has native support for modern platform features like ES modules, including dynamic import for lazy-loading of functionality. - It generates smaller bundles due to its ability to do tree shaking and scope hoisting. The new bundles are generated as ES modules rather than UMD / IIFE bundles. Our target browsers now all support ES modules and this will enable us to use native static/dynamic imports for code splitting or lazy-loading of functionality in future. The initial Rollup build generates one bundle per application (boot script, annotator, sidebar). This simplifies the build process and minimizes production bundle size, at the cost of making incremental updates during development slightly slower. Build performance is currently similar to or a little slower than Browserify. We can optimize this by adding caching of transforms (mainly Babel) and pre-building of a vendor bundle containing large dependencies. As part of changing the bundler this also changes how the code is packaged for tests. We now do the bundling ourselves rather than using a Karma integration. This makes it much easier to inspect the processed code and fix problems. It also makes it easier to optimize the bundle building in future.
Fix an occasional error from Gulp about a missing `boot.bundle.js` file when running `make dev` for the first time after `make clean`. This can happen if `updateManifest` runs before Rollup has generated `boot.bundle.js`. The error can be safely ignored as `updateManifest` will be re-run after `boot.bundle.js` is generated.
Make conditional exclusion of modules work the same way in the app bundle as in the tests bundle.
Since the exclusion is no longer triggered by logic in the source code, add a note about where this happens.
This prevents a warning from Rollup about use of `this` outside a class or (non-arrow) function in development builds, due to the way Babel compiles JSX expressions [1]. Moving top-level code into a function is also useful in case we ever want to write tests for it. [1] babel/babel#9149
Due to pre-existing `<script>` tags on third-party websites that load the client's boot script, via https://hypothes.is/embed.js, we need to keep generating it as a classic rather than ES module script for now.
Add comments to clarify a few things about the way the tests are built and run.
I have rebased this on master and added a commit with some comments to clarify aspects of how the test bundle is built and run. |
`sinon` is exposed as a global in the test environment so it doesn't need to be imported. Additionally some of the dependencies of Sinon (samsam, nise) cause warnings about circular dependencies and use of `eval` when bundled by Rollup.
There is a test that is running slowly on this branch:
It turns out to be caused by attempting to return an Update: The slow execution of this test is something to do with the Looking into this further, I found that attempting to send an Error via a MessagePort (using Since passing an |
This fixes a slow test on this branch, which appears to be caused by an interaction between using `postMessage` to serialize native Error objects and the `source-map-support` Karma plugin that we use. Sending native Errors via `PortRPC` currently doesn't work in all browsers so I expect we'll want to implement our own serialization of errors in `PortRPC` to work around that. That can also work around the `source-map-support`/`postMessage` issue in Chrome in future. To move the Browserify => Rollup conversion forwards, this commit just changes the test to use an Error-like rather than actual Error object.
The CommonJS plugin is the second most expensive transform after Babel. Since all of our own code uses ES modules, limit it to npm dependencies to speed up the build. In tests the Babel transform has also been moved last so that additional code it generates is not unnecessarily processed by subsequent plugins. Combined these changes speed up a build of the full test bundle by ~4s.
Some performance metrics for building the test bundle:
This is not especially surprising since 90% of the test bundle is dependencies. In modules that have code coverage enabled, the inserted code increases the total volume of code a lot and that has consequences for subsequent steps. |
This should make it easier to speed up test runs in future by pre-building npm dependencies into a separate bundle which the main test bundle can import from.
Change the LMS frontend to use Rollup for bundling JS. This change was done for the same reasons and using a similar approach to the corresponding change in the Hypothesis client. See hypothesis/client#3810.
One remaining warning I am aware of is this message when running
A fix for this issue was proposed upstream but later reverted. I tried to restart the discussion with a proposed fix at rollup/rollup#3896 (comment). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're ready to move forward on this! One thing that would be helpful is if you could summarize all the need-to-know stuff for devs (e.g. the slow test, the remaining warning) so we have a quick reference of what to expect.
Sure. The slow test was fixed, so I think the most relevant remaining things are:
|
Change the LMS frontend to use Rollup for bundling JS. This change was done for the same reasons and using a similar approach to the corresponding change in the Hypothesis client. See hypothesis/client#3810.
Change the LMS frontend to use Rollup for bundling JS. This change was done for the same reasons and using a similar approach to the corresponding change in the Hypothesis client. See hypothesis/client#3810.
Change the LMS frontend to use Rollup for bundling JS. This change was done for the same reasons and using a similar approach to the corresponding change in the Hypothesis client. See hypothesis/client#3810.
Replace Browserify with Rollup as a module bundler. This brings several
advantages:
It is a more modern bundler that has better support in the ecosystem,
more active maintenence, fewer dependencies and is easier to write plugins for.
The core team also maintain most of the plugins that we need.
It has native support for modern platform features like ES modules,
including dynamic import for lazy-loading of functionality.
It generates smaller bundles due to its ability to do tree shaking and scope hoisting.
The new bundles are generated as ES modules rather than UMD / IIFE bundles.
Our target browsers now all support ES modules and this will enable us to use
native static/dynamic imports for code splitting or lazy-loading of
functionality in future.
The initial Rollup build generates one bundle per application (boot script,
annotator, sidebar). This simplifies the build process and minimizes production
bundle size, at the cost of making incremental updates during development
slightly slower.
Build performance is currently similar to or a little slower than Browserify.
We can optimize this by adding caching of transforms (mainly Babel) and
pre-building of a vendor bundle containing large dependencies.
As part of changing the bundler this also changes how the code is packaged
for tests. We now do the bundling ourselves rather than using a Karma
integration. This makes it much easier to inspect the processed code and fix
problems. It also makes it easier to optimize the bundle building in future.
Summary of changes:
rollup.config.js
config file that specifies how to build the app bundle, androllup-tests.config.js
that specifies how to build the test bundle.gulpfile.js
to use Rollup's JS API to build bundles instead of Browserifykarma.config.js
. The equivalent functionality is provided bybuildAndRunTests
in gulpfile.js androllup-tests.config.js
.karma-source-map-support
Karma plugin to enable error messages to show original source locations for any errors.<script type="module" ...>
) rather than classic scriptsgulp/scripts/*
that were used to generate the Browserify bundle