Sass 1.23 - La versión que cambió las reglas del juego
Tags: webdev, css
Published: 2020-09-07 05:45:12

Se está por cumplir un año ya desde el lanzamiento de la versión que quizás introdujo los cambios más significativos en Sass como lenguaje desde la migración de su compilador de Ruby a Dart, y he notado que, sin embargo, este suceso ha pasado bastante desapercibido. Fuera del sitio oficial de Sass, los posteos en internet hablando acerca de esto, así como los videos en YouTube, son escasos por no decir nulos, y ni hablemos de encontrar algo en español. Es por eso que decidí ordenar de la forma más prolija y detallada posible pero sin agobiar al lector, las features más trascendentales que introduce Sass 1.23, y que, según sus desarrolladores, llegaron para quedarse.

## Démosle la bienvenida al Module System

Sin lugar a dudas, el salto más importante que ha dado esta versión, y por el cual se la considera una versión bisagra es debido a la deprecación completa (aunque gradual) de la regla `@import` para la carga de archivos, cediendo el paso al nuevo **Module System**, cuya implementación gira en torno a la nueva at-rule `@use`.

Previo a esta versión, cuando usábamos `@import`, era imposible saber dónde habían sido definidas las funciones, mixins y variables (llamados "miembros" por convención, y así nos referiremos de ahora en adelante) que estábamos invocando dentro de un archivo (a los cuales a partir de ahora es correcto referirse como "namespaces"), ya que el solo hecho de hacer un `@import` los volvía disponibles dentro de todos los archivos que fueran llamados a continuación. 

Esto era un dolor de cabeza para muchos desarrolladores, y un potencial problema en proyectos grandes donde podía llegar a ocurrir que se definieran dos miembros con el mismo nombre. Además, nos obligaba a prestar particular atención al orden de importación, cuando existían múltiples imports, si queríamos que determinados miembros estuvieran disponibles cuando los necesitábamos.

Con esto, de paso, Sass se despega por completo de la colisión confusa con el `@import` de CSS.

## Cambios de la versión

Estos son algunos de los pricipales cambios y mejoras que introdujo la versión 1.23:

- Importación de archivos como módulos mediante la regla `@use`. Mejora el encapsulamiento ya que los miembros de éste solo estarán disponibles dentro del archivo que los está usando, a diferencia de `@import` que tenía un scope global.
  
  ```scss
  // some-file.scss
  @import "some-module"; // ❌ disponible dentro de este archivo
  // y de todos los que se importen después 
  ```
  
  ```scss
  // some-file.scss
  @use "some-module"; // ✅ disponible solo dentro de este archivo 
  ```

- Con la regla `@forward`, se podrá hacer que los miembros de otro archivo estén disponibles cuando el archivo actual sea importado con `@use`, creando de esta manera una API extendida. Sin embargo, el archivo que realiza el `@forward` no podrá acceder a los miembros de éste, a diferencia de `@use`, si no que actuará como un simple intermediario. Es decir que podemos hacer lo siguiente:
  
  ```scss
  // mixins.scss
  @mixin expand {
      display: block;
      width: 100%;
  }
  ```
  
  ```scss
  // foo.scss
  @forward "mixins";
  
  .foo {
      align-items: center;
      background-color: peru;
      display: flex;
      flex-direction: column-reverse;
  }
  ```
  
  ```scss
  // some-other-file.scss
  @use "foo";
  
  .expanded {
      @include foo.expand;
  }
  ```
  
  Mientras que esto nos daría un error:

  ```scss
  // palette.scss
  $colors: (
      primary: #007BFF,
      secondary: #6C757D,
      success: #DC3545
  );
  ```
  
  ```scss
  // functions.scss
  @use "sass:map";
  @forward "palette";
  
  @function color($prop) {
      @return map.get(palette.$colors, $prop);
      // => Error: There is no module
      // with the namespace "palette".
  }
  ```
  
  Es posible, de ser necesario, decidir de manera selectiva qué variables, mixins o funciones de determinado namespace estarán disponibles, de la siguiente manera:
  
  ```scss
  // src/_list.scss
  $horizontal-list-gap: 2em;
  
  @mixin list-reset {
    margin: 0;
    padding: 0;
    list-style: none;
  }
  
  @mixin list-horizontal {
    @include reset;
  
    li {
      display: inline-block;
      margin: {
        left: -2px;
        right: $horizontal-list-gap;
      }
    }
  }
  ```
  
  ```scss
  // bootstrap.scss
  @forward "src/list" hide list-reset, $horizontal-list-gap;
  ```
  
  ...o de forma inversa con `show`:
  
  ```scss
  @forward "src/list" show list-horizontal;
  ```
  
  Y también algo que es muy interesante es que podemos agregar un prefijo para todos los miembros del namespace:
  
  ```scss
  // src/_list.scss
  @mixin reset {
    margin: 0;
    padding: 0;
    list-style: none;
  }
  ```
  
  ```scss
  // bootstrap.scss
  @forward "src/list" as list-*;
  ```
  
  ```scss
  // styles.scss
  @use "bootstrap";
  
  li {
    @include bootstrap.list-reset;
  }
  ```

- La at-rule `@extend` se limita al scope del archivo actual cuando se utiliza `@use`. A diferencia de antes, que al usar `@import` el `@extend` quedaba disponible globalmente, en ocasiones haciendo difícil predecir qué reglas estábamos extendiendo.

- Usando los **modificadores de acceso** `-` ó `_` podemos hacer que un miembro sea privado, lo que significa que solo podrá ser utilizado dentro del propio archivo que lo define: `$_private-var: value` ó `$-private-var: value`.

- **Namespaces** personalizados mediante el uso de `as`. Por ejemplo:
  
  ```scss
  @use "functions" as fn;
  
  .button {
      background-color: fn.color("success");
  }
  ```
  
  ...o como **top-level module** (usar con precaución):
  
  ```scss
  @use "functions" as *;
  
  .button {
      background-color: color("success");
  }
  ```
  
  Esta última forma tiene la limitación de que, si usamos otro módulo como "top-level module" y este contiene, también, una función llamada `color()`, el compilador arrojará un error.

- También podemos importar un módulo sobreescribiendo el valor de una o más de sus variables. Para ello, primero tenemos que asignarle un valor por defecto al momento de definirla:
  
  ```scss
  // bootstrap.scss
  $paragraph-margin-bottom: 1rem !default;
  
  p {
    margin-top: 0;
    margin-bottom: $paragraph-margin-bottom;
  }
  ```
  
  Y luego llamar el archivo de esta manera, utilizando la cláusula `with` que recibirá un [mapa](https://sass-lang.com/documentation/values/maps) con las variables que queramos pisar:
  
  ```scss
  @use "bootstrap" with (
    $paragraph-margin-bottom: 1.2rem
  );
  ```

- Las **funciones del core de Sass** ahora son accedidas a través de **módulos nativos**: `sass:color`, `sass:list`, `sass:map`, `sass:math`, `sass:meta`, `sass:selector`, y `sass:string`. Las cargaremos en nuestra hoja de estilos con `@use` (por ej: `@use "sass:map"`) y las usaremos como cualquier otro módulo, con la sintáxis `namespace.function()`. Esto se hizo para evitar la colisión con funciones nativas de CSS, lo que les permitirá, en el futuro, agregar nuevas de manera segura.

- Se incorporaron, además, algunos **mixins al core**, que vienen a solucionar algunas malas prácticas del pasado, como los `@import` anidados, tal es el caso de `meta.load-css($url, $with: ())` que sirve para cargar una hoja de estilos de manera dinámica, recibiendo dos parámetros, el primero `$url` es un string con la url de dicho CSS, y el segundo `$with: ()` es opcional y deberá ser un mapa con la configuración, igual al del ejemplo anterior solo que en este caso las variables no llevan $ adelante.  También tenemos `meta.module-variables()` y `meta.module-functions()` para acceder a las variables o funciones respectivamente de un determinado módulo. Están disponibles de la misma manera que las built-in functions: `@use "sass:meta"`.

## ¿Es seguro de usar?

Desde el lanzamiento de esta versión, ya ha pasado casi un año y han salido 3 versiones menores posteriormente (actualmente se encuentra en la 1.26), por lo que es 100% seguro de utilizar en cualquier proyecto nuevo. Sin embargo, hemos de esperar que no nos funcionen las nuevas features en proyectos de más de un año de antigüedad.

Si bien esta versión es completamente retro-compatible, los desarrolladores avisan que `@import` será eventualmente removido por completo de Sass, pero que dejarán transcurrir un tiempo prudencial para que tengamos chance de migrar.

Por suerte, Sass pone a nuestra disposición una **herramienta de migración automatizada**, a través de línea de comandos: **sass-migrator**. Existen varias formas de instalarla, las cuales [se detallan aquí](https://sass-lang.com/documentation/cli/migrator).

## Migrando nuestro código

Seguiremos las instrucciones para [Sass: Migrator](https://sass-lang.com/documentation/cli/migrator) de la página oficial, y una vez que tengamos instalada la CLI, simplemente navegamos a la carpeta de nuestro proyecto y hacemos:

```bash
sass-migrator module --migrate-deps <path/to/style.scss>
```

El flag `--migrate-deps` lo que hace es decirle que migre no solo el archivo específico que le estamos indicando si no además todos los archivos que pudiera estar importando.

Podemos pasarle el flag `--verbose` para que la consola nos de output sobre los cambios que va realizando sobre los archivos, y el flag `--dry-run` si queremos que corra el comando de manera "simulada" sin ejecutar realmente los cambios, lo cual tiene sentido usándolo en combinación con el anterior.

Si bien lo anterior bastará para la mayoría de los casos, hay un último flag que puede llegar a ser muy útil para quienes desarrollen alguna biblioteca: `--forward`, que acepta los valores `none`(valor por defecto), `all` y `prefixed`. Si elegimos `--forward=all` permitiremos que el usuario de nuestra biblioteca pueda acceder a toda la API (excepto miembros privados) con un solo `@use`, ya que lo que hará es un `@forward` de todos los archivos. En el caso de `prefixed`, básicamente hace un `@forward` únicamente de los miembros a los que hayamos puesto un prefijo, el cual debemos explicitar de manera obligatoria con el flag adicional `--remove-prefix`; la herramienta eliminará automáticamente este prefijo. Antes de los módulos, se solía agregar estos prefijos para evitar colisiones de nombres. El ejemplo del sitio de Sass, lo grafica a la perfección:

```bash
$ cat style.scss
@import "theme";

@mixin app-inverted {
  color: $app-bg-color;
  background-color: $app-color;
}
```

```bash
$ sass-migrator --migrate-deps module --remove-prefix=app- style.scss
$ cat style.scss
@use "theme";

@mixin inverted {
  color: theme.$bg-color;
  background-color: theme.$color;
}
```

Debo decir que he realizado unas cuantas migraciones y, en todos los casos, la herramienta es bastante efectiva e inteligente al realizar su labor. En algúna ocasión he tenido que refactorizar un poco a mano pero han sido cosas mínimas que no me han llevado más de unos minutos. Aun así, es recomendable primero probar siempre con `--dry-run --verbose`, para nuestra tranquilidad.

Referencias: 
- https://sass-lang.com/blog/the-module-system-is-launched 
- https://css-tricks.com/introducing-sass-modules/
- https://seesparkbox.com/foundry/dart_sass_update_version_1.23.0

[![Invitame un café en cafecito.app](https://cdn.cafecito.app/imgs/buttons/button_1.svg)](https://cafecito.app/fena)