There was a long discussion running from https://github.com/w3c/webcomponents/issues/759#issuecomment-490256626 (in which @dandclark lays out three options) through https://github.com/w3c/webcomponents/issues/759#issuecomment-497893148 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.
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.
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.
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.
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.)
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.
Right.
So we have some required, but incompatible behaviors:
@importing the same sheet in multiple JS-imported sheets can go either way.)<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.)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.
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?
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.
I would like to advocate for option (1). Reasons why:
I reviewed the many useful and interesting comments on this Stack Overflow page. My summary of the use cases:
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.
Oh BTW, CSSStyleSheetReplaceWithImport was only added a few days ago, and does not yet have data from the stable channel.
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:
replace and replaceSync resolved in the CSSWG (ignore @import but don't stop parsing or throw an error).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.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.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.
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.
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.
Most helpful comment
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.
@importedstylesheets being separate objects would break this behavior.I would argue that given this setup, with a shared theme.css file:
theme.js:
my-component.css:
my-component.js:
That this code should result in the component's styles changing:
my-app.js:
because this is what would happen if the component imported
theme.cssdirectly. There shouldn't be a difference between the two.