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 CSS assets with Webpack #26820

Merged
merged 7 commits into from
Sep 21, 2018
Merged

Build CSS assets with Webpack #26820

merged 7 commits into from
Sep 21, 2018

Conversation

jsnajdr
Copy link
Member

@jsnajdr jsnajdr commented Aug 22, 2018

Latest status:
This PR evolved from something that was a proof of concept into a finished production-ready PR. See #26820 (comment) for more info about the final state of things.

Original description from the proof-of-concept era:
This is a proof of concept of migrating the CSS build pipeline to Webpack. Moves a few components' styles to import './style.scss' in the JS module, and makes sure that the CSS is built with Webpack and the stylesheets are included in the server-generated index.html on initial load.

Code splitting also seems to work very well: Webpack automatically creates CSS chunks for async-loaded sections and loads the stylesheets together with scripts when loading the section. Our custom code for "sections CSS" can be eventually removed.

See individual commit messages for details.

Largest missing piece is support for RTL. Completely ignored at the moment. I'm afraid we'll need to write a custom Webpack plugin, a nontrivial one, to make it work as we need it:

  • generate both LTR and RTL versions of each CSS asset
  • choose the right stylesheet on initial load
  • load the right stylesheet when async-loading a section

If we wanted to do a gradual migration and keep both CSS pipelines in place for some time, there is one gotcha I discovered that makes this very fragile: we have some CSS rules where specificity is determined by rule order and nothing else. Example:

.button {
  font-weight: bold;
}
.header-button {
  font-weight: normal;
}
<button class="button header-button">

What is the resulting font-weight of the button? It depends on the order of the rules. The last one wins. It's very easy to break this when moving stylesheets around, as the order can change.

Another todo: patch the other index files in client/document (desktop, browsehappy, support-user) to also include links to the right stylesheets.

How to test:
Run Calypso both in debug (npm run start) and production (CALYPSO_ENV=production npm run build) modes and verify that all stylesheets are loaded as expected.

@matticbot
Copy link
Contributor

@jsnajdr jsnajdr requested review from dmsnell and a team August 22, 2018 08:23
@@ -191,6 +194,10 @@ function getWebpackConfig( { externalizeWordPressPackages = false } = {}, argv )
loader: 'sass-loader',
options: {
includePaths: [ path.join( __dirname, 'client' ) ],
data: `
@import '${ path.join( __dirname, 'assets/stylesheets/shared/_colors.scss' ) }';
@import '${ path.join( __dirname, 'assets/stylesheets/shared/_utils.scss' ) }';
Copy link
Member

Choose a reason for hiding this comment

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

It's enough to import just _utils.scss here because it in turn imports colors:

@import '../shared/colors'; // import all of our wpcom colors

Copy link
Member Author

@jsnajdr jsnajdr Aug 23, 2018

Choose a reason for hiding this comment

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

Thanks for pointing this out. Addressed in 3bbffee

@@ -113,6 +113,9 @@ const wordpressExternals = ( context, request, callback ) =>
*/
// eslint-disable-next-line no-unused-vars
function getWebpackConfig( { externalizeWordPressPackages = false } = {}, argv ) {
const cssFilename =
isDevelopment || calypsoEnv === 'desktop' ? 'style-[name].css' : 'style-[name].[chunkhash].css';
Copy link
Member

Choose a reason for hiding this comment

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

Do we necessarily need style- prefix? The .css extension already tells us it's a stylesheet.

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't really need the prefix, it's just that our .gitignore file doesn't ignore all /public/*.css files, but only ones that match certain patterns: /public/style*.css. Easy to change, of course, but I choose the path of least resistance :)

@@ -113,6 +113,9 @@ const wordpressExternals = ( context, request, callback ) =>
*/
// eslint-disable-next-line no-unused-vars
function getWebpackConfig( { externalizeWordPressPackages = false } = {}, argv ) {
const cssFilename =
Copy link
Member

@simison simison Aug 22, 2018

Choose a reason for hiding this comment

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

Can we make this configurable? That would help SDK because if I now run:

npm run sdk:gutenberg -- --editor-script=client/gutenberg/extensions/todo/block.js

That would compile a file client/gutenberg/extensions/todo/build/style-todo-editor.css (with style-) instead of client/gutenberg/extensions/todo/build/todo-editor.css

Perhaps something like this:

function getWebpackConfig( { cssFilename, externalizeWordPressPackages = false } = {}, argv ) {
	if ( ! cssFilename ) {
		cssFilename = isDevelopment || calypsoEnv === 'desktop' ? 'style-[name].css' : 'style-[name].[chunkhash].css';
	}

And in SDK:

const baseConfig = getBaseConfig( { cssFilename: '[name].css', externalizeWordPressPackages: true } );

@simison
Copy link
Member

simison commented Aug 22, 2018

Jest is failing for this because it doesn't know what to do with scss imports.

I solved this in another PR (#26683) by mocking style imports, so you might want to look into that solution: https://github.com/Automattic/wp-calypso/blob/c72ff9c44422589813bdb43f270e619827419aef/test/test/helpers/styles/README.md

https://jestjs.io/docs/en/webpack#handling-static-assets

@ockham
Copy link
Contributor

ockham commented Aug 22, 2018

If we wanted to do a gradual migration and keep both CSS pipelines in place for some time, there is one gotcha I discovered that makes this very fragile: we have some CSS rules where specificity is determined by rule order and nothing else. Example:

.button {
  font-weight: bold;
}
.header-button {
  font-weight: normal;
}
<button class="button header-button">

What is the resulting font-weight of the button? It depends on the order of the rules. The last one wins. It's very easy to break this when moving stylesheets around, as the order can change.

Do you have any grasp how many of those we have? Is this something we can fix and discourage Calypso-wide?

@simison
Copy link
Member

simison commented Aug 22, 2018

Is this something we can fix and discourage Calypso-wide?

If this becomes a blocker, can we scope this PR down to not contain any style imports in some of the JS files for now? I.e. just keep the build and global _utils.scss.

@jsnajdr
Copy link
Member Author

jsnajdr commented Aug 22, 2018

Do you have any grasp how many of those we have? Is this something we can fix and discourage Calypso-wide?

I have no idea. I discovered the button header-button issue by looking at the /plugins/manage page and seeing that a button styling was broken. There will surely be many other styles like this, but I don't know how to find them reliably.

It can and should be fixed by making the selectors more specific: instead of declaring .header-button styles, change them to .button.header-button.

If this becomes a blocker, can we scope this PR down to not contain any style imports in some of the JS files for now?

Like committing the Webpack CSS build pipeline, but not actually using it for anything? Well that's possible, but suboptimal :)

@jsnajdr jsnajdr force-pushed the try/webpackify-css branch from 4b9abfd to 4cf0cce Compare August 23, 2018 08:34
@jsnajdr
Copy link
Member Author

jsnajdr commented Aug 23, 2018

I pushed an update to this PR that adds RTL support. There are three steps:

  1. Use webpack-rtl-plugin to generate RTL and LTR versions of each CSS asset. cssnano minification has been removed from the postcss step, as it needs to happen at the very end, on both "forked" versions of the CSS separately. The webpack-rtl-plugin takes care of that internally, configured with the minify option.
  2. Use mini-css-extract-plugin-with-rtl, a fork of the official version (thanks @ockham for finding it!). It has a patched runtime dynamic loader that looks at the document.dir global variable every time an async CSS chunk is being loaded and requests the appropriate version of the asset.
  3. Patch the code that generates the index.html on server and ships <link rel="stylesheet"/> tags in the initial document. Make it look at the user's isRTL flag and send the right version of the stylesheet.

Step 3 works only when user bootstrapping is in action. If the server is not capable to retrieve the authenticated user's locale info, it will always send the LTR version. There is code in the client (search for setLocaleInDOM) that switches the stylesheets at runtime at the time when the locale becomes known. That runtime switch doesn't work yet in this PR.

@@ -12,7 +12,8 @@ const fs = require( 'fs' );
const path = require( 'path' );
const webpack = require( 'webpack' );
const AssetsWriter = require( './server/bundler/assets-writer' );
const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' );
const MiniCssExtractPlugin = require( 'mini-css-extract-plugin-with-rtl' );
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we rename the import to reflect the WithRtl part?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I'd rather keep the name without a suffix. It's a forked plugin that will likely be merged into upstream sooner or later.

Copy link
Contributor

Choose a reason for hiding this comment

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

We might need to actively lobby the upstream project for this to happen tho...

Copy link
Member Author

Choose a reason for hiding this comment

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

We might need to actively lobby the upstream project for this to happen tho...

I just got involved by commenting on an issue and by filing another, somewhat related one.

@ockham
Copy link
Contributor

ockham commented Aug 23, 2018

This seems to be working pretty well!

In order to divide & conquer, would it make sense to move the webpack config changes (that essentially enable RTL) to a separate PR (which will only affect bundles built by the SDK for now), and to sort out the remaining questions for Calypso as a whole separately?

@@ -113,6 +114,9 @@ const wordpressExternals = ( context, request, callback ) =>
*/
// eslint-disable-next-line no-unused-vars
function getWebpackConfig( { externalizeWordPressPackages = false } = {}, argv ) {
const cssFilename =
isDevelopment || calypsoEnv === 'desktop' ? 'style-[name].css' : 'style-[name].[chunkhash].css';
Copy link
Member

Choose a reason for hiding this comment

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

With this when we build a Gutenberg extension from the SDK for production, the output CSS files will contain the chunkhash in the name:

NODE_ENV=production npm run sdk:gutenberg -- --editor-script=client/gutenberg/extensions/editor-notes/index.js

Perhaps it's a good idea to keep it only style-[name].css for the SDK, regardless of whether we're building for development or production.

jsnajdr added a commit that referenced this pull request Aug 24, 2018
Changes the ignore rules to ignore all CSS and CSS sourcemap files in `/public`. There
are no version-controlled CSS files in that directory and the current rules are overly
specific. They were created a long time ago in pre-OSS era.

Removes unnecessary constraints on naming of CSS chunks in #26820.
@jsnajdr
Copy link
Member Author

jsnajdr commented Sep 19, 2018

Hear hear, this PR is ready for review and introduces the first bit of production CSS built with Webpack into Calypso.

What are the changes in this PR?

Webpack "assets" for the app entrypoint or a chunk used to be just JS files, but now there are three types of them: JS, CSS.LTR and CSS.RTL. When creating the index.html document on server, there is a new code that includes links to these assets correctly: JS as <script> tags, CSS of the appropriate flavor as <link rel=stylesheet> tags.

I created a fork of the mini-css-extract-plugin-with-rtl that includes a data-webpack=true attribute on each <link rel=stylesheet> tag. We'll use that attribute to find <link> tags where we want to switch stylesheets from LTR to RTL. It's unfortunate that we now have our own fork of something that's already a fork. Let's work on integrating all the changes back into upstream, but I don't expect quick results here.

When switching locale at runtime, there's new code (switchWebpackCSS) to switch CSS assets coming from Webpack, too. Try the testing instructions from #27236 on the login page to see how it works.

Finally I converted the login section to Webpack CSS as our first Webpack-generated production CSS.

function switchWebpackCSS( isRTL ) {
const currentLinks = document.querySelectorAll( 'link[rel="stylesheet"][data-webpack]' );

return map( currentLinks, async currentLink => {
Copy link
Contributor

@blowery blowery Sep 19, 2018

Choose a reason for hiding this comment

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

Do we need to return an array of promises? Doesn't appear it's used...

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, that's right, nobody waits for the result. That's true for the whole switchLocale function that does a lot of async updates -- classic CSS, webpack CSS, loading the translation file... We just call switchLocale and never look back.

I think it's still "good manners" to return such a promise, so I'm reluctant to remove the code. 🎵 We shall surely use it one day 🎵

package.json Outdated
@@ -132,7 +132,7 @@
"lru": "3.1.0",
"lunr": "2.3.3",
"marked": "0.5.0",
"mini-css-extract-plugin-with-rtl": "0.1.3",
"mini-css-extract-plugin-with-rtl": "github:jsnajdr/mini-css-extract-plugin-with-rtl",
Copy link
Contributor

Choose a reason for hiding this comment

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

can we either publish this fork to npm under a new name or at least move the github repo under Automattic? :)

Copy link
Member Author

@jsnajdr jsnajdr Sep 19, 2018

Choose a reason for hiding this comment

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

Moved to the A8c org in e16fb8e

mini-css-extract-plugin-with-rtl-and-webpack-attr is too long a name :)

jsnajdr added a commit to Automattic/wp-desktop that referenced this pull request Sep 20, 2018
Server build doesn't need to process any CSS imports -- that's only for Calypso client build.
Fixed build errors in Automattic/wp-calypso#26820.
@jsnajdr
Copy link
Member Author

jsnajdr commented Sep 20, 2018

The desktop build should become green again after Automattic/wp-desktop#503 is merged.

jsnajdr added a commit to Automattic/wp-desktop that referenced this pull request Sep 20, 2018
Server build doesn't need to process any CSS imports -- that's only for Calypso client build.
Fixed build errors in Automattic/wp-calypso#26820.
currentLink.parentElement.removeChild( currentLink );
}
} );
}
Copy link
Contributor

@ockham ockham Sep 20, 2018

Choose a reason for hiding this comment

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

There is some overlap in terms of functionality of this function with what we're doing in DocumentHead to change the document title on the client side when switching between different sections.

I wonder if we could extend DocumentHead to also cover the CSS link case, and then dispatch setDocumentHeadLink when flipping betwwen LTR and RTL. (A word of caution, DocumentHead, and when to change or reset what, has been a source of subtle bugs, due to the many requirements there are for it -- amoung them SSR). However, it might be worth centralizing this kind of logic. (I don't want to block this PR on it though, can't wait to see this one at work 🙂 )

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I'll pass on this idea 🙂

DocumentHead owns the document title and some of the meta and link elements (but not all). The source of truth is Redux state, both in server and client rendering, and DocumentHead is a client-side tool that makes sure that the DOM is updated.

The script and stylesheet links, on the other hand, are not owned by Redux or any app state. They are owned by webpack. setLocaleInDOM updates these webpack-owned links in a careful cooperation with the webpack runtime.

I think these two very distinct concepts should be mixed together. The only thing they have in common is that the stylesheets live in the head element, that's all.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, fair enough.

return map( currentLinks, async currentLink => {
const currentHref = currentLink.getAttribute( 'href' );
const newHref = flipRTL( currentHref, isRTL );
if ( currentHref === newHref ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this ever happen, given what flipRTL does?

Copy link
Member Author

Choose a reason for hiding this comment

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

The logic is "put the URL into the desired state (LTR or RTL)" rather than "toggle the state from the current state to the other one". Maybe flipRTL has a misleading name?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, might be worth renaming 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

Renamed to setRTLFlagOnCSSLink and added doc comment to the function.

@ockham
Copy link
Contributor

ockham commented Sep 20, 2018

I tested successfully:

  • Logged-in mode
  • Switching to Arabic as UI language in logged in mode
  • Logged-out mode, landing at /log-in/de
  • Logged-out mode, landing at /log-in/he, and switching to German

Copy link
Contributor

@ockham ockham left a comment

Choose a reason for hiding this comment

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

This tests well and looks good 👍 (Good job on getting blockers out of the way in separate PRs!)

I have one question, I noticed that while our CLI produced stylesheets are loaded with a hash query arg (e.g. /calypso/style.css?v=d247105eb8), that's not the case for the webpack-produced ones in this PR. Shouldn't they also get one?

@jsnajdr
Copy link
Member Author

jsnajdr commented Sep 21, 2018

I have one question, I noticed that while our CLI produced stylesheets are loaded with a hash query arg (e.g. /calypso/style.css?v=d247105eb8), that's not the case for the webpack-produced ones in this PR. Shouldn't they also get one?

The stylesheets get a hash in production mode, .e.g.,

https://wordpress.com/calypso/login.360b2c2bc4a2f42c5ded.css

Just like the JS chunks.

In development mode, there is no hash. Neither for JS nor CSS.

Copy link
Contributor

@ockham ockham left a comment

Choose a reason for hiding this comment

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

LGTM 👍

…m into index.html

Until now, Webpack generated only JS assets, but now there are also CSS. When generating index.html
on the server, each type needs a different tag: `<link rel="stylesheet">` vs `<script>`. Divide the
assets into groups and insert appropriate tags.
@jsnajdr jsnajdr merged commit 40d13b3 into master Sep 21, 2018
@jsnajdr jsnajdr deleted the try/webpackify-css branch September 21, 2018 13:30
@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 Sep 21, 2018
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.

6 participants