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

RFC: Support for hashing more types of identifiers #390

Open
devongovett opened this issue Apr 7, 2023 · 8 comments
Open

RFC: Support for hashing more types of identifiers #390

devongovett opened this issue Apr 7, 2023 · 8 comments

Comments

@devongovett
Copy link

Hello! I maintain Lightning CSS, a CSS processor which implements CSS modules. Since the CSS modules "spec" was last updated, there have been many new features added to CSS itself, which I think could be affected by CSS modules hashing. I'd like to implement some of this in Lightning CSS, but want to get some consensus on the syntax here so that it remains interoperable between tools.

<custom-ident>

The CSS specification defines the <custom-ident> type for author-defined identifiers. This is used for keyframe animation names and the animation-name property, CSS grid line names, @container names, @counter-style names, and probably more future features. At the moment, CSS modules officially mentioned @keyframes, but not these other features. I think all <custom-ident> values across all features should be hashed consistently.

This has been implemented in Lightning CSS (and perhaps some other implementations) for a while, but I think it would be good if we could update the spec to define it in terms of the <custom-ident> type so that future additions to CSS automatically get hashed.

<dashed-ident>

The CSS spec also defines the <dashed-ident> type for cases where either a spec-defined or author defined identifier may be present. The leading -- allows these to be distinguished. This is most commonly used for CSS variables, but it is also used for other features like the @font-palette-values, @color-profile, and @custom-media rules, as well as other upcoming specs.

Lightning CSS currently implements support for local (hashed) CSS variables and dashed idents under an opt-in flag. I think this feature should also exist in the CSS modules spec so that it can be interoperable between tools. It seems to be in-line with the general CSS modules philosophy to scope all identifiers local to the file by default.

I believe there was a plan for webpack to also implement this at some point (according to @sokra), though I'm not sure what the current status of that is.

Accessing globals and dependencies

One problem with hashing custom and dashed idents is that there isn't a syntax to define or reference a global ident, or reference a <custom-ident> or <dashed-ident> in a different file. With selectors, the :global pseudo class can be used to define a global class or id, and the composes property supports referencing classes defined in another file or globally with the composes: name from "..." syntax. But with general custom or dashed idents this is not currently possible.

The spec currently mentions using :global(xxx) to define a global @keyframes rule. I think this syntax is kinda weird because it is not a selector, so the pseudo-class like syntax with the colon is out of place. It would also require changing the parsing of that rule from the standard CSS syntax. There is also no way to reference a hashed animation-name from a different file at the moment.

The CSS variables implementation in Lightning CSS currently allows referencing a variable defined in a different file using a syntax similar to composesvar(--foo from "./bar.css") or var(--foo from global). However, there is currently no way to define a global custom property within a CSS module file, or update the value of a custom property defined in a different file. The --foo from global: "value"; syntax seems pretty strange for that, and also violates the CSS spec which requires property names to be valid identifiers.

Proposal

Defining a global identifier

I propose adding an @global at-rule, which would make all selectors and identifiers defined within it global (non-hashed). This would enable defining global identifiers using other nested at-rules without changing their syntax.

@global {
  @counter-style circles {
    symbols: Ⓐ Ⓑ Ⓒ;
  }

  @keyframes fade {
    /* ... */
  }

  @custom-media --modern (color), (hover);
}

This could also be used to define global custom properties or selectors. Everything inside the @global rule would become global.

@global {
  .foo {
    --global-var: red;
  }
}

In this case, both .foo and --global-var would not be hashed. With CSS nesting, you could also define only the --global-var as global and keep .foo hashed.

.foo {
  @global {
    --global-var: red;
  }
}

I think this should only affect declarations and not references. Referencing another value within an @global rule should still reference a local (hashed) name.

@global {
  .foo {
    animation-name: fade;
  }
}

In this case, fade would be hashed, but .foo would not. To reference a global name or an identifier from a different file see below.

Referencing an identifier

To reference a <custom-ident> or <dashed-ident> from a different file, or from the global namespace, I propose adding an import() function. This would accept syntax similar to the composes property to define where to import from.

ul {
  list-style: import(circles from "./some-file.css");
  animation-name: import(fade from global);
  color: var(import(--accent-color from "./vars.css"));
}

The reason this is a function rather than simply inline (e.g. animation-name: fade from global) is to disambiguate cases where from and global could be valid identifiers as well, for example if a property accepted a space separated list.

We could also consider keeping var(--accent-color from "./vars.css") as a shortcut, but import() is a more general solution that works in more places.

Re-defining custom properties from a different file

Since the CSS spec requires that all declarations have property names that are identifiers in order to parse, we cannot use import() in a property name. Defining a global custom property can be achieved as described above, but re-defining or updating the value of a custom property from a different file is not possible.

To solve this, we could introduce an @with rule to "import" an identifier name from another file so it can be referenced as if it were local.

@with --accent-color from "./vars.css" {
  .foo {
    --accent-color: purple;
  }
}

This would cause the reference to --accent-color to be hashed as a dependency of "./vars.css".

This could also be used as an alternative to an inline import() call as described above. Here is the same example rewritten to use @with:

@with 
  circles from "./some-file.css",
  fade from global,
  --accent-color from "./vars.css"
{
  ul {
    list-style: circles;
    animation-name: fade;
    color: var(--accent-color);
  }
}

This is sort of similar to @value but with a couple very important differences:

  1. It only allows importing identifier names, not arbitrary values. This makes it much easier to parse, because it doesn't affect every single property value, only places where <custom-ident> or <dashed-ident> are accepted.
  2. It is scoped to a block. This also makes it much easier to replace during parsing, and easier to control where the definitions will apply rather than the whole file.

Grammar

This is the formal grammar proposed above, using the value definition syntax from the CSS specification.

@global {
  <style-block>
}

<css-module-reference> = <custom-or-dashed-ident> from <css-module-namespace>
<custom-or-dashed-ident> = <custom-ident> | <dashed-ident>
<css-module-namespace> = global | <string>

<import> = import(<css-module-reference>)

<css-module-reference-list> = <custom-or-dashed-ident># from <css-module-namespace>

@with <css-module-reference-list># {
  <rule-list>
}

Within a CSS module file, <custom-ident> and <dashed-ident> would also change their grammar to support import().

<css-module-custom-ident> = <custom-ident> | <import>
<css-module-dashed-ident> = <dashed-ident> | <import>

Conclusion

I think these additions to CSS modules could help it support modern CSS features like variables, animations, custom media queries, and more. I'm hoping we can get some consensus on the syntax here so that it remains interoperable between different tools. I'd love to hear your feedback!

@alexander-akait
Copy link
Member

Yes, I completely agree with everything

This has been implemented in Lightning CSS (and perhaps some other implementations) for a while, but I think it would be good if we could update the spec to define it in terms of the type so that future additions to CSS automatically get hashed.

Yeah, anyway let's list them in spec.

Lightning CSS currently implements support for local (hashed) CSS variables and dashed idents under an opt-in flag. I think this feature should also exist in the CSS modules spec so that it can be interoperable between tools. It seems to be in-line with the general CSS modules philosophy to scope all identifiers local to the file by default.

I believe there was a plan for webpack to also implement this at some point (according to @sokra), though I'm not sure what the current status of that is.

:root {
  --EgL3uq_accent-color: hotpink;
}

.EgL3uq_button {
  background: var(--EgL3uq_accent-color);
}

Already supported in webpack built-in CSS support (under experemental flag now)

.button {
  background: var(--accent-color from "./vars.module.css");
}

This is not supported and use we should not use var, because var(...) is from official spec and we may have problems in the future, I think you already understand this, using import is good idea, I like it.

I propose adding an @global at-rule, which would make all selectors and identifiers defined within it global (non-hashed). This would enable defining global identifiers using other nested at-rules without changing their syntax.

I like it, too, will we still support :global for keyframes for compatibility, right?

Since the CSS spec requires that all declarations have property names that are identifiers in order to parse, we cannot use import() in a property name. Defining a global custom property can be achieved as described above, but re-defining or updating the value of a custom property from a different file is not possible.

Actually we can, new spec allows it (but spec it still not fully updated in may places about it) https://w3c.github.io/csswg-drafts/css-syntax/#consume-style-block and https://w3c.github.io/csswg-drafts/css-syntax/#consume-declaration

I find this syntax a bit cumbersome.

@with --accent-color from "./vars.css" {
 .foo {
   --accent-color: purple;
 }
}

@devongovett
Copy link
Author

This is not supported and use we should not use var

I saw that syntax listed in webpack/webpack#14893 as planned, but yeah import() might be a better general solution if only a bit more verbose.

I like it, too, will we still support :global for keyframes for compatibility, right?

sure.

Actually we can, new spec allows it (but spec it still not fully updated in may places about it) https://w3c.github.io/csswg-drafts/css-syntax/#consume-style-block and https://w3c.github.io/csswg-drafts/css-syntax/#consume-declaration

hmm from my reading of "consume a declaration" step 1 in the second link you shared, it still sounds like only <ident-token> can be before the colon and anything else is a parse error. So something like import(--color from "./vars.css"): red; wouldn't be supported. Did I misunderstand or miss something?

@alexander-akait
Copy link
Member

hmm from my reading of "consume a declaration" step 1 in the second link you shared, it still sounds like only can be before the colon and anything else is a parse error. So something like import(--color from "./vars.css"): red; wouldn't be supported. Did I misunderstand or miss something?

I have seen the discussion before for such changed and as far as I understood

.class {
  fn(): something;
}

was planned in support, but the spec is changing quite quickly so how could you see.

Let's ask @tabatkins, If it turns out that this is not possible and is not planned anymore, then we will return to the discussion of your syntax

@tabatkins
Copy link

"Planned in support" is overstating; in the "Option 3" version of Nesting (which the spec currently describes) we wanted to make sure that fn(): something; was parsed as an (invalid) declaration rather than a rule, so that the rules for what triggers declaration-parsing vs rule-parsing were easy to recognize on sight (and authors didn't have to realize that ident and function tokens are actually distinct things in the CSS syntax). We don't have any plans to use such a syntax, but just in case.

And we're planning to simplify that anyway, since it turns out to be easier than we thought - the weird carveout in the parsing algorithm will get removed in that case, and it'll just parse as a declaration if it can, and as a rule otherwise.

It'll still fail to parse, either way, since we don't have functions as property names or bare functions in selectors currently.

@alexander-akait
Copy link
Member

@tabatkins Thanky you for feedback 👍

@devongovett Apparently the only safe solution is using @with at-rule, my only fear is if in the future we have "@with" in the spec.

@devongovett
Copy link
Author

Yeah... that's a danger with adding really any syntax extension to css. I suppose we could namespace them, like @css-modules-with but it gets pretty verbose. Could use a dashed name like @--with but that looks kinda weird. Also :global, :local and composes are already not namespaced so might be weird to change it.

Also @with was just the first name I thought of. Maybe there's a better one.

@alexander-akait
Copy link
Member

I suppose we could namespace them, like @css-modules-with but it gets pretty verbose.

Yeah, I agree, on the one hand it would be the right idea, on the other hand it is too long

Also @with was just the first name I thought of. Maybe there's a better one.

Yeah, I guess we should take this and hope nothing bad happens 😄

@jantimon
Copy link

One idea for Accessing globals and dependencies

What do you think about combining some ideas from esm, @color-profile and @layer?

https://developer.mozilla.org/en-US/docs/Web/CSS/@color-profile

@color-profile --swop5c {
  src: url("https://example.org/SWOP2006_Coated5v2.icc");
}
.header {
  background-color: color(--swop5c 0% 70% 20% 0%);
}

It could be a file wide import similar to esm e.g.:

@import --baz {
   src: url("./colors.css");
}

body {
  background: --baz;
}

in addition to src we might also add type or fallback attributes

@import --baz {
   src: url("./colors.css");
   type: fooBar;
}

combining such an approach with multiple value lists similar to the @layer syntax

https://developer.mozilla.org/en-US/docs/Web/CSS/@layer

@layer theme, layout, utilities;

could allow importing multiple values from the same file:

@import --baz, --bar, --foo {
   src: url("./colors.css");
}

body {
  background: --baz;
}

I am not sure if overloading @import might be a problem - in that case @require might be also an intuitive keyword for web devs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants