Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

define behavior of @import in Cascading Style Sheet (CSS) modules #870

Open
dbaron opened this issue Mar 16, 2020 · 20 comments
Open

define behavior of @import in Cascading Style Sheet (CSS) modules #870

dbaron opened this issue Mar 16, 2020 · 20 comments
Labels
tag-tracker Group bringing to attention of the TAG, or tracked by the TAG but not needing response.

Comments

@dbaron
Copy link

dbaron commented Mar 16, 2020

There was a long discussion running from #759 (comment) (in which @dandclark lays out three options) through #759 (comment) on what the behavior of @import in CSS modules should be. This discussion didn't lead to a resolution.

Since that issue feels like it's getting too large for a single issue, I'm splitting it out into its own issue. (Sorry if that's not the preferred way of dealing with things here.)

The lack of a resolution of that issue is becoming problematic. Because Constructable Stylesheets wants to match the behavior of CSS modules, this led to a extended discussion in last week's CSS teleconference that could have been avoided if this issue had been settled. And as far a I can tell what it's waiting on is primarily the choice between whether option 2 or option 3 is preferable.

Personally I found @tabatkins's arguments in favor of option 2 relatively persuasive, because it requires less change to the CSS OM, and because many of the theorized advantages of option 3 address issues already handled in existing @import handling (even if it's not well specified today). (And I'd note that CSS always has the option of introducing a new syntax for doing an import as a module.)

cc @matthewp @justinfagnani @emilio @argyleink @domenic @rniwa @chrishtr who also participated in that discussion.

I'd note that I'm also filing this as an effort to get w3ctag/design-reviews#405 finished up.

@argyleink
Copy link

I still stand by option 3 and my original comment:

For me, it's less about performance advantages (theoretical or not) or ease of eng (less changes to CSS OM), but more about an advocation of less to think about/learn. Our styles are adapting to a new context/consumer, and it's just code, we can figure that part out. But, the closer it is to js modules (which the web community is certainly studying and learning intricately about), the less we'll have to teach when they reach to import CSS in their app logic.

Typical UX advocate angle though I guess, I feel we should optimize for the moment the community goes to import CSS, and make that seamless.

@tabatkins
Copy link

I have the same argument, but with the opposite conclusion. ^_^

People don't poke into the OM of a stylesheet very often anyway, and particularly don't recurse into imported sheets. But when they do, currently sheets are independent objects, and that won't change (no module graph in <link>). I think it wouldn't be great if stylesheets had a different OM behavior depending on whether they were <link>'d in or JS-imported.

@justinfagnani
Copy link
Contributor

I think there's problem with current semantics and module-like semantics mixing. A constructible stylesheet is shared and can be mutated and effect every scope it's applied to. @imported stylesheets being separate objects would break this behavior.

I would argue that given this setup, with a shared theme.css file:

theme.js:

p { color: red; }

my-component.css:

@import '../theme.css'; /* URLs would ideally be resolved to the referring CSS module's URL, not the document */
...

my-component.js:

import styles from './my-component.css';

That this code should result in the component's styles changing:

my-app.js:

import themeStyles from './theme.css';

themeStyles.cssRules[0].style.setProperty('color', 'blue');

because this is what would happen if the component imported theme.css directly. There shouldn't be a difference between the two.

@tabatkins
Copy link

A constructible stylesheet is shared and can be mutated and effect every scope it's applied to. @imported stylesheets being separate objects would break this behavior.

Can you elaborate on this? Passing around a single stylesheet object lets you share, yes. But constructing a new stylesheet from the same URL will not share things; it's an independent sheet.

(This is the sole unavoidable difference between css modules and constructed sheets; modules implicitly cache the top-level object, at least, by import url, so you'll get the same object from separate JS imports. But I don't think that argues for there being more differences of similar type between the two cases.)

@justinfagnani
Copy link
Contributor

justinfagnani commented Mar 16, 2020

I am saying that when JS-importing a URL from multiple places, you get the same shared CSSStyleSheet object. I'm saying that should apply to transitive imports, otherwise you get an inconsistency depending on how you import the module.

@tabatkins
Copy link

Right.

So we have some required, but incompatible behaviors:

  1. JS-imported top-level CSS modules will be shared across import sites, cached by the import url. (Since they're the same object, all the children sheets are the same object, but @importing the same sheet in multiple JS-imported sheets can go either way.)
  2. <link>-imported CSS sheets, and their @import'd child sheets, will be distinct stylesheet objects regardless of URL. (But might share their contents in copy-on-write semantics under the hood.)
  3. Distinct calls to the CSSStylesheet constructor will always return distinct stylesheet objects, regardless of contents. (But you can stash the sheet and share it manually across different locations.) (Whether imports in a constructed sheet are shared by url or not can potentially go either way.)

We can't change any of these, and if someone used to one encounters the other, they'll have problems.

So the decision here is not "which behavior should we be consistent with", because we can't be consistent, it's "where do we draw the boundary between the two behaviors".

I believe that there's no particularly compelling usability argument either way, as the usage of crawling and mutating stylesheet objects is fairly low in general.

As such, I think the best option is to push the boundary as far in one direction as possible, so as much of the API-space is consistent as possible except for the small "quarantined" bit of inconsistency we can't remove.

Given the current spread of behaviors, I think the right choice is to keep the "everything is a distinct object" behavior as much as possible, containing the "shared objects" behavior to happen only at the JS import API; sheets imported lower down aren't shared across imports.

This keeps the overall semantics as consistent as possible; JS itself caches JS-imported sheets by url, but beyond that everything remains the same as it always has.

@dandclark
Copy link
Contributor

dandclark commented Mar 18, 2020

I have some concerns in addition to the questions of distinct stylesheet identity discussed by @tabatkins and @justinfagnani above, and I am not entirely sure that we need to commit to either of these options at this point.

For option 2 as stated in this post (CSS modules are leaf nodes), there are really two sub-options:

2a) Pull in the full @import tree of the CSS module during the module graph link phase. Block module graph Evaluation until all of these are resolved, and if any of them 404 then cause module graph linking to fail. This approach would mean that we're blocking the module graph on fetches for things that are not in the module graph. Maybe this isn't a real problem, but intuitively something feels off to me here.

2b) Don't block module Evaluation on @imports and don't fail the module graph if they don't resolve. This approach gets us closes to how @import works in normal stylesheets. But, it breaks a key assumption that a module importer can normally make: by the time a module executes, all of its dependencies have already been loaded successfully. Allowing a CSS module's @imports to pop in after module Evaluation, or fail to load altogether, breaches this contract.

On the other hand Option 3 (CSS modules are non-leaf; @imports are treated as CSS modules) changes the behavior of @import in such a fundamental way that I wonder if it would be more appropriate to add this behavior under a new syntax, as a totally separate feature (as suggested by @dbaron in the OP) -- @module-import or something. This should alleviate concerns about @imports behaving differently when a stylesheet is consumed as a module vs in a <link>. It would create a clear break between CSS written for a module and CSS written in the 'classic' way, but there is precedent for that -- JS modules generally won't work right when pulled in by a classic script (e.g. import statements will be syntax errors).

If forced to choose between these options now I think I prefer (2a); it gets us close to the @import behavior that users are familiar while keeping the module semantics of statically resolving all dependencies. It also leaves us open to introducing non-leaf CSS modules in the future with an additional @module-import syntax.

However, I want to take a step back and ask if we really need support for @import in either constructible stylesheets or CSS modules at this point. Is there evidence that it will be widely used or that the lack of support will be an adoption-blocker for these features? Looking at https://chromestatus.com/metrics/feature/popularity, the use of CSSStyleSheetReplaceWithImport (<=0.000001%) is quite a bit lower than the use of CSSStyleSheetReplace overall (0.036757%) (edit: @chrishtr pointed out that CSSStyleSheetReplaceWithImport doesn't yet have date from the stable channel so these numbers aren't meaningful yet) .

I believe there was a concern that removing @import support from constructible stylesheets could be a difficult decision to reverse. But it's not clear to me why this should be the case. If the presence of an @import makes us throw a syntax error (in constructible stylesheets) or fail to load the module (in CSS modules), it seems that we would have a lot of flexibility to make future changes; no one could have reasonably taken a dependency.

So, if explicitly throwing/failing on @import is the path that leaves us the most flexibility, and we don't have a totally solid best path forward for doing otherwise, then I think it would be reasonable to just continue throwing. Once constructible stylesheets and CSS modules are shipped more broadly and gain wider use, we will have more information and more developer feedback about how they are used and how they interact, and this will give us a better basis to make decisions like introducing a new @import syntax. But do we need to constrain our choices at this stage by picking a strategy that we will be stuck with forever?

@tabatkins
Copy link

I actually assumed we'd go with 2c) block module evaluation on loading the @imports, but don't fail the module if an @import fails.

That way by the time you see the stylesheet, it's as fully loaded as it's going to be.

Failing the module due to a failed @import is a change from normal stylesheet behavior, where the rest of the stylesheet continues to work if an @import fails.

@chrishtr
Copy link

I would like to advocate for option (1). Reasons why:

  • I don't think the absence of @import will doom the feature to not being used, because many sites have build tooling for them that can optimize away @import, or can just not use it
  • @import has runtime performance problems due to causing multiple round-trips
  • Choosing options (2) or (3) implies substantial implementation and spec complexity
  • We could add support for @import or something like it based on web developer demand in the future, if really needed
  • The feature seems mostly about developer ergonomics / convenience (though I'm still learning about use-cases, could be wrong; see below)

I reviewed the many useful and interesting comments on this Stack Overflow page. My summary of the use cases:

  • Sharing CSS themes between pages
  • CSS for non-default media queries modes that don't appear on the regular page

The first use-case can I think be solved with developer build-time tooling and is about developer ergonomics only. The second use case is, I believe, about loading low-priority stylesheets later than the main ones, and can be solved by factoring these style sheets into later loads after the initial render.

@chrishtr
Copy link

chrishtr commented Mar 18, 2020

Oh BTW, CSSStyleSheetReplaceWithImport was only added a few days ago, and does not yet have data from the stable channel.

@dandclark
Copy link
Contributor

This was discussed in a TPAC breakout session today: https://www.w3.org/2020/10/29-components-minutes.html

We're not really any closer to making a decision on whether an @import inside a CSS module should or should not result in the @import being itself a CSS module. As such, we discussed moving forward with a V1 of CSS modules that does not allow @import. Shipping the feature, building developer excitement around it, and seeing how it is used in production and gathering feedback could help ensure we arrive at the correct final design choice without risking backwards compatibility problems.

@annevk had a concern that blocking @import initially and then opening it up later is potentially problematic, because suddenly imports are allowed to do fetches where they couldn't previously. The counterpoint to this was that the way browsers do speculative loading means that this sort of thing can already happen, so it's not really a new thing. So I'm considering that issue to be settled, but please let me know if there are still concerns there.

The remaining question then is how to block @import: either fail the module graph if one is present, or ignore it and emit a console warning.

The Constructable StyleSheets spec https://wicg.github.io/construct-stylesheets doesn't appear to have been updated, but @mfreed7's comment here summarizes the state of things after the most recent discussion about this in the CSS Working group (notes inlined in this comment).

Whatever we do with CSS modules should align with the behavior of CSSStyleSheet.replaceSync and CSSStyleSheet.replace. In my opinion, either throwing on @import or just ignoring @import should be sufficient not to introduce compatibility risks, and not throwing seems to align nicely with 'normal' CSS. But if one wants to argue that just ignoring @import without throwing is too risky, then that argument also applies to replace(), where we also may eventually want to support @import. So, one way to move forward could be this:

  1. Update https://wicg.github.io/construct-stylesheets so that it reflects the behavior for replace and replaceSync resolved in the CSSWG (ignore @import but don't stop parsing or throw an error).
  2. In the CSS Modules implementation, make the create a CSS module script steps use the replace steps in https://wicg.github.io/construct-stylesheets/#dom-cssstylesheet-replace to parse the CSS text and generate the stylesheet object. This includes failing the module load if replace throws an error. Referencing constructable stylesheets in this way codifies the matching behavior of these features right into the spec.
  3. Optionally circle back to the CSSWG to reconsider holistically whether replace, replaceSync, and CSS modules should all throw an error on @import instead of ignoring. Any change here would be applied to the constructible stylesheets spec, and CSS modules would get the new behavior automatically.

@mfreed7
Copy link

mfreed7 commented Oct 30, 2020

Thanks @dandclark, that sounds like what I heard at the meeting also. I agree with all three of your points. For #3, @emilio had some good feedback into the change to Constructable Stylesheets, pushing for the current “no error” implementation. Perhaps he can comment here about this.

@emilio
Copy link

emilio commented Oct 30, 2020

I think my main argument for making it not error is that that's how most other CSS errors work, in particular parser errors and other kind of "syntax that doesn't (yet, maybe) work", like unknown at-rules and such. In particular, if someone writes syntax that right now isn't accepted by the parser and we later start accepting it, we'd start throwing, which seems bad.

After a bit of testing, browsers seem to be very lenient with @import syntax (which I don't think is really correct per spec, fwiw...). For example I would've expected the syntax @import "url" supports(display: grid); to get ignored (because it's not implemented yet in browsers), but instead it just gets treated as @import "url" not all;.

That alleviates a bit my concern, though it is still a problem with other things we want to do with @import / url functions, like @import url("foo.css" crossorigin);, for example, which was discussed not too long ago. Thus, I still think not throwing is the consistent thing to do.

@castastrophe
Copy link

The remaining question then is how to block @import: either fail the module graph if one is present, or ignore it and emit a console warning.

+1 I agree with the suggestion that we ignore imports and emit a console warning. Clear and keeps things working otherwise.

@yinonov
Copy link

yinonov commented Sep 1, 2021

couldn't tell where else to ask, but this introduces a great practice to dynamically set styles.
now I know this ain't new but still, adoptedStyleSheets can only be assigned. I think it would be very useful to add / remove specific id'd stylesheets.
great use-case is toggling theming stylesheets

@factoidforrest

This comment was marked as off-topic.

@junaga

This comment was marked as abuse.

@junaga
Copy link

junaga commented Feb 23, 2023

Literally nobody uses @import in CSS. It is okay to break sites that rely on unspecified behavior. It is okay to break 2% of 2% of 2% of sites with a new major browser release. Google does it.

@trusktr
Copy link
Contributor

trusktr commented Sep 1, 2023

This is important because one of the main reasons someone could want to move away from doing this,

this.shadowRoot.innerHTML = `
  <div> ... </div>
  <style>
    @import "/css/animations/wobble.css" /* (build tools not aware of arbitrary code in strings) */

    div {
      animation: 1s ease wobble;
    }
  </style>
`

in their custom elements, is that moving to using JavaScript import can easily allow a build tool can inline all the CSS stylesheets (in a standard way compared to scanning arbitrary strings for @import statements) instead of the browser having to fetch all the re-usable bits of CSS over separate network requests (one per @import).

F.e. they could easily change that code to this:

/* me-el.css */
@import "/css/animations/wobble.css" /* build tools inline official syntax */

div {
  animation: 1s ease wobble;
}
import sheet from "./my-el.css" with { type: 'css' } // build tool inlines

// ...
this.shadowRoot.innerHTML = `
  <div> ... </div>
`
this.shadowRoot.adoptedStyleSheet = [sheet]

At the same time, the workaround is not very difficult, they could also write the following to get things working with CSS Modules as spec'd today (f.e. in Chrome):

/* me-el.css */
div {
  animation: 1s ease wobble;
}
import wobble from "/css/animations/wobble.css" with { type: 'css' } // build tool inlines
import sheet from "./my-el.css" with { type: 'css' } // build tool inlines

// ...
this.shadowRoot.innerHTML = `
  <div> ... </div>
`
this.shadowRoot.adoptedStyleSheet = [wobble, sheet]

but the workaround feels less aligned with the intrinsics of CSS.


Perhaps making an @import alternative (f.e. similar LESS's import-once) in order to solve some of these problems in an alternative way, and leaving @import as-is so it is not confusing depending on usage (people do inspect the CSS OM, we can't prevent it), would be a better approach:

If we agree to keep @import as-is, and agree to later add an alternative that fixes some of the issues outlined here and in that thread, then @import can simply do what it does exactly like inside a <style> element, and then the new alternative can be added for use later.

This might get more people further along today, than having them wait on debating how to change @import.

@junaga
Copy link

junaga commented Sep 1, 2023

I realize this is a tangent at best, but, does anyone know of a way in webpack to compile away the @imports by inlining before loading / as part of the compilation pipeline?

Give them 3 more years, and eventually, they will settle on an implementation. Hopefully, Webpack will be considered legacy by then.

btw, in the time this issue was open, ChatGPT came out, and NVIDIA moved the NASDAQ by 2% iteself

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
tag-tracker Group bringing to attention of the TAG, or tracked by the TAG but not needing response.
Projects
None yet
Development

No branches or pull requests