Shopify Frontend

Use Css Preprocessor and Babel on Shopify

7 minutes reading

Working 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 root assets 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