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

Build: Transpile whitelisted packages in node_modules #30196

Merged
merged 11 commits into from
Mar 28, 2019

Conversation

blowery
Copy link
Contributor

@blowery blowery commented Jan 16, 2019

In the past, we've tried very hard to avoid transpiling code in node_modules. This was because most code shipped in node_modules was plain ES5 that worked in all of our target browsers, so transpiling would be extra work with no benefit. Modules were typically ES5 because they wanted to support Node 4, which was on par with IE11 for language features.

Fast forward to today, where Node 4 is no longer supported. Some modules (debug, d3-*, striptags, ...) are dropping support for Node 4 and adding ES6 features to the code they ship to NPM, like let and for of. This breaks IE11, so we have to transpile these packages to use them in IE11.

This PR proposes to transpile some things in node_modules, provided by a whitelist. This hasn't seemed to affect total compilation time very much, especially for incremental runs when the babel cache is hot. We also use a reduced babel config that only transpiles the set of plugins supported by @babel/preset-env, reducing the surface area of features we'll transpile. We're not really expecting anything outside of the node@6 feature set, but using preset-env gives us a bit of future proofing.

The whitelist is path based, so it has no notion of a package's version. It would be a nice enhancement to only transpile packages that are past a certain version. debug@4, for instance, requires transpilation, but debug@3 does not. We may have both versions in the tree as transitive dependencies.

Original PR Description follows

In the past, we've tried very hard to avoid transpiling code in node_modules. This was because most code shipped in node_modules was plain ES5 that worked in all of our target browsers, so transpiling would be extra work with no benefit. Modules were typically ES5 because they wanted to support Node 4, which was on par with IE11 for language features.

Fast forward to today, were Node 4 is no longer supported. Some modules (debug, d3-*, striptags, ...) are dropping support for Node 4 and adding ES6 features to the code they ship to NPM, like let and for of. This breaks IE11, so we have to transpile these packages to use them in IE11.

This PR proposed to transpile most things in node_modules. It provides a blacklist of modules which should be skipped, but most things will run through a basic transpilation. This hasn't seemed to affect total compilation time very much, especially for incremental runs where the babel cache is hot. We also use a reduced babel config that only transpiles the set of plugins supported by @babel/preset-env, reducing the surface area of features we'll transpile. We're not really expecting anything outside of the node@6 feature set, but using preset-env gives us a bit of future proofing.

We may run into issues transpiling some packages, especially those that ship a minified version as the primary distribution. For those cases we provide a blacklist to exclude the package from transpilation.

A couple upgrades have slipped in and IE11 is broken on some screens (thankfully not login or home) when modules like d3-array@2 are used.

@matticbot
Copy link
Contributor

@jsnajdr
Copy link
Member

jsnajdr commented Jan 17, 2019

An idea how we could auto-test for untranspiled code: parse the production JS chunks with an ES5-only parser (does one exist? can existing popular parser to be configured to be ES5-only?) and verify that the parse succeeds.

@blowery
Copy link
Contributor Author

blowery commented Jan 17, 2019

An idea how we could auto-test for untranspiled code:

I think I'd rather just transpile it all if we can and avoid trying to detect what really needs it. Transpilation is pretty fast now and babel has a quality cache.

@blowery
Copy link
Contributor Author

blowery commented Jan 17, 2019

Looking at how CRA does it, they have a separate config for babel for dependencies. This is due to babel@7 now assuming that esm is in use. There's a config flag to make it autodetect like webpack that we should turn on.

@jsnajdr
Copy link
Member

jsnajdr commented Jan 17, 2019

Interesting. TIL that CRA does that and that transpiling all node_modules is safe and reasonably fast.

@blowery
Copy link
Contributor Author

blowery commented Jan 18, 2019

@jsnajdr should work now?

@blowery blowery self-assigned this Jan 18, 2019
@blowery blowery added [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. and removed [Status] In Progress labels Jan 18, 2019
@blowery
Copy link
Contributor Author

blowery commented Jan 18, 2019

Open questions:

  • Do we need to transpile for tests too? Tests run in node and all this fancy syntax in node_modules should run on Node 10, so ... probably not?
  • How much of a perf hit are we looking at?

@jsnajdr
Copy link
Member

jsnajdr commented Jan 23, 2019

Do we need to transpile for tests too? Tests run in node and all this fancy syntax in node_modules should run on Node 10, so ... probably not?

We transpile tests with current Node as a target: https://github.com/Automattic/wp-calypso/blob/master/babel.config.js#L100-L107

Standard ES2018 should therefore transpile without any changes. The target is JSX and nonstandard extensions like class properties. Do any NPM modules ship that kind of syntax?

@blowery
Copy link
Contributor Author

blowery commented Jan 23, 2019

Do any NPM modules ship that kind of syntax?

Not that I've seen. This is mostly motivated by packages that are dropping old Node support and in the process picking up ES6 features, like let, for of, those sorts of things. I've not seen anyone shipping experimental syntax.

Copy link
Member

@jsnajdr jsnajdr left a comment

Choose a reason for hiding this comment

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

Build speed doesn't seem to be significantly affected. It turns out that the modules from node_modules are in minority. There are 8520 files transpiled, only 3540 of them coming from node_modules.

I gathered all this data by placing a console.log statement to the babel-loader source.

babel.dependencies.config.js Outdated Show resolved Hide resolved
'@babel/react',
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
Copy link
Member

Choose a reason for hiding this comment

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

This transform is also not needed. We don't expect import( ... ).then( ... ) statements in NPM packages, do we?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think so, but CRA has it.... It would be odd.

Copy link
Member

Choose a reason for hiding this comment

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

To CRA it was added by @Timer in facebook/create-react-app#5047. The PR description doesn't mention that any published NPM package would use it.

It's a very minor issue that doesn't really matter at all. It's just my curiosity that's not 100% satisfied 😃

Copy link

Choose a reason for hiding this comment

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

This transform is also not needed. We don't expect import( ... ).then( ... ) statements in NPM packages, do we?

We expect npm packages to use it in Create React App.

Copy link

@Timer Timer Jan 28, 2019

Choose a reason for hiding this comment

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

Also, compiling node_modules is not without its pitfalls, it's very unsafe.
You should avoid compiling node_modules if you can. 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

though that does make me revisit the choice to use a blacklist. Maybe we need a whitelist instead.

Copy link

@Timer Timer Jan 29, 2019

Choose a reason for hiding this comment

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

I'd suggest keeping a list of "bad actors". If packages are intended to be used in the browser (even if they're Node packages) the publisher is responsible for compiling.
We always told users who tried to import non-ES5 that the publisher had to fix, and most of the time, they did.

Bad actors would contain often-used packages that refuse to compile themselves.

Copy link

Choose a reason for hiding this comment

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

Insider tip: be sure to disable transform-typeof-symbol. This should fix mapbox and reduce some of the side effects of compiling node_modules if you go through with it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Timer I saw that in CRA@2 and wondered about it.. IE11 doesn't support Symbol at all, so dropping support for the typeof check seems ... chancy?

Ah, facebook/create-react-app#5278 has more background.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

switch all this over to using a whitelist

webpack.config.js Outdated Show resolved Hide resolved
{
test: /\.jsx?$/,
include: /node_modules\//,
exclude: /@babel(?:\/|\\{1,2})runtime/,
Copy link
Member

Choose a reason for hiding this comment

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

There are several libraries that we can trust to be ES5:

  • babel-runtime -- the 6 version used still by many libraries: react-virtualized, click-outside, wpcom (!)
  • core-js
  • everything in @wordpress/*
  • lodash
  • regenerate-unicode-properties -- used by the @babel/plugin-proposal-unicode-property-regex transform
  • gridicons
  • fbjs
  • react and related
  • wpcom

Copy link
Member

Choose a reason for hiding this comment

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

One strange file that gets transpiled by Babel:

node_modules/css-loader/lib/css-base.js

I don't know why exactly, but it's probably the JS code that's added by css-loader and then removed again by the extract plugin?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like exclude can be a function instead of a regex. Perhaps that would make building and maintaining that list easier. https://webpack.js.org/configuration/module/#condition

Copy link
Contributor Author

Choose a reason for hiding this comment

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

btw, react is not safe. They use Map and Set. :) https://reactjs.org/docs/javascript-environment-requirements.html

Copy link
Member

Choose a reason for hiding this comment

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

btw, react is not safe. They use Map and Set.

That is safe for us. We import @babel/polyfill at the very beginning, and it polyfills the Map and Set globals, according to the browser requirements in @babel/preset-env.

It wouldn't be safe if we:

  • used useBuiltins: 'usage' in preset-env. Babel needs to transpile a module to see that a polyfill is needed.
  • used corejs: true in transform-runtime. Then Babel transpiles usages of the Map global to imports from babel-runtime-corejs, avoiding the need for global namespace pollution.

In both cases, Babel would need to see the React module to polyfill correctly.

Copy link
Contributor Author

@blowery blowery Jan 28, 2019

Choose a reason for hiding this comment

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

switched to a function based exclude in 40d46b8

also looked at trying to automate the set of includes with a function that parses each package.json and checks the engines property, but it looks like that won't fly because at least some of the packages that have dropped node 4 support have not updated engines.

blowery added a commit that referenced this pull request Jan 30, 2019
See facebook/create-react-app#5278 for details.
It makes all code slower and can break some libraries.

Pulled out of #30196
@ockham
Copy link
Contributor

ockham commented Feb 21, 2019

I tend to agree with @Timer that it might make sense to err on the safe side when transpiling node_modules -- but I would love us to start doing it for those packages that actually publish their untranspiled code (so we can drop some of these aliases).

Some prior art here: #19698. FWIW, the heuristics I was using was based on the presence of a module or esnext field in package.json. Granted, the former only means ESM (and not necessarily ES6(+)), and the latter is more of a proposal at this point, but I thought there'd probably be a strong ESM-ES6(+) correlation (translating to 'untranspiled') anyway.

@blowery
Copy link
Contributor Author

blowery commented Feb 21, 2019

@ockham I agree, we'll want to use a whitelist, not transpile everything. I'm not in love with using module / esnext as a heuristic, as it means we'll likely be transpiling more than we need to, but it might serve as a check. Maybe we also come up with a new thing that scans the source and yells if it sees things that need transpilation?

jsnajdr pushed a commit that referenced this pull request Feb 25, 2019
See facebook/create-react-app#5278 for details.
It makes all code slower and can break some libraries.

Pulled out of #30196
jsnajdr pushed a commit that referenced this pull request Feb 26, 2019
* Disable babel transform-typeof-symbol

See facebook/create-react-app#5278 for details.
It makes all code slower and can break some libraries.

Pulled out of #30196

* Move the exclude option to preset-env config
@blowery blowery mentioned this pull request Mar 6, 2019
@blowery blowery requested a review from a team as a code owner March 25, 2019 20:37
@blowery blowery changed the title Try to transpile node_modules Build: Transpile whitelisted packages in node_modules Mar 25, 2019
@blowery
Copy link
Contributor Author

blowery commented Mar 25, 2019

Switched this to use a whitelist in aea84cf

@simison
Copy link
Member

simison commented Mar 26, 2019

Noting that Mapbox dependency might go away soon (~this week) in Calypso after migrating the block's code from Calypso to Jetpack.

In Calypso

"mapbox-gl": "0.52.0",

In Jetpack
https://github.com/Automattic/jetpack/pull/11640/files#diff-b9cfc7f2cdf78a7f4b91a753d10865a2R151

@blowery
Copy link
Contributor Author

blowery commented Mar 26, 2019

@simison with the new whitelist, we're no longer trying to transpile mapbox-gl.

Copy link
Member

@jsnajdr jsnajdr left a comment

Choose a reason for hiding this comment

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

Posted two nits, looks good to 🚢

@@ -160,6 +193,27 @@ function getWebpackConfig( {
cacheIdentifier,
exclude: /node_modules\//,
} ),
{
test: /\.jsx?$/,
include: _.memoize( shouldTranspileDependency ),
Copy link
Member

Choose a reason for hiding this comment

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

The memoize here is probably an insignificant optimization. It will maintain a map with several thousands entries, and webpack does so many checks on each filename so that this extra one is unlikely to make any impact.

],
],
// Exclude transforms that make all code slower, see https://github.com/facebook/create-react-app/pull/5278
exclude: [ 'transform-typeof-symbol' ],
Copy link
Member

Choose a reason for hiding this comment

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

Does the exclude need to be there in two copies?

},
},
],
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we use the Transpile shard from @automattic/calypso-build for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

possibly, i'll try it out

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done!

@jsnajdr
Copy link
Member

jsnajdr commented Mar 28, 2019

Thanks for fixing the nits! Looks good to me 👍

@blowery blowery merged commit 3388c69 into master Mar 28, 2019
@blowery blowery deleted the fix/transpile-node-modules branch March 28, 2019 18:33
@matticbot matticbot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Mar 28, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants