How should libraries be configured?
See original GitHub issueMany Sass libraries expose variables that control their styles. Today that’s often accomplished with !default
variables that downstream users override before importing the library in question. Because the new module system won’t have global variable scope, we need a new solution for this use-case.
The solution proposed in Draft 2 piggybacks on the concept of module mixins. When a module mixin is used, it can be passed parameters that override !default
variables in the module being included. However, this requires any downstream user who want to customize a module to do the multi-step dance of using that module as a mixin and then including that mixin. This is particularly annoying when a user wants to add a customization to an existing @use
.
Design Goals
I’d like to find a better solution, but there are a number of conflicting design goals at play:
-
The common case of configuring a few variables on an upstream library should be easy. This is not well-supported by the Draft 2 behavior.
-
It should be possible to configure a module using the full power of SassScript.
-
Locality should hold; separate stylesheets that don’t refer to one another shouldn’t affect one another.
-
Each module should only be loaded once. This is not well-supported by the Draft 2 behavior.
-
Existing libraries that use
!default
should be usable with the new module system.
It’s impossible to satisfy all these goals at once. For example, goal 3 is only satisfied if independent stylesheets can use the same module with different configurations, but that means it would need to be loaded multiple times, violating goal 4. We must find a compromise that works best for our users.
Open Questions
-
How important is backwards-compatibility, really?
We could, hypothetically, provide no mechanism to configure modules’
!default
variables. This violates goal 5 to such a degree that I suspect it’s impossible, but it would make everything else substantially easier so I think it’s worth mentioning. Other programming languages generally don’t do configuration before a module is loaded; the module exposes an API that’s configured as part of its invocation. We could push Sass libraries to do the same by exposing mixins that are invoked after configuration variables are set, or that just take configuration as arguments.Unfortunately, Sass libraries tend to have one big side effect when loaded: they produce CSS output. This would only work if libraries were willing and able to ensure they generated no CSS outside of mixins. This is generally a good practice, and one we’re talking about enforcing internally at Google, but making it a requirement to migrate to the module system seems likely to severely delay that migration.
-
How often do we expect multiple different configurations of the same module to be used in practice?
In other words, when deciding between goal 3 and goal 4, which should win? Can we get away with simply forbidding a module to be used multiple times with different configurations within a single Sass compilation?
Locality makes reasoning about styles easier for the user, but in this case it could also produce confusing behavior when the same module is included with multiple different configurations. CSS inherently has non-local behavior for style rules, and if users aren’t careful about scoping each use of the module separately, they could interfere in confusing and hard-to-debug ways.
Forbidding different configurations for the same module has some infrastructural benefits as well. It allows implementations (and the spec) to track only a single version of each module, which may or may not have configuration applied. It’s also the safer option in the sense that it would be much easier to add support for multiple configurations later than it would be to take that support away.
On the other hand, I can certainly imagine advanced users wanting to include multiple different configurations of the same module in different selector or media contexts. If that is a relevant use-case, users are probably doing it today, and failing to support that in the new module system could block their ability to adopt it.
-
Should users be able to write code before
@use
?To accommodate goal 1, configuration should be passed to a module as part of its
@use
statement. To accommodate goal 2, users should be able to customize a module’s variables using variable references and even function calls. However, Draft 2 says that@use
directives […] must come before any directives other than@charset
or@forward
. Because each@use
directive affects the namespace of the entire source file that contains it, whereas most other Sass constructs are purely imperative, keeping it at the top of the file helps reduce confusion.Goal 2 would mean making some
@use
directives interact with imperative Sass features, which means they’d need to be able to be written after variable declarations, and probably control rules and@function
rules as well. We could still require that@include
s and style rules appear after the first@use
, though.This could be abused to cause some strange behavior, though. You could write something like
@use "foo"; $foo.var: value; @use "bar";
and if
bar
also usesfoo
, it could hypothetically produce different CSS than it would otherwise. This would make parallelizing module loads difficult, which in turn could affect modular compilation. That said, we could work around this downside by simply forbidding assignments to namespaced variables until after the final@use
.
Straw Man
Here’s an example of how this could work, assuming we decide to support !default
and allow some code before @use
:
$base-color: #abc;
@use "library" with (
$base-color: #abc,
$secondary-color: darken(#abc, 10%),
);
Note that this this is adopting the same with
syntax that’s been proposed in https://github.com/sass/sass/issues/871#issuecomment-22420943.
Issue Analytics
- State:
- Created 5 years ago
- Comments:9 (8 by maintainers)
Top GitHub Comments
Yeah, using map syntax there seems strange when we refer to variables using the
$
prefix elsewhere in the module system syntax. Also, one important thing to keep in mind is thatload-css()
will probably be used much less frequently than@use
, so having it be a little strange isn’t the end of the world.That’s right. From that point forward, the
$layout-max-width
variable in thekickoff-grid
module would be1200
, for all files that refer to it.There are no global variables in the sense of variables that are visible at all times in all files, the way top-level variables are with
@import
. They’re replaced by module variables, which are only visible to files that use the modules they contain. However, module variables are global in the sense that they have the same value from anywhere they’re accessed.If the
$theme
variable is defined in a module namedtheme
, you could write this at the top of your entrypoint file:and then every other file that uses
theme
would see the version with$theme
set to"dark"
.