sass/README.md

395 lines
18 KiB
Markdown
Raw Normal View History

# The Next-Generation Sass Module System
This repository houses a proposal for the `@use` directive and associated module
system, which is intended to be the headlining feature for Sass 4. 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/proposal.module-system
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 block-quoted "implementation note"s.
*Note: at the time of writing, the initial draft of the proposal is not yet
complete*.
## Background
The new `@use` directive is intended to supercede Sass's `@import` directive as
the standard way of sharing styles across Sass files. `@import` is the simplest
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, and there's no
encapsulation to allow them to keep moving pieces hidden.
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 is heavily based on
other languages' module systems, with Python and Dart being particularly strong
influences.
2015-12-12 00:58:26 +00:00
2015-12-12 01:12:08 +00:00
## Goals
### High-Level
These are the philosophical design goals for the module system at large. While
they don't uniquely specify a system, they do represent the underlying
motivations behind many of the lower-level design decisions.
2015-12-12 00:58:26 +00:00
* **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. This also includes the ability to "forward" public
APIs from another file.
2015-12-12 00:58:26 +00:00
* **Configuration**. Sass is unusual among languages in that it encourages 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,
sometimes at the top level. The module system should allow the user to
flexibly use modules with side-effects, and shouldn't force global
configuration.
2015-12-12 01:12:08 +00:00
### 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.
* **Using CSS files**. People often have CSS files that they want to bring into
their Sass compilation. Historically, `@import` has been unable to do this due
to its overlap with the plain-CSS `@import` directive and the requirement that
SCSS remain a CSS superset. With a new directive name, this becomes possible.
* **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,
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, at
least for the default configuration.
2015-12-12 01:29:53 +00:00
### Non-Goals
These are potential goals that we have explicitly decided to avoid pursuing for
various reasons. Some of them may be on the table for future work, but none are
expected to land in Sass 4.
* **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 the module system to include in Sass 4.
## Definitions
### Member
A *member* is anything that's defined, either by the user or the implementation,
that is identified by a Sass identifier. This currently includes variables,
mixins, functions, and placeholder selectors. Each member type has its own
namespace, so for example the variable `$name` doesn't conflict with the
placeholder selector `%name`.
All members have definitions associated with them, whose specific structure
depends on the type of the given member. Variables, mixins, and functions have
intuitive definitions, but placeholder selectors' definitions just indicate
which [module](#module) they come from.
There's some question of whether placeholders ought to be considered members,
and consequently [namespaced](#resolving-members) like other members. On one
hand, they're frequently used in parallel with mixins as the API exposed by a
library, which suggests that they should be namespaced like the mixins they
parallel. On the other hand, this usage is somewhat discouraged since it doesn't
treat them like selectors, and not namespacing them would potentially free up
characters like `.` or `:` to be used as namespace separators.
### CSS Tree
A *CSS tree* is an abstract CSS syntax tree. It has multiple top-level CSS
declarations like `@`-rules or rulesets. The ordering of the roots 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 top-level declarations.
### Configuration
A *configuration* is a set of variables with associated SassScript values. It's
used when executing a [source file](#source-file) to customize its execution. It
may be empty—that is, it may contain no variables.
Two configurations are considered identical if they contain the same variables,
and if each pair of variables with the same name has values that are `==` to one
another.
### Module
A *module* is an abstract collection of [members](#members) as well as a
[CSS tree](#css-tree), although that tree may be empty. Each module may have
only one member of a given type and name (for example, a module may not have two
variables named `$name`). To satisfy this requirement, placeholder selectors are
de-duplicated.
Each module is uniquely identified by the combination of a URI and a
[configuration](#configuration). A given module can be produced by executing the
[source file](#source-file) identified by the module's URI with the module's
configuration.
2016-01-16 01:35:32 +00:00
### Module Graph
Modules also track their `@use` directives, 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` directives. 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 fully executed
before that module is loaded. Although a module's members can be determined
without executing it, Sass allows code to be executed while loading a module,
which means those members may be executed.
### Source File
A *source file* is an entity uniquely identified by a URI. It can be executed
with a [configuration](#configuration) to produce a [module](#module). The names
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 configurations used for those modules.
There are five types of source file:
* Sass files, SCSS files, and CSS files are identified by file paths.
* Core libraries are identified by special URIs in an as-yet-undecided format.
* Implementations may define implementation-specific or pluggable means of
defining source files, which can use any URI.
Each one has different execution semantics that are beyond the scope of this
document. Note that some of these are not or may not actually be files on the
file system.
2016-01-16 03:08:27 +00:00
### 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).
## Syntax
The new directive will be called `@use`. The grammar for this directive is as
follows:
```
UseDirective ::= '@use' QuotedString (AsClause | NoPrefix)?
AsClause ::= 'as' Identifier
NoPrefix ::= 'no-prefix'
```
*Note: this only encompasses the syntax whose semantics are currently described
in this document. As the document becomes more complete, the grammar will be
expanded accordingly.*
`@use` directives must be at the top level of the document, and must come before
any directives other than `@charset`. Because each `@use` directive 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.
## Semantics
### Loading Modules
When encountering a `@use` directive, the first step is to load the associated
[module](#module). To do so:
* Look up the [source file](#source-file) with the given URI. The process for
doing this is out of scope of this document.
* If no such file can be found, throw a fatal error.
* If the source file has already been executed with an empty
[configuration](#configuration), use the module that execution produced. This
fulfills the "import once" low-level goal.
* Otherwise, execute that file with an empty configuration, and use the
resulting module.
> **Implementation note:**
>
> Although this specification only requires that modules be cached and reused
> when compiling a single entrypoint, modules are intentionally
> context-independent enough to store and re-use across multiple entrypoints, as
> long as no source files change. For example, if the user requests that all
> Sass files beneath `stylesheets/sass` be compiled, modules may be shared
> between those separate compilations.
Each module loaded this may have an associated **prefix**, which is a Sass
identifier that's used to identify the module's [member](#member)s within the
current file. No two `@use` directives in a given file may share a prefix,
although any number may have no prefix. The prefix for a given `@use`
directive's module is determined as follows:
* If the directive has an `as` clause, use that clause's identifier.
* If the directive has a `no-prefix` clause, then it has no prefix.
* If the module's URI doesn't match the regular expression
`(.*/)?([^/]+)(\.[^/]*)?`, the `@use` directive is malformed.
* Call the text captured by the second group of the regular expression the
*module name*.
* If the module name isn't a Sass identifier, the `@use` directive is malformed.
* If the module name followed by a hyphen is a initial substring of previous
`@use` directive's prefix, or if another `@use` directive's prefix followed by
a hyphen is an initial substring of the module name, the `@use` directive is
malformed.
* Use the module name.
This proposal follows Python and diverges from Dart in that `@use` imports
modules with a prefix by default. This is for two reasons. First, it seems to be
the case that language ecosystems with similar module systems either prefix all
imports by convention, or prefix 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. Prefixing by default tilts the balance towards
always prefixing, which mitigates this risk.
Second, a default prefix scheme drastically reduces the potential for
inconsistency in prefix choice. If the prefix is left entirely up to the user,
different people may choose to prefix `strings.scss` as `strings`, `string`,
`str`, or `strs`. This taxes the reusability of code and knowledge, and
mitigating it is a benefit.
2016-01-09 03:22:54 +00:00
### 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 set of [module](#module)s loaded via `@use` and a member
type and name to resolve:
* If the name begins with a module's prefix followed by a hyphen:
* Strip the prefix and hyphen to get the *unprefixed name*.
* If the module has a member of the given type with the unprefixed name, use
that member's definition.
* Otherwise, resolution fails.
* If a member of the given type with the given name has already been defined in
the current source file, use its definition.
* If such a member is defined later on in the file, resolution fails. This
ensures that any change in name resolution caused by reordering a file causes
an immediate error rather than an unexpected behavioral change.
* If such a member is defined in exactly one unprefixed module, use that
module's definition.
* Otherwise, if such a member is defined in more than one unprefixed module,
resolution fails. This ensures that, if a new version of a package produces a
conflicting name, it causes an immediate error.
* Otherwise, if such a member isn't defined in any unprefixed module, resolution
fails.
The hyphenated syntax (`namespace-name`) was chosen in preference to other
syntaxes (for example `namespace.name`, `namespace::name`, or `namespace|name`)
because it's likely to be compatible with existing code that uses manual
namespaces, and because it doesn't overlap with plain CSS syntax. This is
especially relevant for namespaced placeholder selectors, because most other
reasonable characters are already meaningful in selector contexts.
The downside to hyphens are that they look like normal identifiers, which makes
it less locally clear what's a namespace and what's a normal member name. It
also allows module prefixes to shadow other members, and introduces the
possibility of conflicting prefixes between modules.
2016-01-16 03:19:04 +00:00
### Resolving Extends
The module system also scopes the resolution of the `@extend` directive. This
helps satisfy locality, making selector extension more predictable than it is
using `@import`s.
Extension is scoped to CSS in [module](#module)s *transitively used* 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. Extending all transitively-used modules means that
the `@extend` affects exactly that CSS that is guaranteed to exist by the `@use`
directives.
Specifically, once all modules have been loaded, do a global pass to resolve the
extends.
* For each module that contains CSS (call it the *extended module*):
* Set this module's *virtual selectors* to the set of selectors in all of its
CSS rules. These virtual selectors remember the original selector they were
generated from.
* Take the subgraph of the module graph that can transitively reach the
extended module.
* For every module in this subgraph (call it the *extending module*) in
reverse [topological][] order:
* For each of the extended module's selectors:
* Take the corresponding virtual selector from each module used by the
extending module, if it has such a selector set.
* Create a new selector that matches the union of all elements matched by
these virtual selectors, and set it as a virtual selector of this
subgraph.
* Replace the selectors in the extended module's rules with the corresponding
virtual selectors of the [entrypoint module](#entrypoint-module).
[topological]: https://en.wikipedia.org/wiki/Topological_sorting
> **Implementation note:**
>
> This process as written is O(n²) for the number of modules in the compilation,
> and in a pathological case this is accurate. However, it's intended to be
> tightly constrained by the number and scope of the extensions the user chooses
> to write. Even a relatively naïve implementation should be able to keep it to
> O(n×m) for extending modules and extended modules. It's possible there's even
> an O(n) implementation that integrates extension with compilation, but that's
> beyond the scope of this document.
There is intentionally no way for a module to affect the extensions of another
module that doesn't transitively use it. This promotes locality, and matches the
behavior of mixins and functions in that monkey-patching is disallowed.