Permalink
Fetching contributors…
Cannot retrieve contributors at this time
434 lines (300 sloc) 15.5 KB

CSS Imports: Draft 3

(Issue, Changelog)

This proposal covers a long-awaited Sass feature: the ability to import plain CSS files from Sass. Although the original plan was to wait on this support until the module system was in place and support CSS imports only with @use, it has become desirable to support them sooner in order to have compatibility with the existing LibSass implementation. See Background for more details.

Table of Contents

Background

Historically, the reference implementations of Sass—first Ruby Sass, then Dart Sass—only supported importing other Sass files. However, LibSass supported importing CSS files as well, interpreting them as though they were SCSS. Although this technically violated the implementation guide's prohibition on unilaterally extending the language, these CSS imports were useful and were widely adopted in the Node.js community.

This became particularly clear when, at the language team's urging, LibSass added deprecation warnings for CSS imports and users were left without a suitable replacement. The language team came together to discuss the problem, and decided to move towards allowing CSS imports but forbidding the use of non-CSS features in the imported files. This proposal describes the specifics of that idea.

LibSass's behavior at time of writing is to import files with the extension .css at the same precedence level as those with the .scss and .sass extensions, and to throw an error if an import is ambiguous between a .css file and a .scss or .sass file.

Summary

This section is non-normative.

This proposal seeks to strike a balance between preserving compatibility with LibSass's existing behavior, and moving towards a more principled scheme for loading CSS. This is particularly important as we intend to allow @use to load CSS files without Sass features, so we want the existing CSS loading support to be as similar as possible.

Locating CSS files for import works similarly under this proposal as it does in LibSass currently: a relative .css file takes precedence over files with any extension on the load path, a .css file earlier on the load path takes precedence over a file with any extension later on the load path, and foo.css takes precedence over index/foo.scss.

The only difference in loading scheme occurs when an import is ambiguous between a .css file and a .scss or .sass file at the same path. LibSass currently produces an error here, but in order to maximize compatibility with existing Dart Sass (and Ruby Sass) behavior, this proposal has the .scss or .sass file taking precedence. This is not a breaking change to LibSass's behavior, since it only applies in situations that would previously have produced an error.

This proposal diverges significantly from LibSass in parsing the imported CSS files, though: it forbids all use of SCSS features in the parsed files. Most SCSS features produce errors (rather than compiling to plain, likely-invalid CSS) in order to help users who accidentally wrote SCSS in their CSS realize what's going wrong. However, features like @import that overlap with plain CSS continue to be rendered as CSS.

In order to avoid a sudden backwards-incompatible change in LibSass, this also includes a proposal for a set of deprecation warnings that can be added to LibSass's existing behavior to steer users away from using Sass features in their imported CSS without entirely breaking their build process.

Definitions

Basename

The basename of a URL is the final component of that URL's path.

Dirname

The dirname of a URL is the prefix of that URL up to, but not including, the beginning of its basename.

Canonical URL of a Stylesheet

The canonical URL of a stylesheet is a URL associated with that stylesheet that represents the location from which it was loaded.

The canonical URL for stylesheets are set by the algorithms for loading an import and loading an entrypoint path.

Semantics

This proposal defines a new algorithm for handling an @import rule. It is intended to replace the existing algorithm. I've also taken this opportunity to more explicitly specify the behavior of @import rules.

This proposal omits importers, which while important, are out of scope. Importers should be considered to effectively replace the algorithm for loading an import, possibly with another algorithm that calls the existing algorithm to handle filesystem imports.

Other than support for importing plain CSS, this algorithm is designed to accurately capture the current behavior of Ruby Sass and Dart Sass (when only filesystem importers are involved). It closely matches the implementation of Dart Sass, whereas Ruby Sass treats imported URLs as plain strings and operates on filesystem paths rather than file: URLs.

This proposal also defines a new algorithm for loading an entrypoint path, which is also intended to replace the existing algorithm. This defines how a Sass implementation should compile a file passed on the command line or through a programming language API.

Handling an Import Rule

To evaluate an @import rule:

  • For each of that rule's arguments:

    • If any of the following are true, the argument is considered "plain CSS":

      • The imported URL begins with http:// or https://.
      • The imported URL ends with .css.
      • The imported URL is syntactically defined as a url().
      • The argument has a media query and/or a supports query.

      Note that this means that imports that explicitly end with .css will continue to be treated as plain CSS @import rules, rather than importing stylesheets as CSS.

    • If the argument is "plain CSS":

      • Evaluate any interpolation it contains.

      • Add an @import with the evaluated string, media query, and/or supports query to the CSS AST.

    • Otherwise, let stylesheet be the result of loading the imported string.

      If this returns null, throw an error.

    • If an AST with the same canonical URL as stylesheet is currently being evaluated, throw an error.

    • Evaluate stylesheet in the global scope.

Loading an import

This algorithm takes a string, argument, and returns a Sass stylesheet.

  • Let root be the current stylesheet's canonical URL if its scheme is file, otherwise null.

  • Let bases be a list beginning with root if it's non-null, followed by the absolute file: URLs of all import paths.

  • For each base in bases:

    • Let url be the result of parsing argument as a URL with base as the base URL.

      If this returns a failure, throw that failure.

    • If url's scheme is not file, return null.

    • Let resolved be the result of resolving url.

    • If resolved is null:

    • If resolved is still null, continue to the next loop.

    • Let text be the contents of the file at resolved.

    • Let ast be:

      • The result of parsing text as SCSS if resolved ends in .scss.
      • The result of parsing text as the indented syntax if resolved ends in .sass.
      • The result of parsing text as CSS if resolved ends in .css.

      The algorithm for resolving a file: URL guarantees that resolved will have one of these extensions.

    • Return ast with the canonical URL resolved.

  • Return null.

Resolving a file: URL

This algorithm takes a URL, url, whose scheme must be file and returns either another URL that's guaranteed to point to a file on disk or null.

Resolving a file: URL for partials

This algorithm takes a URL, url, whose scheme must be file and returns either another URL that's guaranteed to point to a file on disk or null.

  • If url's basename begins with "_":

    • If a file exists on disk at url, return url.

      Otherwise return null.

  • Let partial be dirname(url) + "_" + basename(url).

  • If a file exists on disk at both url and partial, throw an error.

  • If a file exists on disk at url, return url.

  • If a file exists on disk at partial, return partial.

  • Return null.

Parsing text as CSS

This algorithm takes a string, text, and returns a Sass abstract syntax tree.

This algorithm is designed with two goals in mind:

  1. CSS imported from Sass should be as compatible with standard CSS as possible. In some cases we err even more towards CSS compatibility than SCSS does, because the CSS being imported is likely not written by someone who knows to avoid things that Sass interprets specially (such as certain @import URLs).

  2. We should provide clear and eager feedback to users who accidentally try to use Sass features in CSS imports. We don't allow these features, and we want users to know that through error messages rather than digging through generated CSS only to find that Sass features were passed through unmodified. This is a particular concern because LibSass has historically allowed the use of Sass features in CSS imports.

The algorithm for parsing text as CSS works like parsing text as SCSS, with some modifications. The following productions should produce errors:

  • Any at-rules that are defined in Sass and not in plain CSS. At the time of writing, this means:

    • @at-root
    • @content
    • @debug
    • @each
    • @error
    • @extend
    • @for
    • @function
    • @if
    • @include
    • @mixin
    • @return
    • @warn
    • @while
  • An @import that contains interpolation in the url(), the media query, or the supports query.

  • An @import that appears within a style rule or at-rule.

  • An @import with more than one argument.

  • A declaration followed by an open curly brace (that is, a nested declaration).

  • A style rule appearing within another style rule.

  • The parent selector &, either in a selector or a declaration value.

  • Placeholder selectors.

  • All built-in functions, excluding the following:

    • rgb()
    • rgba()
    • hsl()
    • hsla()
    • grayscale()
    • invert()
    • alpha()
    • opacity()

    Note that user-defined functions are not forbidden, whether they're defined using @function or through a host language API.

  • Any function called with keyword arguments or variable-length arguments.

  • Interpolation anywhere its contents would be evaluated. At the time of writing, this means:

    • At-rule values (including @media queries)
    • Declaration names
    • Declaration values
    • Style rule selectors
  • All SassScript operations except for:

    • /
    • not
    • or
    • and

    Note that although unary - is forbidden, the - that appears at the beginning of a number literal is part of that literal and thus allowed.

  • Parentheses in declaration values that aren't part of a CSS production.

  • Map literals.

  • The empty list literal (,).

  • Uses or declarations of Sass variables.

  • //-style ("silent") comments.

In addition, some productions should be parsed differently than they would be in SCSS:

  • All functions that don't produce errors should be parsed as plain CSS functions, regardless of whether a Sass function with that name is defined.

  • All @imports that don't produce errors should be parsed as static CSS imports.

  • The tokens not, or, and, and null should be parsed as unquoted strings.

    The / operation should be parsed as normal. Because variables, parentheses, functions that return numbers, and all other arithmetic expressions are disallowed, it will always compile to slash-separated values rather than performing division.

Loading an entrypoint path

This algorithm takes a string, path, that represents a file on the filesystem. It returns a Sass stylesheet.

  • Let url be the absolute file: URL corresponding to path.

  • Let text be the contents of the file at path.

  • Let ast be:

    • The result of parsing text as the indented syntax if url ends in .sass.
    • The result of parsing text as CSS if url ends in .css.
    • The result of parsing text as SCSS otherwise.

    The algorithm for resolving a file: URL guarantees that resolved will have one of these extensions.

  • Return ast with the canonical URL url.

Deprecation process

Because Dart Sass has no existing behavior for CSS imports–they'll just fail to find a file to import–it can just add the new behavior without need for a deprecation process. But LibSass parses its CSS imports as SCSS, which means it allows Sass features that this proposal forbids. In order to help users migrate away from using these features without a sudden breakage, LibSass should continue to parse CSS imports as SCSS, while emitting deprecation warnings for:

  • All productions that would produce an error according to the algorithm for parsing text as CSS.

  • All invocations of user-defined functions, whether they're defined using @function or through a host language API.

  • The not, or, and and operations.

  • The value null.

Where possible, LibSass should avoid emitting multiple deprecation warnings for the same expression.

For example, if a user writes -$var, producing a warning for the use of the variable already makes it clear that Sass features are in use. There's no additional value in emitting a warning for the unary minus operation.

These deprecation warnings should indicate that Sass features will cease to be allowed in files named .css in the future, and suggest that users either use plain CSS or rename their files to .scss.