Sitecore SXA: Turning on CSS Source Map Files

Anton Tishchenko
Anton Tishchenko
Cover Image for Sitecore SXA: Turning on CSS Source Map Files

Update from 26 November 2021: If you use Sitecore 10.2 version, please use out of the box configuration. Or you can consider use @sca/celt 10.2 for your SXA theme for lower Sitecore versions as well.

CSS source map files are the cornerstone thing for modern web development. Sitecore SXA has the ability to turn on the usage of CSS map files. But there are use cases when you could not turn on SXA source maps out of the box. Let's examine why do you need to have source map files and how to configure using them for different cases in the Sitecore experience accelerator.

Theory: CSS Source Map Files

If you are familiar with CSS source map files, you can skip this chapter and scroll to the next one. It is present here because there will be parts that rely on source map specification in further explanation.

Source maps were introduced a long time ago. It was always easier for developers to save files with proper formatting. But proper formatting takes precious bytes. Each space, tab, line end character, clear variable name make your file bigger. And each additional byte makes page speed slower and users' experience worse. And developers found a way how to solve this problem. You can post-process your source and get rid of everything that doesn't affect styles and JS code execution. That is how minification was introduced. But minification created another problem. You made your page fast, but you also made troubleshooting the frontend much harder. At this point source files mapping appeared. The idea was pretty simple, we can provide a map from minified file to its real sources. And when you open sources in your developer's toolbar you will see a reference to your source file and it will allow you to save pricey seconds during troubleshooting of issues.

Web development evolved and new meta-languages and approaches appeared. SASS, SCSS, Babel, CoffeeScript, TypeScript opened new abilities for developers and made development faster and more comfortable. But browsers didn't evolve fast. And you still can use only plain CSS and JS for your pages. (Fortunately new languages specifications, but still only this approaches). And source map files became even more valuable as you write code in one meta-language, but browsers use a different one. Because browser can pretty print page sources, but it has no idea how file was written initially.

We used that source map has somename.css.map file name. But that is actually not a standard. It is a common location, where the source map is placed. But it could be configured! We can specify sourceMappingURL parameter at the end of .css file.

We can use different sourceMappingURL parameter values:

  • Absolute link to source map file
  • Relative link to source map file
  • Data URL: base64 encoded source map file. sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW.....

Another option, how to specify the source map file is setting X-SourceMap (or SourceMap in the newer specification) header. It will require changes in server-side code or configuring rewrite rules, which will make our solution more complex.

Adding CSS Source Map when Minification is Disabled

SXA Creative Live Exchange allows working in different modes. One of the options is to upload all produced .css files to Sitecore Media Library. (When you have enableMinification: true and disableSourceUploading: true settings in your config.js file.

In this case, everything is easy. You need to open your config.js file and change cssSourceMap and sassSourceMap values to true. In this case, SXA gulp scripts will add source maps to the end of each .css file of your theme.

Challenges of Adding CSS Source Map when Minification is Enabled

When minification is enabled, SXA gulp scripts prepare pre-optimized-min.css file, which is uploaded to Sitecore Media Library and then served as a styles file for your SXA theme.

With enabled cssSourceMap and sassSourceMap values, SXA gulp scripts will start to create pre-optimized-min.css.map source map file. But there is no watcher for this type of file. Also, we will need to have item pre-optimized-min.css with .map extension in Sitecore Media Library. Or pre-optimized-min item with .css.map extension. But Sitecore doesn't accept dots in item names by default. And we will not be able to use .css.map extension because in this case we will need to have 2 Sitecore items in the same level, which is also not allowed. But from the theory part, we know that we can add source map directly inside .css or configure different source map file name. Let's do it!

Placing Source Map inside pre-optimized-min.css

If we will look at how minification is done in SXA gulp scripts we will notice that there it uses gulp-sourcemaps npm package. And this package allows the flexible configuring location of source maps.

All that we need is to open \node_modules\@sxa\celt\util\cssMinificator.js file, which is responsible for concatenation and minification of .css files, and patch it. (Of course, it is better to copy this file to your source control and modify it separately. I am writing about changing in this file only for simplification). We need to replace gulpSourcemaps.write('.\') on gulpSourcemaps.write() . Changing of argument will write source map inside pre-optimized-min.css. It is a quick and easy change. But it could be used only for development environments, but not for production. Because it will double the size of css file and we will lose all improvements added by minification.

Proper Way of Adding CSS Source Map File to Your SXA Theme

We have already reviewed few options, how we can add source map files. But that options don't work for production. It means that you will need to have different approaches for different environments. It is not good. Let's figure out how to do it in a long way, but properly.

First of all, similar to other approaches, we need to set cssSourceMap and sassSourceMap values to true in config.js file.

To avoid problems with dots in item names and the same filenames on the same level, let's name our source maps filename pre-optimize-min-css.map instead of pre-optimized-min.css.map . This small change will make our work easier. We will not need to have any changes in the Sitecore backend or configuration. To achieve it, let's copy cssMinificator.js file and make small change:

// tasks\override\cssMinificator.js
// File is based on \node_modules\@sxa\celt\util\cssMinificator.js
// It allows to use different name for map file
// For our case we need [file]-css.map instead [file].css.map

const gulp = require('gulp');
const cleanCSS = require('gulp-clean-css');
const path = require('path');
const gulpSourcemaps = require('gulp-sourcemaps');
const gulpif = require('gulp-if');
const gulpConcat = require('gulp-concat');
const config = require(path.resolve(process.cwd(), './gulp/config'));

module.exports = function (cb) {
    let conf = config.css;
    if (!conf.enableMinification) {
        if (process.env.debug == 'true') {
            console.log('CSS minification is disabled by enableMinification flag'.red)
        }
        return cb();
    }
    let streamSource = conf.minificationPath.concat(['!' + conf.cssOptimiserFilePath + conf.cssOptimiserFileName])
    let stream = gulp.src(streamSource)
        .pipe(gulpif(function () {
            return conf.cssSourceMap;
        }, gulpSourcemaps.init()))
        .pipe(cleanCSS(config.minifyOptions.css))
        .pipe(gulpConcat(conf.cssOptimiserFileName))
        //Changed part: we use different filename suffix for Sitecore compatibility
        .pipe(gulpif(function () {
            return conf.cssSourceMap;
        }, gulpSourcemaps.write('./', {
            mapFile: function (mapFilePath) {
                // source map files are named *-css.map instead of *.css.map
                return mapFilePath.replace('.css.map', '-css.map');
            }
        })))
        //Disable previous pipe and enable this one if you want to add CSS source maps to the same file
        //.pipe(gulpif(function () {
        //  return conf.cssSourceMap;
        //}, gulpSourcemaps.write()))
        .pipe(gulp.dest('styles'));
    stream.on('end', function () {
        console.log('Css minification done'.grey)
    });
    return stream

}

watchCSS built-in task has a dependency on cssMinificator.js. That is why we need also to override it and use our new minification script.

// tasks\override\watchCss.js
// File is based on \node_modules\@sxa\celt\util\watchCss.js
// We need to call different CSS optimizer with changed map file name

const gulp = require('gulp');
const colors = require('colors');
const vinyl = require('vinyl-file');
const config = require(global.rootPath + '/gulp/config');
//Changed relative path to absolute
const { fileActionResolver } = require('@sxa/celt/util/fileActionResolver');
//Changed path to overridden module
const cssMinificator = require('./cssMinificator');

module.exports = function watchCss() {
    setTimeout(function () {
        console.log('Watching CSS files started...'.green);
    }, 0);
    let conf = config.css,
        indervalId,
        watch = gulp.watch(conf.path, { queue: true });
    watch.on('all', function (event, path) {
        var file = {
            path: path
        };
        if (event !== 'unlink') {
            file = vinyl.readSync(path);
        }

        file.event = event;
        if (!conf.disableSourceUploading || file.path.indexOf(conf.cssOptimiserFileName) > -1) {
            fileActionResolver(file);
        } else {
            if (process.env.debug == 'true') {
                console.log(`Uploading ${file.basename} prevented because value disableSourceUploading:true`.yellow);
            }
        }
        if (conf.enableMinification && file.path.indexOf(conf.cssOptimiserFileName) == -1) {
            indervalId && clearTimeout(indervalId);
            indervalId = setTimeout(function () {
                indervalId = clearTimeout(indervalId);
                cssMinificator();
            }, 400)
        }
    })
}

The next step is adding watch and upload tasks for .map files:

// tasks\watchMap.js
const gulp = require('gulp');
const vinyl = require('vinyl-file');
const config = require(global.rootPath + '/gulp/config');
//Changed relative path to absolute
const { fileActionResolver } = require('@sxa/celt/util/fileActionResolver');

module.exports = function watchMap() {
    setTimeout(function () {
        console.log('Watching MAP files started...'.green);
    }, 0);
    let conf = config.map
        watch = gulp.watch(conf.path, { queue: true });
    watch.on('all', function (event, path) {
        var file = {
            path: path
        };
        if (event !== 'unlink') {
            file = vinyl.readSync(path);
        }

        file.event = event;
        fileActionResolver(file);
    })
}
// tasks\uploadMap.js
const gulp = require('gulp');
const tap = require('gulp-tap');
const config = require(global.rootPath + '/gulp/config');
//Changed relative path to absolute
const { fileActionResolver } = require('@sxa/celt/util/fileActionResolver');

module.exports = function uploadMap() {
    var conf = config.map;
    const promises = [];

    return gulp.src(conf.path)
        .pipe(tap(
            function (_file) {
                let file = _file;
                file.event = 'change';
                promises.push(() => fileActionResolver(file));
            })
        )
        .on('end', async () => {
            for (const prom of promises) {
                await prom();
            }
        })
}

These files need configuration, where to look for .map files. We configure it in the additional setting in config.js file:

// config.js

...
map: {
        path: ['styles/*.map']
    },
...

Now, let's bring it all together in gulpfile.js

// gulpfile.js

...
const watchCssTasks = require('./gulp/tasks/override/watchCss');
// Instead of:
// const watchCssTasks = getTask('watchCss');

...

const cssOptimizationTasks = require('./gulp/tasks/override/cssMinificator');
// Instead of:
// const cssOptimizationTasks = getTask('cssOptimization');

...

// New tasks for .map files
const watchMapTasks = require('./gulp/tasks/watchMap');
const uploadMapTasks = require('./gulp/tasks/uploadMap');
module.exports.watchMap = gulp.series(login, watchMapTasks);
module.exports.uploadMap = gulp.series(login, uploadMapTasks);

...

// Changed default task with added watchMapTasks
module.exports.default = module.exports.watchAll = gulp.series(login,
    gulp.parallel(
        watchHtmlTasks,
        watchCssTasks,
        watchJSTasks,
        watchESTasks,
        watchImgTasks,
        watchScribanTasks,
        watchSassTasks.watchStyles,
        watchSassTasks.watchBase,
        watchSassTasks.watchComponents,
        watchSassTasks.watchDependency,
        watchSassSourceTasks,
        watchMapTasks
    )
);

...

// Extended Build + upload tasks
module.exports.rebuildAll = gulp.series(
    login,
    jsOptimizationTasks, sassComponentsTasks, cssOptimizationTasks,
    uploadJsTasks, uploadCssTasks, uploadImgTasks, uploadMapTasks
)
module.exports.rebuildMain = gulp.series(
    login,
    jsOptimizationTasks, sassComponentsTasks, cssOptimizationTasks,
    uploadJsTasks, uploadCssTasks, uploadMapTasks
)

...

Voila! Now after running gulp buildAll or triggering watch task we get a proper link at the end of pre-optimize-min.css and pre-optimize-min-css.map file itself. And both these files are uploaded to the media library. Now when you troubleshoot any CSS issues using the developer tools, you see, where it is done in sources.

Conclusion

I hope that someone from the Sitecore SXA team will read my article and include the ability to upload CSS source map files for pre-optimize-min.css out of the box with SXA gulp scripts. It is easy to change, but it can save a lot of developer hours.

But even if not, you still have control over your sources and your project. And if you need something to be improved then everything is in your hands! No needs to wait when it will be implemented by someone else.