Sass dynamic import based on environment

Viewed 5431

I'm trying to switch a sass import URL based on environment. I've reviewed many other questions and answers and this has been fighting me every step of the way. @import does not allow for dynamic strings so instead I'm attempting to use a mixin for each string and @if/@else based on an environment variable however I cannot access the root environment variable from the other sass file.

webpack.config.js

const scssRule = config.module.rules.find(rule => rule.test.test(".scss"));
const scssLoaderConfig = scssRule.use.find(({loader}) => loader === "sass-loader");
scssLoaderConfig.options = scssLoaderConfig.options || {};
scssLoaderConfig.options["implementation"] = require("sass"); //dart-sass
scssLoaderConfig.options["additionalData"] = `$env:"${env}";`; //Pass env to sass

theme.scss

//NOTE: This $env line is not actually in the file. It gets injected by webpack via sass-loader additionalData
$env: "preprod";

@forward "icons" as icon_*;
//More @forward directives such as colors

_icons.scss

@mixin importIconsPreprod {
    @import "https://preprod.example.com/icons.css";
}
@mixin importIconsProd {
    @import "https://example.com/icons.css";
}

@mixin importIcons() {
    //SassError: Undefined variable.
    @if $env == "preprod" {
        @include importIconsPreprod;
    } @else {
        @include importIconsProd;
    }
    
    //More styles here
}

Desired Implementation

@use "theme-package" as theme;

//Import icons from theme.icon namespace
@include theme.icon_importIcons;
2 Answers

After some time I found a solution. I ended up generating a file _env.scss as part of webpack. This file contains the $env sass variable indicating the environment and gets loaded by _icons.scss. Code below:

webpack.config.js

const writeEnvScss = require("./scripts/write-env-scss.js");

//Write _env.scss, default to "prod"
writeEnvScss("./packages/Theme/src/Component/includes/_env.scss", "prod");

_env.scss generated as part of build

$env:"preprod";

_icons.scss

//"env" file is created as part of webpack.config.js
@use "includes/env" as *;

@mixin importIconsPreprod {
    @import "https://preprod.example.com/icons.css";
}
@mixin importIconsProd {
    @import "https://example.com/icons.css";
}

@mixin importIcons() {
    @if $env == "preprod" {
        @include importIconsPreprod;
    } @else {
        @include importIconsProd;
    }
}

write-env-scss.js

const fs = require("fs");
const path = require("path");

//looks for optional --env cli param or NODE_ENV environment variable
module.exports = function(scssPath, envDefault) {
    scssPath = scssPath || null;
    if (!scssPath) {
        throw new Error("scss path required");
    }
    envDefault = envDefault || "local";

    //Determine env
    let env = getArg("--env") || process.env.NODE_ENV || null;
    switch (env) {
        case "development":
        case "local":
            env = "local";
            break;
        case "preprod":
            break;
        case "production":
        case "prod":
            env = "prod";
            break;
        default:
            env = envDefault;
            break;
    }
    console.log("ENV =>", env);

    //Write _env.scss to Theme
    fs.writeFileSync(path.resolve(scssPath), `$env:"${env}";`);
};

function getArg(key) {
    var index = process.argv.indexOf(key);
    var next = process.argv[index + 1];
    return index < 0 ? null : !next || next[0] === "-" ? true : next;
}

In my project (cross-browser extension) I am using includePaths option from node-sass config to achieve something similar.

In general, includePaths extends the range of folders from which scss modules can be imported in order to be compiled correctly. Since it's a js API, you can set this path dynamically via a variable.

Here is an example of how I am doing it (I am using gulp and gulp-sass, but it works the same way with webpack. Examples are simplified and without node's "path"):

  • Folder structure:
styles
|-- firefox
|   |-- _vars.scss
|
|-- chrome
|   |-- _vars.scss
|
|-- styles.scss // main file
|-- gulpfile.js // builder
|-- build
  • styles.scss:
@import "vars";
  • gulpfile.js
const { src, dest } = require('gulp');
const sass = require('gulp-sass');

function styles(browser){
  const srcPath = "*.scss";
  const destPath = "build";

  return src(srcPath)
           .pipe(sass({ includePaths: browser }))
           .pipe(dest(destPath))
}

gulp.default = function(){
  const varDirname = "firefox"; // or "chrome"

  styles(varDirname);
}

Note that it is important that in includePaths you only include directory name in which the files are located, not a glob like browser/*.scss.

Related