sass/accepted/module-system.md
2024-06-17 17:49:23 -07:00

1801 lines
76 KiB
Markdown

# The Next-Generation Sass Module System: Draft 10
*([Issues](https://github.com/sass/sass/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3A%22%40use%22), [Changelog](module-system.changes.md))*
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][issues].
[issues]: https://github.com/sass/sass/issues?q=is%3Aissue+is%3Aopen+label%3A%22proposal%3A+module+system%22
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](#background)
* [Goals](#goals)
* [High-Level](#high-level)
* [Low-Level](#low-level)
* [Non-Goals](#non-goals)
* [Summary](#summary)
* [`@use`](#use)
* [Controlling Namespaces](#controlling-namespaces)
* [Configuring Libraries](#configuring-libraries)
* [`@forward`](#forward)
* [Visibility Controls](#visibility-controls)
* [Extra Prefixing](#extra-prefixing)
* [`@import` Compatibility](#import-compatibility)
* [Built-In Modules](#built-in-modules)
* [`meta.load-css()`](#metaload-css)
* [Frequently Asked Questions](#frequently-asked-questions)
* [Definitions](#definitions)
* [Member](#member)
* [Extension](#extension)
* [CSS Tree](#css-tree)
* [Configuration](#configuration)
* [Module](#module)
* [Module Graph](#module-graph)
* [Source File](#source-file)
* [Entrypoint](#entrypoint)
* [Import Context](#import-context)
* [Syntax](#syntax)
* [`@use`](#use-1)
* [`@forward`](#forward-1)
* [Member References](#member-references)
* [Procedures](#procedures)
* [Determining Namespaces](#determining-namespaces)
* [Loading Modules](#loading-modules)
* [Resolving Extensions](#resolving-extensions)
* [Resolving a `file:` URL](#resolving-a-file-url)
* [Semantics](#semantics)
* [Compilation Process](#compilation-process)
* [Executing Files](#executing-files)
* [Resolving Members](#resolving-members)
* [Forwarding Modules](#forwarding-modules)
* [Importing Files](#importing-files)
* [Built-In Modules](#built-in-modules-1)
* [New Functions](#new-functions)
* [`module-variables()`](#module-variables)
* [`module-functions()`](#module-functions)
* [`load-css()`](#load-css)
* [New Features For Existing Functions](#new-features-for-existing-functions)
* [Timeline](#timeline)
## 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
`@import`s 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.
* **Static analysis**. We want to make it possible for tools that consume Sass
files to understand where every variable, mixin, and function reference
points. In service of this, we want to ensure that every module has a "static
shape"—the set of variables, mixins, and functions it exposes, as well as
mixin and function signatures—that's entirely independent of how that module
might be executed.
### 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 `@extend`ed 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.
```scss
@use "bootstrap";
.element {
@include bootstrap.float-left;
border: 1px solid bootstrap.theme-color("dark");
margin-bottom: bootstrap.$spacer;
}
```
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`.
```scss
@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.
```scss
@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.
```scss
// bootstrap.scss
$paragraph-margin-bottom: 1rem !default;
p {
margin-top: 0;
margin-bottom: $paragraph-margin-bottom;
}
```
```scss
@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.
```scss
// bootstrap.scss
@forward "functions";
@forward "variables";
@forward "mixins";
```
#### Visibility Controls
A `@forward` rule can choose to show only specific names:
```scss
@forward "functions" show color-yiq;
```
It can also hide names that are intended to be library-private:
```scss
@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:
```scss
// material/_index.scss
@forward "theme" as theme-*;
```
This way users can use the all-in-one module with well-scoped names for theme
variables:
```scss
@use "material" with ($theme-primary: blue);
```
or they can use the child module with simpler names:
```scss
@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 `@import`s is `@use`d, 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 `@use`s is `@import`ed, 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 [`@forward`ed](#forwarding-modules) 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:
```scss
// bootstrap.scss
@forward "variables";
@use "reboot";
```
```scss
// _variables.scss
$paragraph-margin-bottom: 1rem !default;
```
```scss
// _reboot.scss
@use "variables" as *;
p {
margin-top: 0;
margin-bottom: $paragraph-margin-bottom;
}
```
```scss
// 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:
```scss
.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 `@extend`ed.
An *empty CSS tree* contains no statements.
### Configuration
A *configuration* is a map from variable names to SassScript values. It's used
when [executing](#executing-files) a [source file](#source-file) to customize
its execution. An *empty configuration* contains no entries.
### Module
A *module* is a collection of [members](#member) and [extensions](#extension),
as well as a [CSS tree](#css-tree) (although that tree may be empty).
User-defined modules have an associated [source file](#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](#executing-files) the [source
file](#source-file) identified by the module's canonical URL with a
[configuration](#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*.
[directed acyclic graph]: https://en.wikipedia.org/wiki/Directed_acyclic_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](#executing-files) 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](#executing-files) with a
[configuration](#configuration) to produce a [module](#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](#built-in-modules) *do not* have source files
> associated with them.
### Entrypoint
The *entrypoint* of a compilation is the [source file](#source-file) that was
initially passed to the implementation. Similarly, the *entrypoint module* is
the [module](#module) loaded from that source file with an empty configuration.
The entrypoint module is the root of the [module graph](#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 `@import`s 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:
<x><pre>
**UseRule** ::= '@use' QuotedString AsClause? WithClause?
**AsClause** ::= 'as' ('\*' | Identifier)
**WithClause** ::= 'with' '('
&#32; KeywordArgument (',' KeywordArgument)\* ','?
&#32; ')'
**KeywordArgument** ::= '$' Identifier ':' Expression
</pre></x>
`@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][special
URL scheme] base URL). No whitespace is allowed after `$` in `KeywordArgument`.
[valid URL string]: https://url.spec.whatwg.org/#valid-url-string
[special URL scheme]: https://url.spec.whatwg.org/#special-scheme
> Because each `@use` rule affects the namespace of the entire [source
> file](#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`.
>
> ```scss
> @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](#determining-namespaces). 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](#module)'s members within the current [source file](#source-file).
### `@forward`
This proposal introduces an additional new at-rule, called `@forward`. The
grammar for this rule is as follows:
<x><pre>
**ForwardRule** ::= '@forward' QuotedString AsClause? (ShowClause | HideClause)?
**AsClause** ::= 'as' Identifier '*'
**ShowClause** ::= 'show' MemberName (',' MemberName)*
**HideClause** ::= 'hide' MemberName (',' MemberName)*
**MemberName** ::= '$'? Identifier
</pre></x>
`@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][special URL scheme] 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.
<x><pre>
**PublicIdentifier** ::= [\<ident-token>] that doesn't begin with '-' or '_'
**Variable** ::= '$' Identifier | Identifier '.$' PublicIdentifier
**NamespacedIdentifier** ::= Identifier | Identifier '.' PublicIdentifier
**FunctionCall** ::= NamespacedIdentifier ArgumentInvocation
**Include** ::= '@include' NamespacedIdentifier ArgumentInvocation?
</pre></x>
[\<ident-token>]: https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
No whitespace is allowed before or after the `'.'` in `NamespacedIdentifier`,
before or after the `'.$'` in `VariableIdentifier`, after the `$` in
`VariableIdentifier`, 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][Tab comment] that the syntax will not conflict with
> future CSS syntax additions.
>
> [Tab comment]: https://github.com/sass/sass/issues/2618#issuecomment-174755061
## 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][URL path].
[URL path]: https://url.spec.whatwg.org/#concept-url-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](#configuration) `config`:
* If `url`'s scheme is `sass`:
* If `config` is not empty, throw an error.
* If a [built-in module](#built-in-modules) exists with the exact given URL,
return it.
* Otherwise, throw an error.
* Let `file` be the [source file](#source-file) result of [loading][loading an
import] `url`.
[loading an import]: https://github.com/sass/sass/blob/2c08156248496c75678826420aafb09e2ace9041/spec/at-rules/import.md#loading-an-import
* If `file` is null, throw an error.
* If `file` has already been [executed](#executing-files):
* 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 `@use`s, which ensures that modules can't be used
> until they're fully initialized.
* Otherwise, return the result of [executing](#executing-files) `file` with
`config` and a new [import context](#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](#module) *transitively used or forwarded
by* the module in which the `@extend` appears. This transitivity is necessary
because CSS is not considered a [member](#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,
>
> ```scss
> .foo { /* ... */ }
> .bar { @extend .foo }
> ```
>
> isn't identical (from a downstream user's perspective) to
>
> ```scss
> .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](#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 `new-extensions` be an empty map from modules to sets of extensions.
* Let `extended` be the subgraph of the [module graph](#module-graph) containing
modules that are transitively reachable from `starting-module`.
* For each module `domestic` in `extended`, in reverse [topological] order:
* Let `downstream` be the set of modules that use or forward `domestic`.
> We considered having extension *not* affect forwarded modules that weren't
> also used. This would have matched the visibility of module members, but
> it would also be the only place where `@forward` and `@use` behave
> differently with regards to CSS, which creates confusion and
> implementation complexity. There's also no clear use case for it, so we
> went with the simpler route of making forwarded CSS visible to `@extend`.
* For each style rule `rule` in `domestic`'s CSS:
* Let `selector` be the result of applying `domestic`'s extensions to
`rule`'s selector.
* Let `selector-lists` be an empty set of selector lists.
* For each module `foreign` in `downstream`:
* Let `extended-selector` be the result of applying
`new-extensions[foreign]` to `selector`.
> `new-extensions[foreign]` is guaranteed to be populated 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 modules upstream of it.
* Add `selector` to `selector-lists`.
* Set `new-selectors[rule]` to a selector that matches the union of all
elements matched by selectors in `selector-lists`. This selector must obey
[the specificity laws of extend] relative to the selectors from which it
was generated. For the purposes of the first law of extend, "the original
extendee" is considered only to refer to selectors that appear in
`domestic`'s CSS, *not* selectors that were added by other modules'
extensions.
> Implementations are expected to trim redundant selectors from
> `selector-lists` as much as possible. For the purposes of the first law
> of extend, "the original extendee" is *only* the selectors in `rule`'s
> selector. The new complex selectors in `selector` generated from
> `domestic`'s extensions don't count as "original", and may be optimized
> away.
* For every extension `extension` whose extender appears in `rule`'s
selector:
* For every complex selector `complex` in `new-selectors[rule]`:
* Add a copy of `extension` with its extender replaced by `complex` to
`new-extensions[domestic]`.
[the specificity laws of extend]: ../spec/at-rules/extend.md#specificity
* 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, traverse every module `@use`d or `@forward`ed by `domestic`, in
the order their `@use` or `@forward` rules appear in `domestic`'s source.
> Because this traverses modules depth-first, it emits CSS in reverse
> topological order.
* Let `initial-imports` be the longest initial subsequence of top-level
statements in `domestic`'s CSS that contains only comments and `@import`
rules *and* that ends with an `@import` rule.
* Insert a copy of `initial-imports` in `css` after the last `@import` rule, or
at the beginning of `css` if it doesn't contain any `@import` rules.
* For each top-level statement `statement` in `domestic`'s CSS tree after
`initial-imports`:
* If `statement` is an `@import` rule, insert a copy of `statement` in `css`
after the last `@import` rule, or at the beginning of `css` if it doesn't
contain any `@import` rules.
* Otherwise, add a copy of `statement` to the end of `css`, with any style
rules' selectors replaced with the corresponding selectors in
`new-selectors`.
* Return `css`.
[topological]: https://en.wikipedia.org/wiki/Topological_sorting
### 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, and to allow imports that
include a literal `.css` extension. 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.
[the existing algorithm]: https://github.com/sass/sass/blob/2c08156248496c75678826420aafb09e2ace9041/spec/at-rules/import.md#resolving-a-file-url
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`:
* If this algorithm is being run for an `@import`:
* Let `suffix` be the trailing `.scss`, `.sass`, `.css` in `url`, and
`prefix` the portion of `url` before `suffix`.
* If the result of [resolving `prefix` + `".import"` + `suffix` for
partials][resolving for partials] is not null, return it.
* Otherwise, return the result of [resolving `url` for partials][resolving for
partials].
> `@import`s whose URLs explicitly end in `.css` will have been treated as
> plain CSS `@import`s before this algorithm even runs, so `url` will only end
> in `.css` for `@use` rules.
* If this algorithm is being run for an `@import`:
* Let `sass` be the result of [resolving `url` + `".import.sass"` for
partials][resolving for partials].
* Let `scss` be the result of [resolving `url` + `".import.scss"` for
partials][resolving for partials].
* 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, if the result of [resolving `url` + `".import.css"` for
partials][resolving for partials] is not null, return it.
* Otherwise, let `sass` be the result of [resolving `url` + `".sass"` for
partials][resolving for partials].
* Let `scss` be the result of [resolving `url` + `".scss"` for
partials][resolving for partials].
* 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 the result of [resolving `url` + `".css"` for
partials][resolving for partials]. .
[resolving for partials]: https://github.com/sass/sass/blob/2c08156248496c75678826420aafb09e2ace9041/spec/at-rules/import.md#resolving-a-file-url-for-partials
> 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.
>
> When resolving for `@use`, this algorithm treats 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](#entrypoint) with the canonical URL `url` to CSS.
* Let `module` be the result of [loading](#loading-modules) `url` with the empty
configuration.
> Note that this transitively loads any referenced modules, producing a
> [module graph](#module-graph).
* Let `css` be the result of [resolving extensions](#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](#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](#configuration) `config`, and an
[import context](#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
or imported by `file` contains a variable declaration named `name` with a
`!default` flag at the root of the stylesheet, 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](#module).
* 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](#loading-modules) the module with
`rule`'s URL and `rule-config`.
* Associate `rule` with `module` in `uses`.
* When a `@forward` rule `rule` is encountered:
* If `rule` has an `AsClause` with identifier `prefix`:
* Let `rule-config` be an empty configuration.
* For each variable `variable` in `config`:
* If `variable`'s name begins with `prefix`:
* Let `suffix` be the portion of `variable`'s name after `prefix`.
* Add a variable to `rule-config` with the name `suffix` and with the
same value as `variable`.
* Otherwise, let `rule-config` be `config`.
* Let `forwarded` be the result of [loading](#loading-modules) the module with
`rule`'s URL and `rule-config`.
* [Forward `forwarded`](#forwarding-modules) with `file` through `module`.
* When an `@import` rule `rule` is encountered:
* Let `file` be the result of [loading][loading an import] `rule`'s URL.
* If `file` is `null`, throw an error.
* [Import `file`](#importing-files) into `import` and `module`.
* 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 `@extend`s
> 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.
[complex selectors]: https://drafts.csswg.org/selectors-4/#complex
* When a variable declaration `declaration` is encountered:
> This algorithm is intended to replace [the existing algorithm][old
> assigning-to-a-variable] for assigning to a variable.
[old assigning-to-a-variable]: ../spec/variables.md#executing-a-variable-declaration
* Let `name` be `declaration`'s [`Variable`](#member-references)'s name.
* If `name` is a [namespaced identifier](#member-references) *and*
`declaration` has a `!global` flag, throw an error.
* Otherwise, if `declaration` is outside of any block of statements, *or*
`declaration` has a `!global` flag, *or* `name` is a namespaced identifier:
* Let `resolved` be the result of [resolving a variable named
`name`](#resolving-members) using `file`, `uses`, and `import`.
* If `declaration` has a `!default` flag, `resolved` isn't null, *and*
`resolved`'s value isn't `null`, do nothing.
* Otherwise, if `resolved` is a variable in another module:
* Evaluate `declaration`'s value and set `resolved`'s value to the result.
* Otherwise:
* If `declaration` is outside of any block of statements, it has a
`!default` flag, *and* `config` contains a variable named `name` whose
value is not `null`:
* Let `value` be the value of `config`'s variable named `name`.
* Otherwise, let `value` be the result of evaluating `declaration`'s
value.
* If `name` *doesn't* begin with `-` or `_`, add a variable with name
`name` and value `value` to `module`.
> This overrides the previous definition, if one exists.
* Add a variable with name `name` and value `value` to `import`.
> This also overrides the previous definition.
* Otherwise, if `declaration` is within one or more blocks associated with
`@if`, `@each`, `@for`, and/or `@while` rules *and no other blocks*:
* Let `resolved` be the result of [resolving a variable named
`name`](#resolving-members) using `file`, `uses`, and `import`.
* If `resolved` is not `null`:
* If `declaration` has a `!default` flag and `resolved`'s value isn't
`null`, do nothing.
* Otherwise, let `value` be the result of evaluating `declaration`'s
value.
* If `name` *doesn't* begin with `-` or `_`, add a variable with name
`name` and value `value` to `module`.
> This overrides the previous definition, if one exists.
* Add a variable with name `name` and value `value` to `import`.
> This also overrides the previous definition.
> This makes it possible to write
>
> ```scss
> $variable: value1;
> @if $condition {
> $variable: value2;
> }
> ```
>
> without needing to use `!global`.
* Otherwise, if no block containing `declaration` has a [scope] with a
variable named `name`, set the innermost block's scope's variable `name` to
`value`.
* Otherwise, let `scope` be the scope of the innermost block such that `scope`
already has a variable named `name`. Set `scope`'s variable `name` to `value`.
[scope]: ../spec/spec.md#scope
* When a top-level mixin or function declaration `declaration` is encountered:
> Mixins and functions defined within rules are never part of a module's API.
* If `declaration`'s name *doesn't* begin with `-` or `_`, add `declaration` to
`module`.
> This overrides the previous definition, if one exists.
* Add `declaration` to `import`.
> This happens regardless of whether or not it begins with `-` or `_`.
* When a member use `member` is encountered:
* Let `scope` be the [scope] of the innermost block containing `member` such
that `scope` has a member of `member`'s name and type, or `null` if no such
scope exists.
* If `scope` is not `null`, return `scope`'s member of `member`'s name and
type.
* Otherwise, return the result of [resolving `member`](#resolving-members)
using `file`, `uses`, and `import`. If this returns null, throw an error.
* Finally:
* For each variable declaration `variable` with a `!global` flag in `file`,
whether or not it was evaluated:
* If `variable`'s name *doesn't* begin with `-` or `_` and `variable` is not
yet in `module`, set `variable` to `null` in `module`.
> This isn't necessary for implementations that follow the most recent
> [variables spec] and don't allow `!global` assignments to variables
> that don't yet exist. However, at time of writing, all existing
> implementations are in the process of deprecating the old `!global`
> behavior, which allowed `!global` declarations to create new
> variables.
>
> Setting all `!global` variables to `null` if they weren't otherwise set
> guarantees [static analysis] by ensuring that the set of variables a
> module exposes doesn't depend on how it was executed.
* Return `module`. Its functions, mixins, and CSS are now immutable.
[variables spec]: ../spec/variables.md
[static analysis]: #low-level
> 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.
>
> ```scss
> // 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.
> ```scss
> // 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](#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](#module) loaded by those rules, a member to resolve named `name` of
type `type`, and an [import context](#import-context) `import`:
> Note that this procedure only covers non-local member resolution. Local
> members that are scoped to individual blocks are covered in [Executing
> Files](#executing-files).
* If `name` is a [namespaced identifier](#member-references)
`namespace.raw-name`:
* Let `use` be the `@use` rule in `uses` whose namespace is `namespace`. If
there is no such rule, throw an error.
> Unlike other identifiers in Sass, module namespaces *do not* treat `-` and
> `_` as equivalent. This equivalence only exists for
> backwards-compatibility, and since modules are an entirely new construct
> it's not considered necessary.
* If `use` hasn't been evaluated yet, throw an error.
* Otherwise, 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 not "variable" and `file` contains a top-level definition of a
member of type `type` named `name`:
> A top-level variable definition will set the module's variable value rather
> than defining a new variable local to this module.
* If `import` contains a member `member` of type `type` named `name`, return
it.
> This includes member definitions within the current module.
* Otherwise, return `null`.
> This ensures that it's an error to refer to a local member before it's
> defined, even if a member with the same name is defined in a loaded
> module. It also allows us to guarantee that the referent to a member
> doesn't change due to definitions later in the file.
* Let `member-uses` be the set of modules in `uses` whose `@use` rules are
global, and which contain members of type `type` named `name`.
* Otherwise, if `import` contains a member `member` of type `type` named `name`:
* If `member-uses` is not empty, throw an error.
* Otherwise, return `member`.
* Otherwise, if `member-uses` contains more than one module, throw an error.
> This ensures that, if a new version of a library produces a conflicting
> name, it causes an immediate error.
* Otherwise, if `member-uses` contains a single module, return the member of
type `type` named `name` in that module.
* Otherwise, if the implementation defines a global member `member` of type
`type` named `name`, return that member.
> This includes the global functions and mixins defined as part of the Sass
> spec, and may also include other members defined through the
> implementation's host language API.
* Otherwise, return null.
### Forwarding Modules
The [`@forward`](#forward-1) rule forwards another [module](#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 an immutable module `forwarded`, a [source
file](#source-file) `file`, and a mutable module `module`.
* For every member `member` in `forwarded`:
* 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 at the top level of `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 `module` 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.
>
> ```scss
> // _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.
This algorithm takes a [source file](#source-file) `file`, an [import
context](#import-context) `import`, and a mutable [module](#module) `module`.
* If `file` is currently being executed, throw an error.
* Let `imported` be the result of [executing](#executing-files) `file` with the
empty configuration and `import` as its import context, except that if the
`@import` rule is nested within at-rules and/or style rules, that context is
preserved when executing `file`.
> Note that this execution can mutate `import`.
* Let `css` be the result of [resolving extensions](#resolving-extensions) for
`imported`, except that if the `@import` rule is nested within at-rules and/or
style rules, that context is added to CSS that comes from modules loaded by
`imported`.
> This creates an entirely separate CSS tree with an entirely separate
> `@extend` context than normal `@use`s of these modules. This means their CSS
> may be duplicated, and they may be extended differently.
* Add `css` to `module`'s CSS.
* Add `imported`'s [extensions](#extension) to `module`.
* If the `@import` rule is nested within at-rules and/or style rules, add each
member in `imported` to the local [scope].
* Otherwise, add each member in `imported` to `import` and `module`.
> Members defined directly in `imported` will have already been added to
> `import` in the course of its execution. This only adds members that
> `imported` forwards.
>
> Members from `imported` override members of the same name and type that have
> already been added to `import` and `module`.
> When a stylesheet contains only `@import`s without any `@use`s, the `@import`s
> 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][issue 631]).
[issue 631]: https://github.com/sass/sass/issues/631
We'll move all current built-in functions to built-in [modules](#module), 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 |
| `complement` | | sass:color | | `nth` | | sass:list |
| `invert` | | sass:color | | `set-nth` | | sass:list |
| `alpha` | | sass:color | | `join` | | sass:list |
| `adjust-color` | `adjust` | sass:color | | `append` | | sass:list |
| `scale-color` | `scale` | sass:color | | `zip` | | sass:list |
| `change-color` | `change` | sass:color | | `index` | | sass:list |
| `ie-hex-str` | | sass:color | | `list-separator` | `separator` | sass:list |
| | | | | | | |
| `map-get` | `get` | sass:map | | `feature-exists` | | sass:meta |
| `map-merge` | `merge` | sass:map | | `variable-exists` | | sass:meta |
| `map-remove` | `remove` | sass:map | | `global-variable-exists` | | sass:meta |
| `map-keys` | `keys` | sass:map | | `function-exists` | | sass:meta |
| `map-values` | `values` | sass:map | | `mixin-exists` | | sass:meta |
| `map-has-key` | `has-key` | sass:map | | `inspect` | | sass:meta |
| | | | | `get-function` | | sass:meta |
| `unquote` | | sass:string | | `type-of` | | sass:meta |
| `quote` | | sass:string | | `call` | | sass:meta |
| `str-length` | `length` | sass:string | | `content-exists` | | sass:meta |
| `str-insert` | `insert` | sass:string | | `keywords` | | sass:meta |
| `str-index` | `index` | sass:string | | | `module-variables` | sass:meta |
| `str-slice` | `slice` | sass:string | | | `module-functions` | sass:meta |
| `to-upper-case` | | sass:string | | | | |
| `to-lower-case` | | sass:string | | `selector-nest` | `nest` | sass:selector |
| `unique-id` | | sass:string | | `selector-append` | `append` | sass:selector |
| | | | | `selector-replace` | `replace` | sass:selector |
| | | | | `selector-unify` | `unify` | sass:selector |
| | | | | `is-superselector` | | sass:selector |
| | | | | `simple-selectors` | | sass:selector |
| | | | | `selector-parse` | `parse` | sass:selector |
| | | | | `selector-extend` | `extend` | sass:selector |
In addition, one built-in mixin will be added:
| Name | Module |
| ---------- | --------- |
| `load-css` | sass:meta |
The existing built-in functions `adjust-hue()`, `lighten()`, `darken()`,
`saturate()`, `desaturate()`, `opacify()`, `fade-in()`, `transparentize()`, and
`fade-out()` will not be added to any module. Instead, functions with the same
names will be added to the `sass:color` module that will always emit errors
suggesting that the user use `color.adjust()` instead.
> These functions are shorthands for `color.adjust()`. However, `color.adjust()`
> generally produces less useful results than `color.scale()`, so having
> shorthands for it tends to mislead users. The automated module migrator will
> migrate uses of these functions to literal `color.adjust()` calls, and the
> documentation will encourage users to use `color.scale()` instead.
>
> Once the module system is firmly in place, we may add new `color.lighten()`
> *et al* functions that are shorthands for `color.scale()` instead.
The `grayscale()`, `invert()`, `alpha()`, and `opacity()` functions in
`sass:color` will only accept color arguments, unlike their global counterparts.
> These global functions need to accept non-color arguments for compatibility
> with CSS functions of the same names. Since module namespacing eliminates the
> ambiguity between built-in Sass functions and plain CSS functions, this
> compatibility is no longer necessary.
Built-in modules will contain only the functions described above. They won't
contain any other [members](#member), CSS, or extensions. New members may be
added in the future, but CSS will not be added to existing modules.
> ```scss
> @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 (with all `_`s converted to `-`s) defined
in the module loaded by that rule (as quoted strings, without `$`) to the
current values of those variables.
> Variable names are normalized to use hyphens so that callers can safely work
> with underscore-separated libraries using this function the same as they can
> when referring to variables directly.
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 (with all `_`s converted to `-`s) defined
in the module loaded by that rule (as quoted strings) to function values that
can be used to invoke those functions.
> Function names are normalized to use hyphens so that callers can safely work
> with underscore-separated libraries using this function the same as they can
> when calling functions directly.
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](#loading-modules) `$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](#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.
>
> ```scss
> // 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()`, `mixin-exists()`, and
`get-function()` 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, or in the case of `get-function()`, it returns the function with the given
name from that module.
If the `$module` parameter is `null`, or when the `variable-exists()` function
is called, these functions will look 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. If multiple global `@use` rules define a member
of the given name and type, these functions will throw an error.
> We considered having the functions return `true` in the case of a conflicting
> member, but eventually decided that such a case was likely unexpected and
> throwing an error would help the user notice more quickly.
The `get-function()` function will throw an error if the `$module` parameter is
non-`null` *and* the `$css` parameter is truthy.
## Timeline
Our target dates for implementing and launching the module system are as
follows:
* **1 March 2019**: Support for `@use` without configuration or core libraries
landed in a Dart Sass branch, with specs in a sass-spec branch.
* **1 August 2019**: Full support for this spec landed in a Dart Sass branch, with
specs in a sass-spec branch.
* **1 September 2019**: Alpha release for Dart Sass module system support.
* **1 October 2019**: Stable release of Dart Sass module system support.
Although it would be desirable to have both Dart Sass and LibSass launch support
for the module system simultaneously, this hasn't proven to be logistically
feasible. As of August 2019, LibSass has not yet begun implementing the module
system, and there are no concrete plans for it to do so.
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 implementations launch 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): 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): 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.~~
**July 2022**: In light of the fact that LibSass was deprecated before ever
adding support for the new module system, the timeline for deprecating and
removing `@import` has been pushed back. We now intend to wait until 80% of
users are using Dart Sass (measured by npm downloads) before deprecating
`@import`, and wait at least a year after that and likely more before removing
it entirely.
**March 2023**: As week of Mar 06 to Mar 12, the npm downloads of the sass and
node-sass packages are 11,700,729 and 2,831,234 respectively, meaning we have
reached 80.5% adoption rate for Dart Sass, which is above the target for making
the deprecation `@import` current.