Request For Comments: Module System

Posted 27 November 2018 by Natalie Weizenbaum

Many of the most frequently-requested features for Sass have to do with its imports. The import system that we’ve had since the very early releases of Sass is, to put it simply, not great. It does little more than textually include one Sass file in another, which makes it hard to keep track of where mixins, functions, and variables were defined and hard to be sure that any new additions won’t happen to conflict with something elsewhere in the project. To make matters worse, it overlaps with CSS’s built-in @import rule, which forces us to have a bunch of heuristics to decide which is which.

Because of these problems and others, we’ve wanted to do a full overhaul of the way Sass files relate to one another for a long time. Over the last few years, I’ve been working with the Sass core team and Sass framework maintainers to create a proposal for a module system that’s fit to replace @import. That proposal is now in a place that the core team is pretty happy with, at least as a starting point, so we want to open it up for community feedback.

If you want to read the full proposal, it’s available on GitHub. Feel free to file issues for any feedback you have. The main body of the proposal is written as a spec, so it’s very detailed, but the Goals, Summary, and FAQ sections (reproduced below) should be accessible to anyone familiar with Sass.

Goals permalinkGoals

High-Level permalinkHigh-Level

These are the philosophical design goals for the module system as a whole. While they don’t uniquely specify a system, they do represent the underlying motivations behind many of the lower-level design decisions.

Low-Level permalinkLow-Level

These are goals that are based less on philosophy than on practicality. For the most part, they’re derived from user feedback that we’ve collected about @import over the years.

Non-Goals permalinkNon-Goals

These are potential goals that we have explicitly decided to avoid pursuing as part of this proposal for various reasons. Some of them may be on the table for future work, but we don’t consider them to be blocking the module system.

However, it’s not feasible in practice. Modules that generate CSS almost always do so based on some configuration, which may be changed by different entrypoints rendering caching useless. What’s more, multiple modules may depend on the same shared module, and one may modify its configuration before the other uses it. Forbidding this case in general would effectively amount to forbidding modules from generating CSS based on variables.

Fortunately, implementations have a lot of leeway to cache information that the can statically determine to be context-independent, including source trees and potentially even constant-folded variable values and CSS trees. Full context independence isn’t likely to provide much value in addition to that.

As tempting as it is, though, we want to make all existing use-cases as easy as possible in the new system, even if we think they should be avoided. This module system is already a major departure from the existing behavior, and will require a substantial amount of work from Sass users to support. We want to make this transition as easy as possible, and part of that is avoiding adding any unnecessary hoops users have to jump through to get their existing stylesheets working in the new module system.

Once @use is thoroughly adopted in the ecosystem, we can start thinking about increased strictness in the form of lints or TypeScript-style --strict-* flags.

We believe that this module system can work in concert with external code-splitting systems. For example, the module system can be used to load libraries that are used to style individual components, each of which is compiled to its own CSS file. These CSS files could then declare dependencies on one another using special comments or custom at-rules and be stitched together by a code-splitting post-processor.

Summary permalinkSummary

This proposal adds two at-rules, @use and @forward, which may only appear at the top level of stylesheets before any rules (other than @charset). Together, they’re intended to completely replace @import, which will eventually be deprecated and even more eventually removed from the language.

@use permalink@use

@use 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 {
  @include bootstrap.float-left;
}

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

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

Controlling Namespaces permalinkControlling 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 permalinkConfiguring 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 permalink@forward

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 permalinkVisibility 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 permalinkExtra 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);

@import Compatibility permalink@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:

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".

Built-In Modules permalinkBuilt-In Modules

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

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

meta.load-css() permalinkmeta.load-css()

This proposal also adds 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.

Frequently Asked Questions permalinkFrequently Asked Questions

// bootstrap.scss
@forward "variables";
@use "reboot";
 
// _variables.scss
$paragraph-margin-bottom: 1rem !default;
// _reboot.scss
@use "variables" as *;

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

Sending Feedback permalinkSending Feedback

This is still just a proposal. We’re pretty happy with the overall shape of the module system, but it’s not at all set in stone, and anything can change with enough feedback provided by users like you. If you have opinions, please file an issue on GitHub or just tweet at @SassCSS. We’ll take anything from “it looks awesome” to “it looks awful”, although the more specific you can be the more information we have to work with!