This post is a collection of thoughts and ideas around CSS development in Magento 2. It is triggered by several background activities:
- Magento 2 has Grunt support, but we get a lot of feedback that they would like Gulp support. For example, https://github.com/magento/magento2/issues/2104.
- I am (in my spare time) currently writing a book on CSS and HTML in themes for Magento 2. I am not a CSS expert – the book is explaining how M2 works with examples and recommendations on how to do your work and fit in with the M2 strategy. So I am thinking about this area more at present. How to minimize the pain for CSS developers.
Stakeholders
There are several different scenarios that Magento 2 supports at the same time.
- An extension developer should be able to create a module with HTML that just works when loaded. No front end developer should need to be present.
- A theme developer should be able to create a new theme to adjust the look and feel of all extensions of a site, without knowledge of what the extensions are. This requires agreements/conventions for extension developers to follow that theme developers can take advantage of. (Theme should be able to make module specific adjustments, but changing the color of buttons should not require the theme to know of every module with a button in it.)
- It is common for a site to have a custom theme developed to brand the site as their own. There is a great range in sophistication that can occur here from “just get it going” to “save 5% more render time on the home page no matter what!”
Supporting all of the above does add complexity, but the benefit is also significant. You can buy an extension and it will just work; you can buy a theme and it will just work; you can get a specialist if you want to invest the extra money. (Yes, life is never quite this simple, but the more it can be achieved the better.)
CSS Preprocessing
Magento 2 made the decision to use Less for a CSS preprocessor. (The pipeline is actually generic – we could slot in Sass in the future, but all the files shipped by default today are Less.) I am not going to rehash this decision here. The question I want to address is why use a CSS preprocessor at all?
CSS preprocessors (I believe) came from projects with a single web site where you want to get the overall site consistent. CSS preprocessors made it easier to manage large collections of CSS – for example, making sure that everything that is Magento Orange uses the official color code by having a global variable everyone can use. (There are lots of better examples – the point is it is useful to a site developer building a single site.)
Magento 2 goes further and leverages the concept of preprocessing to provide a bridge between theme developers and extension developers. By having a library of presentation concepts (like buttons, panels, etc) encoded in “mixins”, extensions can use the mixins without concern for exact CSS styling to use, and themes can adjust the mixin (or variables the mixin uses) and have all occurrences across the site adjust appropriately. This is the role of the “Magento UI Library”. It stands between the two parties as the definition of common ground – it is a library of mixins and variables used by the mixins.
For a specialist front end developer putting together a site, the challenge is then how to take the same CSS files (which have been somewhat fragmented because of lots of different extensions being loaded) and adjust them further to build a consistent final product. (They also get to clean up any mess along the way.) They don’t really want to waste time by starting from scratch as some of the CSS controls the layout of say a form. But the fragmented set of CSS files as a result of lots of extensions being loaded is not what they are normally used to working with if they got to build the HTML for the site completely from scratch. The front end developer in Magento is apply further changes on top of something someone else has developed.
This leads to the question of when should a front end developer tweak the CSS (and HTML) files provided versus start again because they want to do something so radically different. When is it cheaper to understand and adjust existing CSS versus starting with a clean slate. The clean slate approach unfortunately typically has a higher upgrade cost later – more customization usually increases upgrade costs.
I suspect (!) providing the initial set of HTML and CSS (from extension developers and possibly theme developers) reduces the cost of getting a new site up and going, but may be a less fun job for a front end developer as a result as they are more constrained. (This is conjecture on my part!)
//@magento_import and Fallbacks
So where does //@magento_import sit in all of this? This was added to Less to search for a specified file name in every loaded module. This was a generic mechanism to collect Less fragments from lots of different modules, rather than hard coding it into the code base. If the module has the file, it is added to the import list; if not, it is skipped. The //@magento_import mechanism is useful, but clearly adds to the processing effort to locate all required files.
Fallbacks is another area that adds overhead – Magento has the concept of allowing one module to override a file in another module (and for a locale to override a base definition of a file). So opening a single file may require a series of file probes to locate where the file is on disk. This is useful, but surely adds extra overhead. (I have not measured how much, so not sure how significant.)
Gulp vs Grunt
So now let’s switch tack and talk about tooling.
Given that developers no longer edit CSS directly, but instead edit Less files that need processing to generate the CSS files from, tools emerged to make this process less painful. Grunt and the newer Gulp tools are examples of supporting tools. They can be made to watch the file system for changes, then regenerate the CSS file immediately. Combined with the LiveReload protocol, you can then save a Less file, have it recompiled, and the web browser immediately reloads the new CSS file when ready. Cool!
Front end developers can then tweak settings all day long to achieve the effect they need until they get it right.
So why do people care about Gulp vs Grunt? From some quarters I hear “there is not really that much difference – people just prefer the tools they are used to” and in others I hear “Gulp is much faster as more processing is done completely in memory, so you get more done quicker”. Not being a front end developer myself, I have no opinion here – but I do find it confusing the quite different opinions expressed.
I was pondering about whether a generic tool (like Grunt or Gulp) would deliver enough of a benefit or whether a custom tool is ultimately needed. There are other questions like whether you need to configure Grunt and Gulp to tell them about every theme, every locale, every CSS file generated – especially when Magento knows what most of these are (so why not deduce them automatically?). How far to automate vs how much to optimize using Magento specific knowledge (e.g. modules downloaded via Composer are generally static) vs how much to leverage existing tools is an interesting question here.
So as I sat here pondering, I was thinking about the tools Magento does supply. One command is “magento dev:source-theme:deploy”. What the following example command does is generate a directory tree under pub/static/frontend/Magento/luma/en_US with all the less source files required to compile up the CSS files for the site.
$ magento dev:source-theme:deploy --type=less --locale=en_US --area=frontend --theme=Magento/luma css/styles-m css/styles-l css/print css/email
Where possible it creates symbolic links off to the real files (so any change to the master is spotted immediately), creating a copy where a direct link cannot be used. For example, “//@magento_import” directives are expanded to a set of “@import” directives with specific path names. The output of the above command is standard Less syntax and so can be complied by any Less preprocessor. Because of this behavior, file containing “//@magento_import” directives cannot be symlinked – a copy is made where all the files to import are injected into the copy.
So what about Gulp? Here is a hacky first attempt at a gulpfile.js. (I have never used Gulp before, so please excuse any silliness.)
// Include gulp var gulp = require('gulp'); // Include Our Plugins var jshint = require('gulp-jshint'); var less = require('gulp-less'); var concat = require('gulp-concat'); var uglify = require('gulp-uglify'); var rename = require('gulp-rename'); var gutil = require('gulp-util'); // Compile Our Less gulp.task('less', function() { return gulp.src(['pub/static/frontend/Magento/luma/en_US/css/styles-m.less', 'pub/static/frontend/Magento/luma/en_US/css/styles-l.less', 'pub/static/frontend/Magento/luma/en_US/css/print.less', 'pub/static/frontend/Magento/luma/en_US/css/email.less']) .pipe(less()) .pipe(gulp.dest('pub/static/frontend/Magento/luma/en_US/css')); }); // Watch Files For Changes gulp.task('watch', function() { gulp.watch('pub/static/frontend/Magento/luma/en_US/**/*.less', ['less']); }); // Default Task gulp.task('default', ['less', 'watch']);
So what went wrong when I tried it? Nothing. Gulp started watching the input “*.less” files and recompiled the “*.css” files on any change to a “*.less” file! It worked!
Now the above script is far from optimal – any less file change will recompile everything. So it is certainly not flexible at present. But it is enough to demonstrate that actually Gulp can be used with Magento 2 today. You need to run “magento dev:source-theme:deploy” if a new file is created, renamed, or removed, but if you are just making repeated edits to the same file, you will not need to rerun “magento dev:source-theme:deploy” every time.
(I then wasted an hour trying to get LiveReload working as well, but failed. If someone could help that would be fantastic – I may update this blog to sneak it in here.)
Conclusions
Surprise number one for me. After all this talk about if Gulp can be supported, the answer is “yes”. A bit of manual setup is required, but it seems to work!
So what do you think? I welcome comments on parts you agree or disagree with. I am more like a sponge in this case, seeking external knowledge. I understand the different use cases that need to be satisfied – there are clearly different usage scenarios with different requirements. That is fine. All opinions welcome.
Some specific questions I had:
- Do you want Gulp features, or are you just after speed? (There may be a fast solution that does not involve Gulp.)
- Do you use client side Less compilation and why? (If server side compilation was fast, is client side Less compilation still useful in circumstances?)
- Did I characterize the different use cases correctly from your experience?
- Can you suggest a better, more flexible, gulp file, with LiveReload support?
This are still other problem – flexibility. Many of us are using CSS postprocessing i.e. standalone Autoprefixer or PostCSS with plugins. How to keep same code on dev and production mode?
CLI setup:static-content:deploy override everything with output of PHP based LESS (or whatever lang you chose) preprocessor.
Could you expand or point me at an example?
The deploy script does rebuild the symlinks. Are you saying you want extra files in the same directory? That would work if you put the master file in theme/module directory right? Then deploy script will symlink them into directory. So yes, redeploy required if add a new file – but hopefully not too painful. Would that work?
Output of postprocessed styles (i.e. LESS -> CSS -> PostCSS -> CSS) is different than results of PHP styles preprocessing (LESS -> CSS).
Using node.js based tasks localy is great, but is hard to keep same output on production, without compiling them manually on production / using CI.
How do people do it with gulp today on other projects? Git commit generated CSS file? Or build gulp also into deployment pipeline?
IMO best way is to use Continuous Delivery as release building tool, to be always ready to push new release on production/stage.
+ Repository is clean, only source files, without annoying merge problems on minified files
– Requires some knowledge/time to configure and keep this running in good shape
And this “time requirement” point, is why some devs (like we in Snowdog) currently keep up compiled styles inside repositories of our M1 stores. But definitely we want to change this, probably during switch process to M2, so… right now 🙂
For me “good enough” solution will be to change M2 CLI a little bit, to let devs easily disable styles processing. Maybe some flag, like “–no-styles”?
Want to create a –no-styles request on github? So associated with your name
Why not, I’ll will look on this – I have currently overridden ‘setup:static-content:deploy’ (correct me if I’m wrong – this is only front-end related production deployment taks?) to not process any files with “css, less, sass” extensions, but not sure how to make it flexible, to let define devs custom extensions without changing code and typing them over and over as CLI params
It’s similar problem as I have with Gulp tools – where and how to keep important configuration variables, to keep it simple and flexible at same time.
Thank you for your post, i just tried it out and worked so much faster then grunt… love it so far. Probably will add some things though
Hi Alan,
You could probably use something like: https://github.com/sindresorhus/gulp-changed to only process changed files.
Unfortunately it’s useless in styles processing 🙂
Hi Alan,
I didn’t find any help in the docs about this deployment procedure.
So you’re working locally in developer mode, everything fine, styles are changing.
You push your file to your production server (which of course is in production mode)
and run setup:static-content:deploy
but on production server the changes aren’t showing unless you first delete the pub/static/frontend
and then run the deploy command. Why is that? We don’t want to manually delete files…
Hi. I might not be completely following, but in production I generally recommend running “magento deploy:mode:set production”. This does quite a few of the steps for you (put in maintenance mode, deploy, take out of maintenance mode). However, when I run setup:static-content:deploy I am getting the changes put under pub/static. Is it a caching problem perhaps? (E.g. Varnish.)