Introducing Sass Modules

Hello, Habr! I present to you the translation of the article “Introducing Sass Modules” by Miriam Suzanne.



Recently, a feature has appeared in Sass that is familiar to you in other languages: a modular system . This is a big step forward for @import



, one of the most commonly used functions in Sass. Despite the fact that the existing @import



directive allows you to connect third-party packages and separate your styles into supported elements, it still has several limitations :





The authors of Sass packages (like me) tried to get around namespace problems by manually setting prefixes for variables and functions - but Sass modules are a much more powerful solution. In short, @import



is @import



replaced by more explicit @use



and @forward



. Over the next few years, @import



in Sass will be deprecated and then removed. You can still use CSS Import



's
, but they will not be compiled by Sass. But don’t worry, there is a migration tool that will help you upgrade.



Import files using @use





 @use 'buttons';
      
      





The new @use



is similar to @import



, but it has some notable differences:





When we attach the file via @use



, Sass automatically generates a namespace based on the file name.



 @use 'buttons'; /*    `buttons`*/ @use 'forms'; /*    `forms`*/
      
      





Now we have access to the members of both the buttons.scss



file and the forms.scss



file, but this access is not transferred between imports: forms.scss



still does not have access to the variables defined in buttons.scss



. Since imported entities have a namespace, we must use the new dot-delimited syntax to access them:



 /* : <namespace>.$variable */ $btn-color: buttons.$color; $form-border: forms.$input-border; /* : <namespace>.function() */ $btn-background: buttons.background(); $form-border: forms.border(); /* : @include <namespace>.mixin() */ @include buttons.submit(); @include forms.input();
      
      





We can change or remove the default namespace by adding as <name>



to the import.



 @use 'buttons' as *; /*      */ @use 'forms' as 'f'; $btn-color: $color; /* buttons.$color    */ $form-border: f.$input-border; /* forms.$input-border    */
      
      





Using as *



adds the module to the root namespace, so the prefix is ​​not needed, but its members are still locally limited by the current document.



Import Sass Embedded Modules



The internal capabilities in Sass have also been moved to a modular system, so we have full control over the global namespace. There are several built-in modules - math



, color



, string



, list



, map



, selector



and meta



- which must be imported into the file explicitly before use.



 @use 'sass:math'; $half: math.percentage(1/2);
      
      





Embedded modules can also be imported into global space:



 @use 'sass:math' as *; $half: percentage(1/2);
      
      





Built-in functions that already have prefix names, such as map-get



or str-index



, can be used without duplicating this prefix:



 @use 'sass:map'; @use 'sass:string'; $map-get: map.get(('key': 'value'), 'key'); $str-index: string.index('string', 'i');
      
      





You can find a complete list of built-in modules, functions, and name changes in the Sass module specification .



New and changed core features



As an added benefit, this means that Sass can safely add new internal mixins and functions without causing name conflicts. The most amazing example is the load-css



sass:meta



from the sass:meta



module. It works by analogy with @use



, but only returns the generated CSS and works dynamically anywhere in your code:



 @use 'sass:meta'; $theme-name: 'dark'; [data-theme='#{$theme-name}'] { @include meta.load-css($theme-name); }
      
      





The first argument is the module URL (as in @use



), but it can be changed dynamically using a variable, even using interpolation, for example theme-#{$name}



. The second (optional) argument takes a map



structure with the configuration:



 /*   $base-color  'theme/dark'   */ @include meta.load-css( 'theme/dark', $with: ('base-color': rebeccapurple) );
      
      





The $with



argument allows you to configure any variable in the loaded module using the map



structure, and this variable must satisfy the conditions:





 /* theme/_dark.scss */ $base-color: black !default; /*    */ $_private: true !default; /*        */ $config: false; /*    ,      !default */
      
      





Note that the key 'base-color'



sets the variable $base-color



.



There are a couple of new functions from the sass:meta



module sass:meta



: module-variables()



and module-functions()



. Each of them returns a map



structure from names and values ​​from an already imported module. They take one argument corresponding to the module namespace:



 @use 'forms'; $form-vars: module-variables('forms'); /* ( button-color: blue, input-border: thin, ) */ $form-functions: module-functions('forms'); /* ( background: get-function('background'), border: get-function('border'), ) */
      
      





Several other functions from sass:meta



- global-variable-exists()



, function-exists()



, mixin-exists()



, and get-function()



- will receive additional $module



arguments that allow us to explicitly check each namespace.



Adjust and scale colors



The sass:color



module also has some interesting caveats about resolving some of our old problems. Many of the legacy functions like lighten()



or adjust-hue()



no longer recommended for use in favor of the explicit functions color.adjust()



and color.scale()



:



 /*  lighten(red, 20%) */ $light-red: color.adjust(red, $lightness: 20%); /*  adjust-hue(red, 180deg) */ $complement: color.adjust(red, $hue: 180deg);
      
      







Some of these deprecated functions (e.g. adjust-hue



) are redundant and unnecessary. Others - such as lighten



, darken



, saturate



, etc. - need re-implementation to improve internal logic. The original functions were based on adjust()



, which uses linear math: adding 20%



to the current lightness of red



in our example above. In most cases, we want to change ( scale()



) the color by a certain percentage relative to the current value:



 /*        20,   0.2,     */ $light-red: color.scale(red, $lightness: 20%);
      
      





After being completely deprecated and removed, these functions will eventually reappear in sass:color



with a new behavior based on color.scale()



rather than color.adjust()



. This will happen gradually to avoid sudden backward compatibility issues. In the meantime, I recommend manually checking your code to see where color.scale()



might be more useful.



Configure Imported Libraries



Third-party or reusable libraries often come with variables with some default values ​​that you can override. We did this with variables before import:



 /* _buttons.scss */ $color: blue !default; /* old.scss */ $color: red; @import 'buttons';
      
      





Since when using modules there is no longer access to local variables, we need a new way to set values. We can do this by passing the settings via map



to @use



:



 @use 'buttons' with ( $color: red, $style: 'flat', );
      
      





This is similar to the $with



argument in load-css()



, but instead of using variable names as keys, we use the variables themselves with the $



symbol.



I like how explicit the setting has become, but there is one rule that has baffled me several times: a module can only be configured once upon first use . The connection order has always been important for Sass, even with @import



, but these problems went unnoticed. Now we get a clear error, and this is both good and a bit unexpected. Make sure you connect the libraries via @use



and configure them in the entry-file (the central document that imports all other files) so that these settings are compiled before other library connections via @use



.



It is not possible (at the moment) to “link” the configurations together, keeping them editable, but you can wrap the configured module and transfer it as a new module.



Transferring files with @forward





We do not always need to use the file and refer to its members. Sometimes we just want to pass it on to a subsequent import. Suppose we have several files associated with forms, and we want to connect them all together as one namespace. We can do this with @forward



:



 /* forms/_index.scss */ @forward 'input'; @forward 'textarea'; @forward 'select'; @forward 'buttons';
      
      





Members of such forwarded files are not available in the current document and no namespace is created, but these variables, functions and mixins will be available when another file connects them via @use



or @use



entire collection through @forward



. If the submitted individual files contain actual CSS, it will also be transmitted without directly generating it until the package itself is used. At this stage, all this will be considered as one module with one namespace:



 /* styles.scss */ @use 'forms'; /*        `forms` */
      
      





Note : If you ask Sass to attach a folder, it will look for the index



or _index



file in it.



By default, all public members will be forwarded along with the module. But we can be more selective by using the show



and hide



conditions and specifying specific members that we want to add or exclude.



 /*    `border()`   `$border-color`   `input` */ @forward 'input' show border, $border-color; /*     `buttons`    `gradient()` */ @forward 'buttons' hide gradient;
      
      





Note : when functions and mixins have a common name, they are added and hidden also together.



To clarify the sources or avoid conflicts of names of forwarded modules, we can add prefixes to the members of the connected file using as



:



 /* forms/_index.scss */ /* @forward "<url>" as <prefix>-*; */ /* ,      `background()` */ @forward 'input' as input-*; @forward 'buttons' as btn-*; /* style.scss */ @use 'forms'; @include forms.input-background(); @include forms.btn-background();
      
      





And, if we need, we can always use via @use



and @forward



the same module through @forward



, adding both rules:



 @forward 'forms'; @use 'forms';
      
      





This is especially useful if you want to pre-configure the library or add additional tools before transferring it to other files. This can help simplify connection paths:



 /* _tools.scss */ /*        */ @use 'accoutrement/sass/tools' with ( $font-path: '../fonts/', ); /*    */ @forward 'accoutrement/sass/tools'; /* -  ... */ /* _anywhere-else.scss */ /*      */ @use 'tools';
      
      





Both @use



and @forward



must be declared at the root of the document (not nested) and at the beginning of the file. Only @charset



and simple variable definitions can appear before import directives.



Transition to a modular system



To test the new syntax, I created a new Sass open source library ( Cascading Color Systems ) and a new site for my group - both still under development. I needed to understand the modules from the point of view of the author of the library and from the point of view of the site developer. Let's start with the experience of the "end user" in writing site styles using module syntax ...



Support and writing styles



Using the modules on the site was enjoyable. The new syntax supports the code architecture that I already use. All my imports of global settings and tools are in the same directory (I call it config



) with an index file that transfers everything I need:



 /* config/_index.scss */ @forward 'tools'; @forward 'fonts'; @forward 'scale'; @forward 'colors';
      
      





By developing other parts of the site, I can import these tools and configurations wherever I need them:



 /* layout/_banner.scss */ @use '../config'; .page-title { @include config.font-family('header'); }
      
      





It even works along with my existing libraries, such as Accoutrement and Herman , which still use the old @import



syntax. Since the @import



rule @import



not be replaced everywhere at once, Sass developers have given some time for the transition. Modules are available now, but @import



will not @import



another year or two - and will be removed from the language only a year after that. At the same time, the two systems will work together in any way:





This means that you can immediately start using the new module syntax, without waiting for the release of a new version of your favorite libraries: and I can spend some time updating all my libraries!



Migration tool



The upgrade will not take long if we use the migration tool created by Jennifer Thakar. It can be installed using NPM, Chocolatey or Homebrew:



 npm install -g sass-migrator choco install sass-migrator brew install sass/sass/migrator
      
      





This is not a one-time tool for migrating to modules. Now that Sass is back in active development (see below), the migration tool will also receive regular updates to help port each new feature. It is a good idea to install this tool globally, and save it for future use.



The migrator can be launched from the command line and, hopefully, will be added to third-party applications such as CodeKit and Scout. Point him to a single Sass file, for example style.scss



and tell him which migrations to apply. At the moment, there is only one migration called module



:



 # sass-migrator <migration> <entrypoint.scss...> sass-migrator module style.scss
      
      





By default, the migrator updates only one file, but in most cases we want to update the main file and all its dependencies: any elements connected via @import



, @forward



or @use



. We can do this by specifying each file individually or simply by adding the --migrate-deps



flag.



 sass-migrator --migrate-deps module style.scss
      
      





For a test run, we can add --dry-run --verbose



(or -nv



in abbreviated form) and look at the results without changing the source files. There are a number of other options that we can use to configure migration — there is even one specifically designed to help library authors remove old manually created namespaces — but I won’t describe all of them here. The migration tool is fully documented on the Sass website .



Updating Published Libraries



I encountered several problems on the library side, in particular when I tried to make custom configurations available for several files and find a solution for missing “chain” configurations. Order-related errors can be difficult to debug, but the results are worth the effort, and I think we will see some additional fixes soon. I still need to experiment with the migration tool on complex packages and perhaps write an additional article for library authors.



The important thing to know right now is that Sass provided us with protection during the transition. Not only can the old imports and modules work together, we can create “ import-only ” files to provide more convenient work for users who still connect our libraries via @import



. In most cases, this will be an alternative version of the main package file, and you want them to be near: <name>.scss



for module users and <name>.import.scss



for old users. Each time the user calls @import <name>



, he loads the .import



file:



 /*  `_forms.scss` */ @use 'forms'; /*  `_forms.import.scss` */ @import 'forms';
      
      







This is especially useful for adding prefixes for developers who do not use modules:



 /* _forms.import.scss */ /*       */ @forward 'forms' as forms-*;
      
      







Sass Update



You may remember that Sass froze the addition of new functions several years ago, so that its various implementations (LibSass, Node Sass, Dart Sass) catch up with the original Ruby implementation, in order to completely abandon it . The freeze ended last year with several new features and active discussions and development on GitHub - but not so solemnly. If you missed these releases, then you can read the Sass blog :





Currently, Dart Sass is a canonical implementation and is usually the first to introduce new features. I recommend switching to it if you want to receive all the latest. You can install Dart Sass using NPM, Chocolatey or Homebrew. It also works great with gulp-sass .



Like CSS (starting with CSS3), there is no longer a single version number for new releases. All Sass implementations work with the same specification, but each of them has a unique release schedule and numbering, which is reflected in the support information in beautiful new documentation designed by Jina .



image



Sass modules are available from October 1, 2019 in Dart Sass 1.23.0 .



All Articles