Use Css Preprocessor and Babel on Shopify
7 minutes readingWorking on the Shopify frontend can be frustrating if you’re used to use the new amazing frontend tools and features like CSS preprocessors, Javascript ES6 and task runners or pack bundlers that perfom for you some common boring tasks like autoprefixing new css specifications.
This is most felt when you’re not the only one working on a Shopify theme.
Lately, in fact, I had the chance of working on a Shopify theme with two other people and you know that, as for Shopify development, there’s no local environment and all the website is in test on an online “stage-like” environment so, if you’re working on the same file, this is constantly overwritten online.
How Shopify works under the hood
All liquid files are special files that, apart from parsing the liquid syntax, they can also access and read the website settings variables and this also works for css and js files that, together with their own extension, they also have liquid extension.
For instance style.css.liquid and script.js.liquid when put inside the assets folder and add to the <head>
tag of the layout template with {{ 'style.css' | asset_url | stylesheet_tag }}
and with {{ 'script.js' | asset_url | stylesheet_tag }}
will be parsed to replace all liquid variables with their value.
If you, then, include a style.scss.liquid
like this {{ 'style.scss' | asset_url | stylesheet_tag }}
the sass will be, in addition, compiled in css.
But that’s all. You cannot write modern js because there’s not a server babel-like transpilation like the sass compilation for the js, for instance, and there are no sourcemaps provided. A little bit to be in 2019.
So, after studying how Shopify works under the hood and analysing our needs, this is the solution I proposed.
Needs
- We would like to be able to work on separate scss files which can be combined together for production without further manual effort
- We would like to use an automatic autoprefixer task during compilation of the files
- We would like to have sourcemaps in our compiled css for easier debugging
- We would like to be able to use Javascript ES6 without thinking to browser compatibility
So let’s configure our package.json!
Create a package.json file and copy this into it:
{
"name": "cool-shopify",
"version": "1.0.0",
"devDependencies": {
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^6.0.0",
"gulp-babel": "^7.0.1",
"gulp-concat": "^2.6.0",
"gulp-if": "^3.0.0",
"gulp-rename": "^1.4.0",
"gulp-sass": "^4.0.2",
"gulp-sourcemaps": "^2.6.5",
"gulp-uglify": "^3.0.1",
"node-sass": "^4.11.0"
},
"dependencies": {
"babel-polyfill": "^6.26.0"
},
"browserslist": [
"defaults"
]
}
and run npm install
.
We’re going to use Gulp + Babel for convenience but it can be configured with Webpack as well, I think.
The Gulpfile
'use strict';
const gulp = require('gulp');
const babel = require('gulp-babel');
const autoprefixer = require('gulp-autoprefixer');
const concat = require('gulp-concat');
const gulpif = require('gulp-if');
const rename = require('gulp-rename');
const sass = require('gulp-sass');
const uglify = require('gulp-uglify');
const sourcemaps = require('gulp-sourcemaps');
// set variables
const env = process.env.NODE_ENV || 'production';
const srcDest = './assets/';
const srcPath = './src/';
/**
* SCSS task
*/
gulp.task('css', function () {
if (env === 'production') {
gulp.src(srcPath + 'scss/**/*.scss.liquid')
.pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError))
.pipe(autoprefixer({ cascade : false }))
.pipe(rename('theme.scss.liquid'))
.pipe(gulp.dest(srcDest));
} else {
// splitted css when more than one person is working on the project. Example of css name: <name-of-the-person>.chunk.scss
gulp.src(srcPath + 'scss/**/*.chunk.scss')
.pipe(gulpif(env !== 'production', sourcemaps.init()))
.pipe(sass({ outputStyle: 'compact' }).on('error', sass.logError))
.pipe(autoprefixer({ cascade : false }))
.pipe(gulpif(env !== 'production', sourcemaps.write()))
.pipe(rename(function (path) {
path.basename = `style-${path.basename.replace('.chunk', '')}.scss`;
path.extname = '.liquid';
}))
.pipe(gulp.dest(srcDest));
}
});
/**
* JS task
*/
gulp.task('js', function () {
return gulp.src(srcPath + 'js/**/*.chunk.js')
.pipe(babel({
presets: ['es2015']
}))
// if on production concat all chunks together to create a unique file
.pipe(gulpif(env === 'production', concat('theme.js')))
.pipe(gulp.dest(srcDest))
.pipe(gulpif(env === 'production', uglify()))
.pipe(gulp.dest(srcDest));
});
/**
* Watch task
*/
gulp.task('watch', function () {
gulp.watch([srcPath + 'scss/**/*.scss', srcPath + 'scss/**/*.scss.liquid'], ['css']);
gulp.watch(srcPath + 'js/*.js', ['js']);
});
As you can see, for this to work, we will need to create a src
folder into the root and inside of it two folders: scss
and js
.
src
|_ scss
|_ js
For scss files the pattern name is <name>.chunk.scss
for every chunk of style you want to be able to edit without overwriting. In our case, we chose to create them one for person.
Inside of your own chunk you will import the partial scss you need (starting with _, ie. _header.scss
).
Every scss chunks will be compiled into assets as style-<name>.scss.liquid
.
For the production style, we’ll create instead a style.scss.liquid
file on which we include every scss partial (excluding our single chunk, of course).
Similar to scss, the chunks of js file will be created inside src/js
folder using this name pattern <name>.chunk.js
and will be output with the same name.
Here’s our src folder situation at this point:
src
|_ scss
|_header.scss
|_...
|_name1.chunk.scss
|_name2.chunk.scss
|_style.scss.liquid
|_ js
|_name1.chunk.js
|_name2.chunk.js
When running gulp watch
in dev mode these are the operations that will be performed:
- All scss chunks will be compiled individually, by adding autoprefixer, sourcemaps and save to root
assets
folder - All js chunks will be compiled individually, non uglified and save to root
assets
folder
While these are the operations performed when gulp watch
is run in production mode:
- Only *.scss.liquid files are compiled individually, together with the partials they’re importing, minified, no sourcemap added, and save to root
assets
folder - Only *.chunk.js files are compiled together and combined into one file called
theme.js
, transpiled with babel, uglified, and save to rootassets
folder
At this point we have to face a problem with the compiler: using the Shopify setting variables in the Shopify format {{ variable }}
is not allowed both in the scss than in the js.
And while in css we can easily arrange this by creating a *.scss.liquid file into the assets folder writing a list of css variables joined with the Shopify variables syntax or simply creating a css that styles the elements of the website that need to be configured via these variables, in js we have to make a trick.
So, I got around the problem by creating a variable.js.liquid
file (remember that liquid file can access and parse Shopify variables) inside the assets folder and add it to our <head>
before every js file inclusion:
<script src="{{ 'variables.js' | asset_url }}" defer="defer"></script>
Inside this file here’s what we’ll write:
window.Shopify = window.Shopify || {};
Shopify.settings = {{ settings | json}};
Shopify.trans = {
productsProductAdd_to_cart: "{{ 'products.product.add_to_cart' | t }}",
productsProductSold_out: "{{ 'products.product.sold_out' | t }}",
};
We’re assigning to the Shopify.setting the entire json of the config and to Shopify.trans all the translations we will need (we are importing them individually as in our js we won’t ever need all the translations strings).
Done this, we can now call the theme settings inside our js:
Shopify.trans.productsProductAdd_to_cart
Let’s now install cross-env with npm install cross-env --save-dev
and modify our package.json adding two different commands to run the compilation for development and production:
"scripts": {
"dev": "cross-env NODE_ENV=development gulp watch",
"build": "cross-env NODE_ENV=production gulp watch"
},
Finally, we’ll need to include these assets into our website.
Inside layout/theme.liquid
we add these lines:
{% if settings.enable_production %}
{{ 'theme.scss' | asset_url | stylesheet_tag }}
{% else %}
{{ 'style-name1.scss' | asset_url | stylesheet_tag }}
{{ 'style-name2.scss' | asset_url | stylesheet_tag }}
{% endif %}
{% if settings.enable_production %}
<script src="{{ 'theme.js' | asset_url }}" defer="defer"></script>
{% else %}
<script src="{{ 'name1.chunk.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'name2.chunk.js' | asset_url }}" defer="defer"></script>
{% endif %}
You can see that the inclusion or one file or another depends on a variable called settings.enable_production
.
This is a theme-level setting that we are now going to implement.
Open your config/settings_schema.json
and add this to your json:
{
"name": "Developer",
"settings": [
{
"type": "checkbox",
"id": "enable_production",
"label": "Enable production mode",
"default": true,
"info": "This will load production assets file instead of development one."
}
]
}
And here we are!
While developing you will keep the enable_production
setting to false and run npm run dev
to use your chunked files, while before going on production you will run npm run build
and set enable_production
to true.
Post of
Lorena Ramonda☝ Ti piace quello che facciamo? Unisciti a noi!