sass-site/source/blog/024-the-module-system-is-launched.html.md
Natalie Weizenbaum 837a475ddd Move the blog onto sass-lang.com
Logdown seems to be basically unmaintained, its servers are
unreliable, and it's been causing some mixed-content errors lately.
This moves all blog posts to sass-lang.com itself; I'll set up
redirects from the blog as best I can once this lands.

Closes #401
Closes #402
Closes #403
2019-11-12 18:21:51 -08:00

13 KiB

title author date
The Module System is Launched Natalie Weizenbaum 2019-10-01 18:58 PST

The Sass team has known for years that the @import rule, one of the earliest additions to Sass, wasn't as good as we wanted it. It caused a litany of problems for our users:

  • It was next to impossible to figure out where a given variable, mixin, or function (collectively called "members") was originally defined, since anything defined in one stylesheet was available to all stylesheets that were imported after it.

  • Even if you chose to explicitly import every stylesheet that defined members you used, you'd end up with duplicate CSS and strange side-effects, because stylesheets were reloaded from scratch every time they were imported.

  • It wasn't safe to use terse and simple names because there was always a possibility that some other stylesheet elsewhere in your application would use the same name and mess up your logic. To be safe users had to manually add long, awkward namespaces to everything they defined.

  • Library authors had no way to ensure that their private helpers wouldn't be accessed by downstream users, causing confusion and backwards-compatibility headaches.

  • The @extend rule could affect any selector anywhere in the stylesheet, not just those that its author explicitly chose to extend.

We also knew that any replacement we wanted to introduce would have to be designed and developed with the utmost care to ensure it would provide a rock-solid foundation for the future of Sass development. Over the past few years, we've discussed, designed, and developed a brand-new module system that solves these problems and more, and today we're excited to announce that it's available in Dart Sass 1.23.0.

Please note that the module system is fully backwards-compatible. No existing features have been removed or deprecated, and your current Sass stylesheets will keep working just as they always have. We designed the module system to be fully interoperable with @import to make it easy for stylesheet authors to migrate to it incrementally. We do plan to eventually get rid of @import, but not until long after everyone's had a chance to migrate.

@use, the Heart of the Module System

The @use rule is the primary replacement for @import: it makes CSS, variables, mixins, and functions from another stylesheet accessible in the current stylesheet. By default, variables, mixins, and functions are available in a namespace based on the basename of the URL.

@use "bootstrap";

.element {
  background-color: bootstrap.$body-bg;
  @include bootstrap.float-left;
}

In addition to namespacing, there are a few important differences between @use and @import:

  • @use only executes a stylesheet and includes its CSS once, no matter how many times that stylesheet is used.
  • @use only makes names available in the current stylesheet, as opposed to globally.
  • Members whose names begin with - or _ are private to the current stylesheet with @use.
  • If a stylesheet includes @extend, that extension is only applied to stylesheets it imports, not stylesheets that import it.

Note that placeholder selectors are not namespaced, but they do respect privacy.

Controlling Namespaces

Although a @use rule's default namespace is determined by the basename of its URL, it can also be set explicitly using as.

@use "bootstrap" as b;

.element {
  @include b.float-left;
}

The special construct as * can also be used to include everything in the top-level namespace. Note that if multiple modules expose members with the same name and are used with as *, Sass will produce an error.

@use "bootstrap" as *;

.element {
  @include float-left;
}

Configuring Libraries

With @import, libraries are often configured by setting global variables that override !default variables defined by those libraries. Because variables are no longer global with @use, it supports a more explicit way of configuring libraries: the with clause.

// bootstrap.scss
$paragraph-margin-bottom: 1rem !default;

p {
  margin-top: 0;
  margin-bottom: $paragraph-margin-bottom;
}
@use "bootstrap" with (
  $paragraph-margin-bottom: 1.2rem
);

This sets bootstrap's $paragraph-margin-bottom variable to 1.2rem before evaluating it. The with clause only allows variables defined in (or forwarded by) the module being imported, and only if they're defined with !default, so users are protected against typos.

@forward, for Library Authors

The @forward rule includes another module's variables, mixins, and functions as part of the API exposed by the current module, without making them visible to code within the current module. It allows library authors to be able to split up their library among many different source files without sacrificing locality within those files. Unlike @use, forward doesn't add any namespaces to names.

// bootstrap.scss
@forward "functions";
@forward "variables";
@forward "mixins";

Visibility Controls

A @forward rule can choose to show only specific names:

@forward "functions" show color-yiq;

It can also hide names that are intended to be library-private:

@forward "functions" hide assert-ascending;

Extra Prefixing

If you forward a child module through an all-in-one module, you may want to add some manual namespacing to that module. You can do what with the as clause, which adds a prefix to every member name that's forwarded:

// material/_index.scss
@forward "theme" as theme-*;

This way users can use the all-in-one module with well-scoped names for theme variables:

@use "material" with ($theme-primary: blue);

or they can use the child module with simpler names:

@use "material/theme" with ($primary: blue);

Built-In Modules

The new module system also adds built-in modules (sass:math, sass:color, sass:string, sass:list, sass:map, sass:selector, and sass:meta) to hold all the existing built-in Sass functions. Because these modules will (typically) be imported with a namespace, it's now much easier to use Sass functions without running into conflicts with plain CSS functions.

This in turn makes it much safer for Sass to add new functions. We expect to add a number of convenience functions to these modules in the future.

Renamed Functions

Some functions have different names in the built-in modules than they did as global functions. Built-in functions that already had manual namespaces, like map-get(), have those namespaces removed in the built-in modules so you can just write map.get(). Similarly, adjust-color(), scale-color(), and change-color() are now color.adjust(), color.scale(), and color.change().

We've also taken this opportunity to change a couple confusing old function names. unitless() is now math.is-unitless(), and comparable() is now math.compatible().

Removed Functions

Sass's shorthand color functions lighten(), darken(), saturate(), desaturate(), opacify(), fade-in(), transparentize(), and fade-out() all had very unintuitive behavior. Rather than scaling their associated attributes fluidly, they just incremented them by a static amount, so that lighten($color, 20%) would return white for a color with 85% lightness rather than returning a color with 88% lightness (20% closer to full white).

To help set us on the path towards fixing this, these functions (along with adjust-hue()) aren't included in the new built-in modules. You can still get the same effect by calling color.adjust()—for example, lighten($color, $amount) is equivalent to color.adjust($color, $lightness: $amount)—but we recommend trying to use color.scale() instead if possible because of how much more intuitive it is.

At some point in the future, we plan to add color.lighten() and similar functions as shorthands for color.scale().

meta.load-css()

The new module system comes with a new built-in mixin, meta.load-css($url, $with: ()). This mixin dynamically loads the module with the given URL and includes its CSS (although its functions, variables, and mixins are not made available). This is a replacement for nested imports, and it helps address some use-cases of dynamic imports without many of the problems that would arise if new members could be loaded dynamically.

@import Compatibility

The Sass ecosystem won't switch to @use overnight, so in the meantime it needs to interoperate well with @import. This is supported in both directions:

  • When a file that contains @imports is @used, everything in its global namespace is treated as a single module. This module's members are then referred to using its namespace as normal.

  • When a file that contains @uses is @imported, everything in its public API is added to the importing stylesheet's global scope. This allows a library to control what specific names it exports, even for users who @import it rather than @use it.

In order to allow libraries to maintain their existing @import-oriented API, with explicit namespacing where necessary, this proposal also adds support for files that are only visible to @import, not to @use. They're written "file.import.scss", and imported when the user writes @import "file".

Automatic Migration

Concurrent with the launch of the new module system, we're launching a new automated Sass migrator. This tool makes it easy to migrate most stylesheets to use the new module system automatically. Follow the instructions on the Sass website to install it, then run it on your application:

$ sass-migrator module --migrate-deps <path/to/style.scss>

The --migrate-deps flag tells the migrator to migrate not only the file you pass, but anything it imports as well. The migrator will automatically pick up files imported through Webpack's node_modules syntax, but you can also pass explicit load paths with the --load-path flag.

If you want the migrator to tell you what changes it would make without actually making them, pass both the --dry-run flag and the --verbose flag to tell it to just print out the changes it would make without saving them to disk.

Migrating a Library

If you want to migrate a Sass library that's meant for downstream users to load and use, run:

$ sass-migrator module --migrate-deps --forward=all <path/to/index.scss>

The --forward flag tells the migrator to add @forward rules so that users can still load all the mixins, variables, and functions your library defines with a single @use.

If you added a manual namespace to your library to avoid name conflicts, the migrator will remove it for you if you pass the --remove-prefix flag. You can even choose to only forward members that originally had that prefix by passing --forward=prefixed.

Filing Issues

The migration tool is brand new, so it may still have some rough edges. If you run into any problems, please don't hesitate to file an issue on GitHub!

Try It Now!

The module system is available as part of Dart Sass 1.23.0. You can install it right now using:

$ npm install -g sass

Alternately, check out the installation page for all the different ways it can be installed!

Future Plans

The Sass team wants to allow for a large amount of time when @use and @import can coexist, to help the ecosystem smoothly migrate to the new system. However, doing away with @import entirely is the ultimate goal for simplicity, performance, and CSS compatibility. As such, we plan to gradually turn down support for @import on the following timeline:

  • One year after both Dart Sass and LibSass have launched support for the module system or two years after Dart Sass launches support for the module system, whichever comes sooner (1 October 2021 at latest), we will deprecate @import as well as global core library function calls that could be made through modules.

  • One year after this deprecation goes into effect (1 October 2022 at latest), we will drop support for @import and most global functions entirely. This will involve a major version release for all implementations.

This means that there will be at least two full years when @import and @use are both usable at once, and likely closer to three years in practice.