Permalink
440c4e4 Nov 21, 2018
1 contributor

Users who have contributed to this file

1552 lines (1180 sloc) 64.7 KB

The Next-Generation Sass Module System: Draft 4

(Issues, Changelog)

This repository houses a proposal for the @use rule and associated module system. This is a living proposal: it's intended to evolve over time, and is hosted on GitHub to encourage community collaboration and contributions. Any suggestions or issues can be brought up and discussed on the issue tracker.

Although this document describes some imperative processes when describing the semantics of the module system, these aren't meant to prescribe a specific implementation. Individual implementations are free to implement this feature however they want as long as the end result is the same. However, there are specific design decisions that were made with implementation efficiency in mind—these will be called out explicitly in non-normative block-quoted asides.

Table of Contents

Background

This section is non-normative.

The new @use at-rule is intended to supercede Sass's @import rule as the standard way of sharing styles across Sass files. @import is the simplest possible form of re-use: it does little more than directly include the target file in the source file. This has caused numerous problems in practice: including the same file more than once slows down compilation and produces redundant output; users must manually namespace everything in their libraries; there's no encapsulation to allow them to keep implementation details hidden; and it's very difficult for either humans or tools to tell where a given variable, mixin, or function comes from.

The new module system is intended to address these shortcomings (among others) and bring Sass's modularity into line with the best practices as demonstrated by other modern languages. As such, the semantics of @use are heavily based on other languages' module systems, with Python and Dart being particularly strong influences.

Goals

This section is non-normative.

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

  • Locality. The module system should make it possible to understand a Sass file by looking only at that file. An important aspect of this is that names in the file should be resolved based on the contents of the file rather than the global state of the compilation. This also applies to authoring: an author should be able to be confident that a name is safe to use as long as it doesn't conflict with any name visible in the file.

  • Encapsulation. The module system should allow authors, particularly library authors, to choose what API they expose. They should be able to define entities for internal use without making those entities available for external users to access or modify. The organization of a library's implementation into files should be flexible enough to change without changing the user-visible API.

  • Configuration. Sass is unusual among languages in that its design leads to the use of files whose entire purpose is to produce side effects—specifically, to emit CSS. There's also a broader class of libraries that may not emit CSS directly, but do define configuration variables that are used in computations, including computation of other top-level variables' values. The module system should allow the user to flexibly use and configure modules with side-effects.

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

  • Import once. Because @import is a literal textual inclusion, multiple @imports of the same Sass file within the scope of a compilation will compile and run that file multiple times. At best this hurts compilation time for little benefit, and it can also contribute to bloated CSS output when the styles themselves are duplicated. The new module system should only compile a file once.

  • Backwards compatibility. We want to make it as easy as possible for people to migrate to the new module system, and that means making it work in conjunction with existing stylesheets that use @import. Existing stylesheets that only use @import should have identical importing behavior to earlier versions of Sass, and stylesheets should be able to change parts to @use without changing the whole thing at once.

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

  • Dynamic imports. Allowing the path to a module to be defined dynamically, whether by including variables or including it in a conditional block, moves away from being declarative. In addition to making stylesheets harder to read, this makes any sort of static analysis more difficult (and actually impossible in the general case). It also limits the possibility of future implementation optimizations.

  • Importing multiple files at once. In addition to the long-standing reason that this hasn't been supported—that it opens authors up to sneaky and difficult-to-debug ordering bugs—this violates the principle of locality by obfuscating which files are imported and thus where names come from.

  • Extend-only imports. The idea of importing a file so that the CSS it generates isn't emitted unless it's @extended is cool, but it's also a lot of extra work. This is the most likely feature to end up in a future release, but it's not central enough to include in the initial module system.

  • Context-independent modules. It's tempting to try to make the loaded form of a module, including the CSS it generates and the resolved values of all its variables, totally independent of the entrypoint that cause it to be loaded. This would make it possible to share loaded modules across multiple compilations and potentially even serialize them to the filesystem for incremental compilation.

    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.

  • Increased strictness. Large teams with many people often want stricter rules around how Sass stylesheets are written, to enforce best practices and quickly catch mistakes. It's tempting to use a new module system as a lever to push strictness further; for example, we could make it harder to have partials directly generate CSS, or we could decline to move functions we'd prefer people avoid to the new built-in modules.

    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.

  • Code splitting. The ability to split monolithic CSS into separate chunks that can be served lazily is important for maintaining quick load times for very large applications. However, it's orthogonal to the problems that this module system is trying to solve. This system is primarily concerned with scoping Sass APIs (mixins, functions, and placeholders) rather than declaring dependencies between chunks of generated CSS.

    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

This section is non-normative.

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

@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:

  • @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

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

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

Built-In Modules

The new module system will also add seven built-in modules: math, color, 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()

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

This section is non-normative.

  • Why this privacy model? We considered a number of models for declaring members to be private, including a JS-like model where only members that were explicitly exported from a module were visible and a C#-like model with an explicit @private keyword. These models involve a lot more boilerplate, though, and they work particularly poorly for placeholder selectors where privacy may be mixed within a single style rule. Name-based privacy also provides a degree of compatibility with conventions libraries are already using.

  • Can I make a member library-private? There's no language-level notion of a "library", so library-privacy isn't built in either. However, members used by one module aren't automatically visible to downstream modules. If a module isn't @forwarded through a library's main stylesheet, it won't be visible to downstream consumers and thus is effectively library-private.

    As a convention, we recommend that libraries write library-private stylesheets that aren't intended to be used directly by their users in a directory named src.

  • How do I make my library configurable? If you have a large library made up of many source files that all share some core !default-based configuration, we recommend that you define that configuration in a file that gets forwarded from your library's entrypoint and used by your library's files. For example:

    // 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
    );

Definitions

Member

A member is a Sass construct that's defined either by the user or the implementation and is identified by a Sass identifier. This currently includes variables, mixins, and functions (but not placeholder selectors). Each member type has its own namespace, so for example the mixin name doesn't conflict with the function name or the variable $name. All members have definitions associated with them, whose specific structure depends on the type of the given member.

Extension

An extension is an object that represents a single @extend rule. It contains two selectors: the extender is the selector for the rule that contains the @extend, and the extendee is the selector that comes after the @extend. For example:

.extender {
  @extend .extendee;
}

An extension may be applied to a selector to produce a new selector. This process is outside the scope of this document, and remains unchanged from previous versions of Sass.

CSS Tree

A CSS tree is an abstract CSS syntax tree. It has multiple top-level CSS statements like at-rules or style rules. The ordering of these statements is significant.

A CSS tree cannot contain any Sass-specific constructs, with the notable exception of placeholder selectors. These are allowed so that modules' CSS may be @extended.

An empty CSS tree contains no statements.

Configuration

A configuration is a map from variable names to SassScript values. It's used when executing a source file to customize its execution. An empty configuration contains no entries.

Module

A module is a collection of members and extensions, as well as a CSS tree (although that tree may be empty). User-defined modules have an associated source file as well. Each module may have only one member of a given type and name (for example, a module may not have two variables named $name).

A given module can be produced by executing the source file identified by the module's canonical URL with a configuration.

Module Graph

Modules also track their @use and @forward at-rules, which point to other modules. In this sense, modules can be construed as a directed acyclic graph where the vertices are modules and the edges are @use rules and/or @forward rules. We call this the module graph.

The module graph is not allowed to contain cycles because they make it impossible to guarantee that all dependencies of a module are available before that module is loaded. Although the names and APIs of a module's members can be determined without executing it, Sass allows code to be evaluated while loading a module, so those members may not behave correctly when invoked before the module is executed.

Source File

A source file is a Sass abstract syntax tree along with its canonical URL. Each canonical URL is associated with zero or one source files.

A source file can be executed with a configuration to produce a module.

The names (and mixin and function signatures) of this module's members are static, and can be determined without executing the file. This means that all modules for a given source file have the same member names regardless of the context in which those modules are loaded.

Note that built-in modules do not have source files associated with them.

Entrypoint

The entrypoint of a compilation is the source file that was initially passed to the implementation. Similarly, the entrypoint module is the module loaded from that source file with an empty configuration. The entrypoint module is the root of the module graph.

Import Context

An import context is a collection of members, indexed by their types and names. It's used to ensure that the previous global-namespace behavior is preserved when @imports are used.

An import context is mutable throughout its entire lifetime, unlike a module whose CSS and function/mixin definitions don't change once it's been fully created. This allows it to behave as a shared namespace for a connected group of imports.

Note that an import context never includes members made visible by @use, even if a file with @use rules is imported.

Syntax

@use

The new at-rule will be called @use. The grammar for this rule is as follows:

UseRule         ::= '@use' QuotedString AsClause? WithClause?
AsClause        ::= 'as' ('*' | Identifier)
WithClause      ::= 'with' '('
                      KeywordArgument (',' KeywordArgument)* ','?
                    ')'
KeywordArgument ::= '$' Identifier ':' Expression

@use rules must be at the top level of the document, and must come before any rules other than @charset or @forward. The QuotedString's contents, known as the rule's URL, must be a valid URL string (for non-special base URL). No whitespace is allowed after $ in KeywordArgument.

Because each @use rule affects the namespace of the entire source file that contains it, whereas most other Sass constructs are purely imperative, keeping it at the top of the file helps reduce confusion.

Variable declarations aren't rules, and so are valid before or between @use and @forward rules. This makes it possible to define intermediate variables when passing configuration to a WithClause.

@use "sass:color";

$base-color: #abc;
@use "library" with (
  $base-color: $base-color,
  $secondary-color: color.scale($base-color, $lightness: -10%),
);

A @use rule's namespace is determined using this algorithm. If the algorithm for determining a namespace fails for a @use rule, that rule is invalid. If it returns null, that rule is called global. A namespace is used to identify the used module's members within the current source file.

@forward

This proposal introduces an additional new at-rule, called @forward. The grammar for this rule is as follows:

ForwardRule ::= '@forward' QuotedString (ShowClause | HideClause)? AsClause?
ShowClause  ::= 'show' MemberName (',' MemberName)*
HideClause  ::= 'hide' MemberName (',' MemberName)*
MemberName  ::= '$'? Identifier
AsClause    ::= 'as' Identifier '*'

@forward rules must be at the top level of the document, and must come before any rules other than @charset or @use. If they have a QuotedString, its contents, known as the rule's URL, must be a valid URL string (for non-special base URL). No whitespace is allowed after $ in MemberName, or before * in AsClause.

Member References

This proposal updates the syntax for referring to members. For functions and mixins, this update affects only calls, not definitions. Variables, on the other hand, may use this syntax for either assignment or reference.

NamespacedIdentifier ::= (Identifier '.')? Identifier
Variable             ::= '$' NamespacedIdentifier
FunctionCall         ::= NamespacedIdentifier ArgumentInvocation
Include              ::= '@include' NamespacedIdentifier ArgumentInvocation?

No whitespace is allowed before or after the '.' in NamespacedIdentifier, after the '$' in Variable, or between the NamespacedIdentifier and the ArgumentInvocation in FunctionCall or Include.

The dot-separated syntax (namespace.name) was chosen in preference to a hyphenated syntax (for example namespace-name) because it makes the difference between module-based namespaces and manually-separated identifiers very clear. It also matches the conventions of many other languages. We're reasonably confident that the syntax will not conflict with future CSS syntax additions.

Procedures

The following procedures are not directly tied to the semantics of any single construct. Instead, they're used as components of multiple constructs' semantics. They can be thought of as re-usable functions.

Determining Namespaces

This algorithm takes a @use rule rule, and returns either a string or an identifier.

This algorithm is context-independent, so a namespace for a @use rule can be determined without reference to anything outside the syntax of that rule.

  • If rule has an 'as' clause as:

    • If as has an identifier, return it.

    • Otherwise, return null. The rule is global.

  • Let path be the rule's URL's path.

  • Let basename be the text after the final / in path, or the entire path if path doesn't contain /.

  • Let module-name be the text before the first . in path, or the entire path if path doesn't contain ..

  • If module-name isn't a Sass identifier, throw an error.

  • Return module-name.

Loading Modules

This describes the general process for loading a module. It's used as part of various other semantics described below. To load a module with a given URL url and configuration config:

  • If url's scheme is sass:

    • If config is not empty, throw an error.

    • If a built-in module exists with the exact given URL, return it.

    • Otherwise, throw an error.

  • Let file be the source file result of loading url.

  • If file is null, throw an error.

  • If file has already been executed:

    • If config is not empty, throw an error.

    • Otherwise, return the module that execution produced.

    This fulfills the "import once" low-level goal.

  • If file is currently being executed, throw an error.

    This disallows circular @uses, which ensures that modules can't be used until they're fully initialized.

  • Otherwise, return the result of executing file with config and a new import context.

For simplicity, this proposal creates an import context for every module. Implementations are encouraged to avoid eagerly allocating resources for imports, though, to make use-cases only involving @use more efficient.

Resolving Extensions

The module system also scopes the resolution of the @extend rule. This helps satisfy locality, making selector extension more predictable than its global behavior under @import.

Extension is scoped to CSS in modules transitively used by the module in which the @extend appears. This transitivity is necessary because CSS is not considered a member of a module, and can't be controlled as explicitly as members can.

We considered having extension also affect modules that were downstream of the @extend, on the theory that they had a similar semantic notion of the selector in question. However, because this didn't affect other modules imported by the downstream stylesheet, it created a problem for the downstream author. It should generally be safe to take a bunch of style rules from one module and split them into multiple modules that are all imported by that module, but doing so could cause those styles to stop being affected by upstream extensions.

Extending downstream stylesheets also meant that the semantics of a downstream author's styles are affected by the specific extensions used in an upstream stylesheet. For example,

.foo { /* ... */ }
.bar { @extend .foo }

isn't identical (from a downstream user's perspective) to

.foo, .bar { /* ... */ }

That could be a drawback or a benefit, but it's more likely that upstream authors think of themselves as distributing a chunk of styles rather than an API consisting of things they've extended.

We define a general process for resolving extensions for a given module starting-module. This process returns a CSS tree that includes CSS for all modules transitively used or forwarded by starting-module.

  • Let new-selectors be an empty map from style rules to selectors. For the purposes of this map, style rules are compared using reference equality, meaning that style rules at different points in the CSS tree are always considered different even if their contents are the same.

  • Let extended be the subgraph of the module graph containing modules that are transitively reachable from starting-module.

  • For each module domestic in extended, in reverse topological order:

    • Let foreign-modules be the set of modules used by domestic, as well as the set of modules transitively used or forwarded by those modules.

      This excludes modules that are only accessible from domestic because it forwarded them. @extend only applies to used CSS, not forwarded CSS.

    • For each module in foreign-modules, in reverse topological order:

      • For each style rule rule in foreign's CSS:

        • Set new-selectors[rule] to the result of applying domestic's extensions to new-selectors[rule].

          This overwrites the previous value of new-selectors[rule].

          new-selectors[rule] is guaranteed to exist at this point because extended is traversed in reverse topological order, which means that foreign's own extensions will already have been resolved by the time we start working on its dependers.

    • For each style rule rule in domestic:

      • Set new-selectors[rule] to the result of applying domestic's extensions to rule's selector.
  • Let css be an empty CSS tree.

  • Define a recursive procedure, "traversing", which takes a module domestic:

    • If domestic has already been traversed, do nothing.

    • Otherwise, for each top-level statement statement in domestic's CSS tree:

      • Traverse the modules of every @use or @forward rule that appears before statement's source location in domestic's source file.

        Most of the time, this means that all @use rules are traversed before any statements are copied into css, because @use and @forward must appear before any CSS rules. However, /* comments may appear before @use and @forward, and their relative location should be preserved in the generated CSS.

        If there are no comments that appear before @use or @forward rules, this emits CSS in reverse topological order.

      • Add a copy of statement to css, with any style rules' selectors replaced with the corresponding selectors in new-selectors.

  • Return css.

Resolving a file: URL

This algorithm is intended to replace the existing algorithm for resolving a file: URL to add support for @import-only files. This algorithm takes a URL, url, whose scheme must be file and returns either another URL that's guaranteed to point to a file on disk or null.

This allows a library to define two parallel entrypoints, one (_file.import.scss) that's visible to @import and one (_file.scss) that's visible to @use. This will allow it to maintain backwards-compatibility even as it switches to supporting a @use-based API.

The major design question here is whether the file for @use or @import should be the special case. The main benefit to _file.use.scss would be that users don't need to use a version of Sass that supports @use to get the import-only stylesheet, but in practice it's likely that most library authors will want to use @use or other new Sass features internally anyway.

On the other hand, there are several benefits to _file.import.scss:

  • It makes the recommended entrypoint is the more obvious one.

  • It inherently limits the lifetime of language support for the extra entrypoint: once imports are removed from the language, import-only files will naturally die as well.

  • It will make @use resolution somewhat faster, since it has to check for half as many files every time.

Resolving a file: URL for Extensions

"Extensions" in this procedure's name refers to "file extensions", not Sass extensions.

This algorithm takes a URL, url, whose scheme must be file and returns either another URL that's guaranteed to point to a file on disk or null.

  • If url ends in .scss, .sass, or .css, return the result of resolving url for partials.

    @imports whose URLs explicitly end in .css will have been treated as plain CSS @imports before this algorithm even runs, so url will only end in .css for @use rules.

  • Let sass be the result of resolving url + ".sass" for partials.

  • Let scss be the result of resolving url + ".scss" for partials.

  • Let css be the result of resolving url + ".css" for partials.

  • If this algorithm is being run for an @import:

    • If neither sass nor scss are null, throw an error.

    • Otherwise, if exactly one of sass and scss is null, return the other one.

    • Otherwise, return css.

  • Otherwise:

    • If all of sass, scss, and css are null, return null.

    • If exactly one of sass, scss, and css is not null, return it.

    • Otherwise, throw an error.

This algorithm is based on the existing algorithm for resolving a file: URL. The difference is that, when resolving for @use, a .css file is treated with the same priority as a .scss and .sass file.

The only reason a .css file was ever treated as secondary was that CSS imports were added later on, and backwards-compatibility needed to be maintained for @import. @use allows us to make CSS more consistent with the other extensions, at a very low risk of migration friction.

Semantics

Compilation Process

First, let's look at the large-scale process that occurs when compiling a Sass entrypoint with the canonical URL url to CSS.

  • Let module be the result of loading url with the empty configuration.

    Note that this transitively loads any referenced modules, producing a module graph.

  • Let css be the result of resolving extensions for module.

  • Convert css to a CSS string. This is the result of the compilation.

Executing Files

Many of the details of executing a source file are out of scope for this specification. However, certain constructs have relevant new semantics that are covered below. This procedure should be understood as modifying and expanding upon the existing execution process rather than being a comprehensive replacement.

Given a source file file, a configuration config, and an import context import:

  • If this file isn't being executed for a @forward rule:

    • For every variable name name in config:

      • If neither file nor any source file for a module transitively forwarded by file contains a variable declaration named name with a !default flag, throw an error.

        Although forwarded modules are not fully loaded at this point, it's still possible to statically determine where those modules are located and whether they contain variables with default declarations.

        Implementations may choose to verify this lazily, after file has been executed.

  • Let module be an empty module with the same URL as file.

  • Let uses be an empty map from @use rules to modules.

  • When a @use rule rule is encountered:

    • If rule has a namespace that's the same as another @use rule's namespace in file, throw an error.

    • Let rule-config be the empty configuration.

    • If rule has a WithClause:

      • For each KeywordArgument argument in this clause:

        • Let value be the result of evaluating argument's expression.

          If the expression refers to a module that's used below rule, that's an error.

        • Add a variable to rule-config with the same name as argument's identifier and with value as its value.

    • Let module be the result of loading the module with rule's URL and rule-config.

    • Associate rule with module in uses.

  • When a @forward rule is encountered, forward the module it refers to with config.

  • When an @import rule is encountered, import the file it refers to with import.

  • When an @extend rule is encountered, add its extension to module.

    Note that this adds the extension to the module being evaluated, not the module in which the @extend lexically appears. This means that @extends are effectively dynamically scoped, not lexically scoped. This design allows extensions generated by mixins to affect rules also generated by mixins.

  • When a style rule or a plain CSS at-rule is encountered:

    • Let css be the result of executing the rule as normal.

    • Remove any complex selectors containing a placeholder selector that begins with - or _ from css.

    • Remove any style rules that now have no selector from css.

    • Append css to module's CSS.

  • When a variable declaration variable is encountered:

    • If variable's name is a namespaced identifier and it has a !global flag, throw an error.

    • If variable is at the top level of file, or its name is a namespaced identifier, or it has a !global flag:

      • Let resolved be the result of resolving variable using file, uses, config, and import.

      • If variable has a !default flag, and resolved isn't null, and resolved's value isn't null, do nothing.

      • Otherwise, if resolved is a variable in another module:

        • Set resolved's value to variable's value.
      • Otherwise:

        • If variable's name doesn't begin with - or _, add variable to module.

          This overrides the previous definition, if one exists.

        • Add variable to import.

    • Otherwise, evaluate it as usual.

  • When a top-level mixin or function declaration member is encountered:

    Mixins and functions defined within rules are never part of a module's API.

    • If member's name doesn't begin with - or _, add member to module.

      This overrides the previous definition, if one exists.

    • Add member to import.

      This happens regardless of whether or not it begins with - or _.

  • When a member use is encountered, resolve it using file, uses, config, and import. If this returns null, throw an error.

  • Finally, return module. Its functions, mixins, and CSS are now immutable.

Note that members that begin with - or _ (which Sass considers equivalent) are considered private. Private members are not added to the module's member set, but they are visible from within the module itself. This follows Python's and Dart's privacy models, and bears some similarity to CSS's use of leading hyphens to indicate experimental vendor features.

For backwards-compatibility, privacy does not apply across @import boundaries. If one file imports another, either may refer to the other's private members.

// This function is private and may only be used within this module.
@function -parse-gutters($short) {
  // ...
}

// By contrast, this mixin is part of the module's public API.
@mixin gutters($span) {
  // But it can use private members within its own module.
  $span: -parse-gutters($span);
}

This proposal follows Python and diverges from Dart in that @use imports modules with a namespace by default. There are two reasons for this. First, it seems to be the case that language ecosystems with similar module systems either namespace all imports by convention, or namespace almost none. Because Sass is not object-oriented and doesn't have the built-in namespacing that classes provide many other languages, its APIs tend to be much broader at the top level and thus at higher risk for name conflict. Namespacing by default tilts the balance towards always namespacing, which mitigates this risk.

Second, a default namespace scheme drastically reduces the potential for inconsistency in namespace choice. If the namespace is left entirely up to the user, different people may choose to namespace strings.scss as strings, string, str, or strs. This taxes the reusability of code and knowledge, and mitigating it is a benefit.

// This has the default namespace "susy".
@use "susy";

// This has the explicit namespace "bbn".
@use "bourbon" as bbn;

// This has no namespace.
@use "compass" as *;

// Both libraries define their own "gutters()" functions. But because the
// members are namespaced, there's no conflict and the user can use both at
// once.
#susy {@include susy.gutters()}
#bourbon {@include bbn.gutters()}

// Users can also import without a namespace at all, which lets them use the
// original member names.
#compass {@include gutters()}

Resolving Members

The main function of the module system is to control how member names are resolved across files—that is, to find the definition corresponding to a given name. Given a source file file, a map uses from @use rules to the modules loaded by those rules, a member to resolve named name of type type, a configuration config, and an import context import:

  • If name is a namespaced identifier namespace.raw-name:

    • Let use be the @use rule in uses whose namespace is namespace. If there is no such rule, throw an error.

    • If use hasn't been evaluated yet, throw an error.

    • Let module be the module in uses associated with use.

    • Return the member of module with type type and name raw-name. If there is no such member, throw an error.

  • If type is "variable" and config contains a variable named name, return it.

  • If file defines a member member of type named name:

    • If member's definition has already been evaluated, return it.

    • Otherwise, return null.

  • If a member of type type named name is defined in exactly one module in uses whose @use rule is global, return that member.

  • Otherwise, if a member of type type named name is defined in more than one module in uses whose @use rule is global, throw an error.

    This ensures that, if a new version of a library produces a conflicting name, it causes an immediate error.

  • If import exists and contains a member of type type named name, return it.

  • Otherwise, return null.

Forwarding Modules

The @forward rule forwards another module's public API as though it were part of the current module's.

Note that @forward does not make any APIs available to the current module; that is purely the domain of @use. It does include the forwarded module's CSS tree, but it's not visible to @extend without also using the module.

This algorithm takes a @forward rule rule and a configuration config. It modifies the current module.

  • Let module be the result of loading the module for rule's URL with config.

  • For every member member in module:

    • Let name be member's name.

    • If rule has an AsClause as, prepend as's identifier to name (after the $ if member is a variable).

    • If there's a member defined in the current source file named name with the same type as member, do nothing.

      Giving local definitions precedence ensures that a module continues to expose the same API if a forwarded module changes to include a conflicting member.

    • Otherwise, if rule has a show clause that doesn't include name (including $ for variables), do nothing.

      It's not possible to show/hide a mixin without showing/hiding the equivalent function, or to do the reverse. This is unlikely to be a problem in practice, though, and adding support for it isn't worth the extra syntactic complexity it would require.

    • Otherwise, if rule has a hide clause that does include name (including $ for variables), do nothing.

    • Otherwise, if another @forward rule's module has a member named name with the same type as member, throw an error.

      Failing here ensures that, in the absence of an obvious member that takes precedence, conflicts are detected as soon as possible.

    • Otherwise, add member to the current module's collection of members with the name name.

      It's possible for the same member to be added to a given module multiple times if it's forwarded with different prefixes. All of these names refer to the same logical member, so for example if a variable gets set that change will appear for all of its names.

      It's also possible for a module's members to have multiple prefixes added, if they're forwarded with prefixes multiple times.

This forwards all members by default to reduce the churn and potential for errors when a new member gets added to a forwarded module. It's likely that most libraries will already break up their definitions into many smaller modules which will all be forwarded, which makes the API definition explicit enough without requiring additional explicitness here.

// _susy.scss would forward its component files so users would see its full
// API with a single @use, but the definitions don't have to live in a single
// file.

@forward "susy/grids";
@forward "susy/box-sizing";
@forward "susy/content";

// You can show or hide members that are only meant to be used within the
// library. You could also choose not to forward this module at all and only
// use it from internal modules.
@forward "susy/settings" hide susy-defaults;

Importing Files

For a substantial amount of time, @use will coexist with the old @import rule in order to ease the burden of migration. This means that we need to define how the two rules interact.

When executing an @import rule rule with an import context import:

  • Let file be the source file result of loading url.

  • If file is null, throw an error.

  • If file is currently being executed, throw an error.

  • Let module be the result of executing file with the empty configuration and import as its import context, with the following differences:

    • If the @import rule is nested within at-rules and/or style rules, that context is preserved when executing file.

    • The generated CSS for style rules or at-rules in file is appended to the current module's CSS.

    Note that this execution can mutate import.

  • Add the module's extensions to the current module.

  • For each member member in module:

    • If member has the same type and name as a member in import, do nothing.

      Note that all members defined in file or in files it imports will already be in import. Only members brought in by @forward are added to import in this step.

    • Otherwise, add member to import and to the current module.

      This makes forwarded members available in the importing module, but does not allow them to overwrite existing members with the same names and types.

When a stylesheet contains only @imports without any @uses, the @imports are intended to work exactly as they did in previous Sass versions. Any difference should be considered a bug in this specification.

This definition allows files that include @use to be imported. Doing so includes those modules' CSS as well as any members they define or forward. This makes it possible for users to continue using @import even when their dependencies switch to @use, which conversely makes it safer for libraries to switch to @use.

It also allows files that use @import to be used as modules. Doing so treats them as though all CSS and members were included in the module itself.

Built-In Modules

The new module system provides an opportunity to bring more locality and organization to the set of built-in functions that comprise Sass's core library. These functions currently reside in the same global namespace as everything else, which makes it difficult to add new functions without risking conflict with either user code or future CSS functions (which has happened in practice).

We'll move all current built-in functions to built-in modules, except for those functions that are intentionally compatible with plain CSS functions. These modules are identified by URLs that begin with "sass:". This scheme was chosen to avoid conflicting with plausible filenames while still being relatively concise.

The built-in functions will be organized as follows:

Current Name New Name Module Current Name New Name Module
rgb global percentage sass:math
rgba global round sass:math
hsl global ceil sass:math
hsla global floor sass:math
if global abs sass:math
min sass:math
red sass:color max sass:math
blue sass:color random sass:math
green sass:color unit sass:math
mix sass:color unitless is-unitless sass:math
hue sass:color comparable compatible sass:math
saturation sass:color
lightness sass:color length sass:list
adjust-hue sass:color nth sass:list
lighten sass:color set-nth sass:list
darken sass:color join sass:list
saturate sass:color append sass:list
desaturate sass:color zip sass:list
grayscale sass:color index sass:list
complement sass:color list-separator separator sass:list
invert sass:color
alpha sass:color feature-exists sass:meta
opacify sass:color variable-exists sass:meta
transparentize sass:color global-variable-exists sass:meta
adjust-color adjust sass:color function-exists sass:meta
scale-color scale sass:color mixin-exists sass:meta
change-color change sass:color inspect sass:meta
ie-hex-str sass:color get-function sass:meta
type-of sass:meta
map-get get sass:map call sass:meta
map-merge merge sass:map content-exists sass:meta
map-remove remove sass:map module-variables sass:meta
map-keys keys sass:map module-functions sass:meta
map-values values sass:map
map-has-key has-key sass:map unquote sass:string
keywords sass:map quote sass:string
str-length length sass:string
selector-nest nest sass:selector str-insert insert sass:string
selector-append append sass:selector str-index index sass:string
selector-replace replace sass:selector str-slice slice sass:string
selector-unify unify sass:selector to-upper-case sass:string
is-superselector sass:selector to-lower-case sass:string
simple-selectors sass:selector unique-id sass:string
selector-parse parse sass:selector

In addition, one built-in mixin will be added:

Name Module
load-css sass:meta

Built-in modules will contain only the functions described above. They won't contain any other members, CSS, or extensions. New members may be added in the future, but CSS will not be added to existing modules.

@use "sass:color";
@use "sass:map";
@use "sass:math";

// Adapted from https://css-tricks.com/snippets/sass/luminance-color-function/.
@function luminance($color) {
  $colors: (
    'red': color.red($color),
    'green': color.green($color),
    'blue': color.blue($color)
  );

  @each $name, $value in $colors {
    $adjusted: 0;
    $value: $value / 255;

    @if $value < 0.03928 {
      $value: $value / 12.92;
    } @else {
      $value: ($value + .055) / 1.055;
      $value: math.pow($value, 2.4);
    }

    $colors: map.merge($colors, ($name: $value));
  }

  @return map.get($colors, 'red') * .2126 +
      map.get($colors, 'green') * .7152 +
      map.get($colors, 'blue') * .0722;
}

New Functions

The module system brings with it the need for additional introspection abilities. To that end, several new built-in functions will be defined in the sass:meta module.

module-variables()

The module-variables() function takes a $module parameter, which must be a string that matches the namespace of a @use rule in the current source file. It returns a map from variable names defined in the module loaded by that rule (as quoted strings, without $) to the current values of those variables.

Note that (like the existing *-defined() functions), this function's behavior depends on the lexical context in which it's invoked.

module-functions()

The module-functions() function takes a $module parameter, which must be a string that matches the namespace of a @use rule in the current source file. It returns a map from function names defined in the module loaded by that rule (as quoted strings) to function values that can be used to invoke those functions.

Note that (like the existing *-defined() functions), this function's behavior depends on the lexical context in which it's invoked.

load-css()

The load-css() mixin takes a $url parameter, which must be a string, and an optional $with parameter, which must be either a map with string keys or null. When this mixin is invoked:

  • Let config be a configuration whose variable names and values are given by $with if $with is passed and non-null, or the empty configuration otherwise.

  • Let module be the result of loading $url with config. The URL is loaded as though it appeared in a @use rule in the stylesheet where @include load-css() was written.

    This means that load-css() doesn't see import-only stylesheets, and that URLs are resolved relative to the file that contains the @include call even if it's invoked from another mixin.

  • Let css be the result of resolving extensions for module.

    This means that, if a module loaded by load-css() shares some dependencies with the entrypoint module, those dependencies' CSS will be included twice.

  • Treat css as though it were the contents of the mixin.

The load-css() function is primarily intended to satisfy the use-cases that are currently handled using nested imports. It clearly also goes some way towards dynamic imports, which is listed as a non-goal. It's considered acceptable because it doesn't dynamically alter the names available to modules.

There are a couple important things to note here. First, every time load-css() is included, its module's CSS is emitted, which means that the CSS may be emitted multiple times. This behavior makes sense in context, and is unlikely to surprise anyone, but it's good to note nonetheless as an exception to the import-once goal.

Second, load-css() doesn't affect name resolution at all. Although it loads the module in an abstract sense, the user is only able to access the module's CSS, not any functions, mixins, or variables that it defines.

// The CSS from the print module will be nested within the media rule.
@media print {
  @include load-css("print");
}

// These variables are set in the scope of susy's main module.
@include load-css("susy", $with: (
  "columns": 4,
  "gutters": 0.25,
  "math": fluid
));

New Features For Existing Functions

Several functions will get additional features in the new module-system world.

The global-variable-exists(), function-exists(), and mixin-exists() functions will all take an optional $module parameter. This parameter must be a string or null, and it must match the namespace of a @use rule in the current module. If it's not null, the function returns whether the module loaded by that rule has a member with the given name and type. If it's null, it looks for members defined so far in the current module or import context, members of any modules loaded by global @use rules, or global built-in definitions.

Timeline

The exact timing of the release of the module system is up in the air, and depends on available resources and the practical difficulty of implementing it, both of which are currently major question marks. The best we can say for a launch date is that we hope to have it launched in Dart Sass by the end of September 2019. We also hope to launch it concurrently with a migration tool that will automatically convert @import to @use and adjust member references accordingly.

Eventually, we will deprecate @import and all global function calls that can now be made through modules. We plan to do this either one year after both major Sass implementations support this proposal in full, or two years after one implementation does, whichever comes first. This gives LibSass plenty of time to implement the module system without completely gating the ecosystem on it.

We want there to be a large amount of time when @use and @import can coexist, to help people migrate. However, doing away with @import entirely is the ultimate goal for simplicity, performance, and CSS compatibility. As such, one year after @import is deprecated, we plan to drop support for @import 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.