[csswg-drafts] [css-cascade] Specify how `@import` cycles work (#9171)

evanw has just created a new issue for https://github.com/w3c/csswg-drafts:

== [css-cascade] Specify how `@import` cycles work ==
I'm the author of esbuild and I'm trying to write a CSS bundler. As far as I can tell, there is no CSS specification that explains what to do when there's an import cycle. This is surprising to me so I'm guessing I'm wrong and there is such a specification. If so, please disregard this issue!

## Background

Here's everything I could find about how to interpret `@import` rules from the [CSS cascade specification](https://drafts.csswg.org/css-cascade-5/) (where the behavior of `@import` appears to be specified):

<details>
<summary><i>(Click for quotes from the specification)</i></summary>
<p>

From https://drafts.csswg.org/css-cascade-5/#at-import:

> ### 2. Importing Style Sheets: the `@import` rule
>
> The `@import` rule allows users to import style rules from other style sheets. If an `@import` rule refers to a valid stylesheet, user agents must treat the contents of the stylesheet as if they were written in place of the `@import` rule

and from https://drafts.csswg.org/css-cascade-5/#import-processing:

> ### 2.2. Processing Stylesheet Imports
>
> When the same style sheet is imported or linked to a document in multiple places, user agents must process (or act as though they do) each link as though the link were to an independent style sheet.

</p>
</details>

This doesn't say anything about how cycles work. It also doesn't disallow them, and indeed they are allowed by all browsers. A literal interpretation of the specification would cause a hang when a cycle is encountered due to infinite expansion. One such implementation is `postcss-import` which can have this exact problem: https://github.com/postcss/postcss-import/issues/462. But browsers don't hang, so they must be doing something else. The only hint that I've found about how browsers might do this is in the description for issue #4287:

> That makes sense — although I’d found no spec text specifically addressing circularity, it seems implicitly permitted because I think the implementation only needs to ‘insert’ them once, in the ‘furthest-down’ place they’re referenced, in order to achieve the specified behavior.

This makes sense. You can traverse the import graph in reverse to find this "furthest-down" ordering and you can handle cycles by not visiting a given node more than once. That lets you handle cases like this:

<details>
<summary><i>(Click for example code with a cycle)</i></summary>
<p>

* `entry.css`

    ```css
    @import "foreground.css";
    @import "background.css";
    ```

* `foreground.css`

    ```css
    @import "reset.css";
    body {
      color: red;
    }
    ```

* `background.css`

    ```css
    @import "reset.css";
    body {
      background: green;
    }
    ```

* `reset.css`

    ```css
    @import "entry.css";
    body {
      color: green;
      background: red;
    }
    ```

</p>
</details>

This example should set both the body's `color` and `background` to green. Following the "furthest-down" algorithm gives the order `foreground.css` + `reset.css` + `background.css` + `entry.css` which successfully reproduces the behavior observed in browsers. This is what esbuild currently implements.

## Problem 1

The "furthest-down" algorithm doesn't actually work. The problem is that `@layer` is specified to take effect in the "furthest-up" location instead of the "furthest-down" location (defined [here](https://drafts.csswg.org/css-cascade-5/#layer-ordering)). For example, this doesn't work:

<details>
<summary><i>(Click for example code with this edge case)</i></summary>
<p>

* `entry.css`

    ```css
    @import url("a.css");
    @import url("b.css");
    @import url("a.css");
    ```

* `a.css`

    ```css
    @layer a {
      body {
        background: red;
      }
    }
    ```

* `b.css`

    ```css
    @layer b {
      body {
        background: green;
      }
    }
    ```

</p>
</details>

Following the "furthest-down" algorithm gives the order `b.css` + `a.css` + `entry.css` which is incorrect. It causes layer `b` to come before `a` (and therefore the color `red` wins) while in the browser layer `a` comes before `b` (and therefore the color `green` wins).

## Problem 2

Browsers don't even have consistent behavior in the presence of import cycles. This is understandable because the behavior import cycles doesn't appear to be specified, but it seems undesirable to leave this unspecified and for browsers to have divergent behavior. Here's an example of a case with inconsistent behavior:

<details>
<summary><i>(Click for example code with inconsistent browser behavior)</i></summary>
<p>

* `entry.css`

    ```css
    @import url("b.css");
    @import url("c.css");
    ```

* `a.css`

    ```css
    @import url("red.css");
    @import url("b.css");
    ```

* `b.css`

    ```css
    @import url("green.css");
    @import url("a.css");
    ```

* `c.css`

    ```css
    @import url("a.css");
    ```

* `red.css`

    ```css
    body {
      background: red;
    }
    ```

* `green.css`

    ```css
    body {
      background: green;
    }
    ```

</p>
</details>

This CSS sets the body to green in Chrome but red in Firefox. Following the "furthest-down" algorithm gives the order `red.css` + `green.css` + `b.css` + `a.css` + `c.css` + `entry.css` which results in a green body. But it's not clear which browser is "correct" without a specification.

## Conclusion

It would be helpful for me if this behavior was specified. Then I could build esbuild to a specification instead of what I have been doing, which is trying to reverse-engineer what the behavior is supposed to be from how existing browsers work. What got me to file this issue was a) the realization that browsers aren't even consistent so reverse-engineering won't work and b) the realization that I have no idea how to implement `@layer` in combination with `@import` in a CSS bundler, especially in the presence of cycles. I'm creating a new issue for this because while #4287 is related, it's discussing how the JavaScript API should behave while I'm interested in how CSS is supposed to behave.


Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/9171 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Tuesday, 8 August 2023 04:01:12 UTC