|
| 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 some 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 [`js/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/protofier.dart`]. |
| 199 | + |
| 200 | +[`js/value`]: js/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 | +* [`js/logger.dart`](js/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