Skip to content

Commit 6c59213

Browse files
committed
Start writing implementation documentation
1 parent ca2be2a commit 6c59213

File tree

2 files changed

+241
-0
lines changed

2 files changed

+241
-0
lines changed

CONTRIBUTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Want to contribute? Great! First, read this page.
99
* [Synchronizing](#synchronizing)
1010
* [File Headers](#file-headers)
1111
* [Release Process](#release-process)
12+
* [Package Structure](#package-structure)
1213

1314
## Before You Contribute
1415

@@ -233,3 +234,13 @@ few things to do before pushing that tag:
233234

234235
You *don't* need to create tags for packages in `pkg`; that will be handled
235236
automatically by GitHub actions.
237+
238+
## Package Structure
239+
240+
The structure of the Sass package is documented in README.md files in most
241+
directories under `lib/`. This documentation is intended to help contributors
242+
quickly build a basic understanding of the structure of the compiler and how its
243+
various pieces fit together. [`lib/src/README.md`] is a good starting point to get
244+
an overview of the compiler as a whole.
245+
246+
[`lib/src/README.md`]: lib/src/README.md

lib/src/README.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
## The Sass Compiler
2+
3+
* [Life of a Compilation](#life-of-a-compilation)
4+
* [Late Parsing](#late-parsing)
5+
* [Early Serialization](#early-serialization)
6+
* [JS Support](#js-support)
7+
* [APIs](#apis)
8+
* [Importers](#importers)
9+
* [Custom Functions](#custom-functions)
10+
* [Loggers](#loggers)
11+
* [Built-In Functions](#built-in-functions)
12+
* [`@extend`](#extend)
13+
14+
This is the root directory of Dart Sass's private implementation libraries. This
15+
contains essentially all the business logic defining how Sass is actually
16+
compiled, as well as the APIs that users use to interact with Sass. There are
17+
two exceptions:
18+
19+
* [`../../bin/sass.dart`] is the entrypoint for the Dart Sass CLI (on all
20+
platforms). While most of the logic it runs exists in this directory, it does
21+
contain some logic to drive the basic compilation logic and handle errors. All
22+
the most complex parts of the CLI, such as option parsing and the `--watch`
23+
command, are handled in the [`executable`] directory. Even Embedded Sass runs
24+
through this entrypoint, although it gets immediately gets handed off to [the
25+
embedded compiler].
26+
27+
[`../../bin/sass.dart`]: ../../bin/sass.dart
28+
[`executable`]: executable
29+
[the embedded compiler]: embedded/README.md
30+
31+
* [`../sass.dart`] is the entrypoint for the public Dart API. This is what's
32+
loaded when a Dart package imports Sass. It just contains the basic
33+
compilation functions, and exports the rest of the public APIs from this
34+
directory.
35+
36+
[`../sass.dart`]: ../sass.dart
37+
38+
Everything else is contained here, and each file and most subdirectories have
39+
their own documentation. But before you dive into those, let's take a look at
40+
the general lifecycle of a Sass compilation.
41+
42+
### Life of a Compilation
43+
44+
Whether it's invoked through the Dart API, the JS API, the CLI, or the embedded
45+
host, the basic process of a Sass compilation is the same. Sass is implemented
46+
as an AST-walking [interpreter] that operates in roughly three passes:
47+
48+
[interpreter]: https://en.wikipedia.org/wiki/Interpreter_(computing)
49+
50+
1. **Parsing**. The first step of a Sass compilation is always to parse the
51+
source file, whether it's SCSS, the indented syntax, or CSS. The parsing
52+
logic lives in the [`parse`] directory, while the abstract syntax tree that
53+
represents the parsed file lives in [`ast/sass`].
54+
55+
[`parse`]: parse/README.md
56+
[`ast/sass`]: ast/sass/README.md
57+
58+
2. **Evaluation**. Once a Sass file is parsed, it's evaluated by
59+
[`visitor/async_evaluate.dart`]. (Why is there both an async and a sync
60+
version of this file? See [Synchronizing] for details!) The evaluator handles
61+
all the Sass-specific logic: it resolves variables, includes mixins, executes
62+
control flow, and so on. As it goes, it builds up a new AST that represents
63+
the plain CSS that is the compilation result, which is defined in
64+
[`ast/css`].
65+
66+
[`visitor/async_evaluate.dart`]: visitor/async_evaluate.dart
67+
[Synchronizing]: ../../CONTRIBUTING.md#synchronizing
68+
[`ast/css`]: ast/css/README.md
69+
70+
Sass evaluation is almost entirely linear: it begins at the first statement
71+
of the file, evaluates it (which may involve evaluating its nested children),
72+
adds its result to the CSS AST, and then moves on to the second statement. On
73+
it goes until it reaches the end of the file, at which point it's done. The
74+
only exception is module resolution: every Sass module has its own compiled
75+
CSS AST, and once the entrypoint file is done compiling the evaluator will go
76+
back through these modules, resolve `@extend`s across them as necessary, and
77+
stitch them together into the final stylesheet.
78+
79+
SassScript, the expression-level syntax, is handled by the same evaluator.
80+
The main difference between SassScript and statement-level evaluation is that
81+
the same SassScript values are used during evaluation _and_ as part of the
82+
CSS AST. This means that it's possible to end up with a Sass-specific value,
83+
such as a map or a first-class function, as the value of a CSS declaration.
84+
If that happens, the Serialization phase will signal an error when it
85+
encounters the invalid value.
86+
87+
3. **Serialization**. Once we have the CSS AST that represents the compiled
88+
stylesheet, we need to convert it into actual CSS text. This is done by
89+
[`visitor/serialize.dart`], which walks the AST and builds up a big buffer of
90+
the resulting CSS. It uses [a special string buffer] that tracks source and
91+
destination locations in order to generate [source maps] as well.
92+
93+
[`visitor/serialize.dart`]: visitor/serialize.dart
94+
[a special string buffer]: util/source_map_buffer.dart
95+
[source maps]: https://web.dev/source-maps/
96+
97+
There's actually one slight complication here: the first and second pass aren't
98+
as separate as they appear. When one Sass stylesheet loads another with `@use`,
99+
`@forward`, or `@import`, that rule is handled by the evaluator and _only at
100+
that point_ is the loaded file parsed. So in practice, compilation actually
101+
switches between parsing and evaluation, although each individual stylesheet
102+
naturally has to be parsed before it can be evaluated.
103+
104+
#### Late Parsing
105+
106+
Some syntax within a stylesheet is only parsed _during_ evaluation. This allows
107+
authors to use `#{}` interpolation to inject Sass variables and other dynamic
108+
values into various locations, such as selectors, while still allowing Sass to
109+
parse them to support features like nesting and `@extend`. The following
110+
syntaxes are parsed during evaluation:
111+
112+
* [Selectors](parse/selector.dart)
113+
* [`@keyframes` frames](parse/keyframe_selector.dart)
114+
* [Media queries](parse/media_query.dart) (for historical reasons, these are
115+
parsed before evaluation and then _reparsed_ after they've been fully
116+
evaluated)
117+
118+
#### Early Serialization
119+
120+
There are also some cases where the evaluator can serialize values before the
121+
main serialization pass. For example, if you inject a variable into a selector
122+
using `#{}`, that variable's value has to be converted to a string during
123+
evaluation so that the evaluator can then parse and handle the newly-generated
124+
selector. The evaluator does this by invoking the serializer _just_ for that
125+
specific value. As a rule of thumb, this happens anywhere interpolation is used
126+
in the original stylesheet, although there are a few other circumstances as
127+
well.
128+
129+
### JS Support
130+
131+
One of the main benefits of Dart as an implementation language is that it allows
132+
us to distribute Dart Sass both as an extremely efficient stand-alone executable
133+
_and_ an easy-to-install pure-JavaScript package, using the dart2js compilation
134+
tool. However, properly supporting JS isn't seamless. There are two major places
135+
where we need to think about JS support:
136+
137+
1. When interfacing with the filesystem. None of Dart's IO APIs are natively
138+
supported on JS, so for anything that needs to work on both the Dart VM _and_
139+
Node.js we define a shim in the [`io`] directory that will be implemented in
140+
terms of `dart:io` if we're running on the Dart VM or the `fs` or `process`
141+
modules if we're running on Node. (We don't support IO at all on the browser
142+
except to print messages to the console.)
143+
144+
[`io`]: io/README.md
145+
146+
2. When exposing an API. Dart's JS interop is geared towards _consuming_ JS
147+
libraries from Dart, not producing a JS library written in Dart, so we have
148+
to jump through some hoops to make it work. This is all handled in the [`js`]
149+
directory.
150+
151+
[`js`]: js/README.md
152+
153+
### APIs
154+
155+
One of Sass's core features is its APIs, which not only compile stylesheets but
156+
also allow users to provide plugins that can be invoked from within Sass. In
157+
both the JS API, the Dart API, and the embedded compiler, Sass provides three
158+
types of plugins: importers, custom functions, and loggers.
159+
160+
#### Importers
161+
162+
Importers control how Sass loads stylesheets through `@use`, `@forward`, and
163+
`@import`. Internally, _all_ stylesheet loads are modeled as importers. When a
164+
user passes a load path to an API or compiles a stylesheet through the CLI, we
165+
just use the built-in [`FilesystemImporter`] which implements the same interface
166+
that we make available to users.
167+
168+
[`FilesystemImporter`]: importer/filesystem.dart
169+
170+
In the Dart API, the importer root class is [`importer/async_importer.dart`].
171+
The JS API and the embedded compiler wrap the Dart importer API in
172+
[`importer/node_to_dart`] and [`embedded/importer`] respectively.
173+
174+
[`importer/async_importer.dart`]: importer/async_importer.dart
175+
[`importer/node_to_dart`]: importer/node_to_dart
176+
[`embedded/importer`]: embedded/importer
177+
178+
#### Custom Functions
179+
180+
Custom functions are defined by users of the Sass API but invoked by Sass
181+
stylesheets. To a Sass stylesheet, they look like any other built-in function:
182+
users pass SassScript values to them and get SassScript values back. In fact,
183+
all the core Sass functions are implemented using the Dart custom function API.
184+
185+
Because custom functions take and return SassScript values, that means we need
186+
to make _all_ values available to the various APIs. For Dart, this is
187+
straightforward: we need to have objects to represent those values anyway, so we
188+
just expose those objects publicly (with a few `@internal` annotations here and
189+
there to hide APIs we don't want users relying on). These value types live in
190+
the [`value`] directory.
191+
192+
[`value`]: value/README.md
193+
194+
Exposing values is a bit more complex for other platforms. For the JS API, we do
195+
a bit of metaprogramming in [`node/value`] so that we can return the
196+
same Dart values we use internally while still having them expose a JS API that
197+
feels native to that language. For the embedded host, we convert them to and
198+
from a protocol buffer representation in [`embedded/value.dart`].
199+
200+
[`node/value`]: node/value/README.md
201+
[`embedded/value.dart`]: embedded/value.dart
202+
203+
#### Loggers
204+
205+
Loggers are the simplest of the plugins. They're just callbacks that are invoked
206+
any time Dart Sass would emit a warning (from the language or from `@warn`) or a
207+
debug message from `@debug`. They're defined in:
208+
209+
* [`logger.dart`](logger.dart) for Dart
210+
* [`node/logger.dart`](node/logger.dart) for Node
211+
* [`embedded/logger.dart`](embedded/logger.dart) for the embedded compiler
212+
213+
### Built-In Functions
214+
215+
All of Sass's built-in functions are defined in the [`functions`] directory,
216+
including both global functions and functions defined in core modules like
217+
`sass:math`. As mentioned before, these are defined using the standard custom
218+
function API, although in a few cases they use additional private features like
219+
the ability to define multiple overloads of the same function name.
220+
221+
[`functions`]: functions/README.md
222+
223+
### `@extend`
224+
225+
The logic for Sass's `@extend` rule is particularly complex, since it requires
226+
Sass to not only parse selectors but to understand how to combine them and when
227+
they can be safely optimized away. Most of the logic for this is contained
228+
within the [`extend`] directory.
229+
230+
[`extend`]: extend/README.md

0 commit comments

Comments
 (0)