Skip to content

Extract module functionality from loading #60

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

Closed
geelen opened this issue May 26, 2015 · 20 comments · Fixed by #78
Closed

Extract module functionality from loading #60

geelen opened this issue May 26, 2015 · 20 comments · Fixed by #78

Comments

@geelen
Copy link

geelen commented May 26, 2015

Been chatting with @markdalgleish about this, but I thought I'd put my thoughts down into an issue that was a bit more async-friendly (since he has a day job and so chatrooms aren't ideal).

I love the module mode for css-loader, I love the local-by-default idea, as well as being able to import other files & extend classes. But I think those things are being all tied together in the current implementation, and could be broken out, which would mean more flexibility, reuse, and ease-of-contributions.

Here's my suggestion:

  • Define a series of PostCSS transform that take the currently-supported syntax and converts it. Taking the example off the home page.

Input:

:local(.continueButton) {
  extends: button from "library/button.css";
  background: red;
}

After parse-imports.js

:import("library/button.css") {
  button: __tmp_487387465fczSDGHSABb;
}
:local(.continueButton) {
  extends: __tmp_487387465fczSDGHSABb;
  background: red;
}

After localize-and-extend.js

:import("library/button.css") {
  button: __tmp_487387465fczSDGHSABb;
}
:export {
  continueButton: _13LGdX8RMStbBE9w-t0gZ1 __tmp_487387465fczSDGHSABb;
}
._13LGdX8RMStbBE9w-t0gZ1 {
  background: red;
}

These transformations each have a single responsibility, are easier to test & maintain, can operate on a single file at once and totally agnostic to Webpack/JSPM/Browserify. After this:

  • Automatically attach those two PostCSS transforms in the same way importLoaders can be used.
  • If css-loader?module is set and CSS Module mode is activated, add the plugin postcss-local-scope to translate .localName to :local(.localName), just as it was before.
  • The current localisation/extension logic is adapted to look for the :import and :export primitives, and look for usages of temporary imported symbols (e.g. __tmp_487387465fczSDGHSABb) and replace them with the exports of another file.

So, this achieves a couple of things. It reduces the complexity of this project in particular, yet still presents the same interface to users of the module (:local and extends are always available, local-by-default is there in "module mode"). I can implement :import and :export for JSPM, and then we can share the CSS Modules syntax & plugins. I think it increases the chances of making CSS Modules a specification, since we'll have multiple loaders using it (more users) & be able to solve spec-level issues at the same time for everyone.

I'm happy to build these PostCSS plugins, and I think I could write a PR to include them as importLoaders. Getting :import and :export to work might be a bit beyond me right now.

From the reaction to both this project & Mark's blog post last week, as well as the discussions at CampJS and in chat rooms, people are really excited about this kind of functionality. I think this would make a good next logical step for the life of CSS Modules, and give it the best chance of being adopted globally.

Thanks!

@joshgillies
Copy link

Nice, and exactly what I'd like to see. I'm currently (after discussion with @markdalgleish over the weekend) aiming to develop a Browserify transform to accommodate CSS Modules. Having postcss-local-scope exist independent of css-loader would be of great benefit here. Definitely keen to see this happen, and happy to offer a hand where possible. 👍

@sokra
Copy link
Member

sokra commented May 26, 2015

I already discussed it with @geelen, we are doing this.

@knownasilya
Copy link

Yes please on the browserify transform, and splitting out the scope part. Share the love 😻

@necolas
Copy link

necolas commented Jun 3, 2015

please could you bake in the option to disable imports, exports, and extends? i consider those anti-patterns (terrible for code complexity) and would like to prevent those from slipping into our code-base.

@ai
Copy link
Contributor

ai commented Jun 3, 2015

I agree with @necolas. Loader should not became a many-in-one combine. We has many tools to do it.

@MoOx
Copy link

MoOx commented Jun 3, 2015

+1 for an option to disable imports, exports, and extends

@markdalgleish
Copy link
Contributor

@ai I disagree, CSS Modules is offering something unique here.

AFAIK, CSS Modules are the only way to turn nested extends into multiple classes on an element. Historically, this has only served to bloat up your CSS.

@markdalgleish
Copy link
Contributor

@necolas (honest question...) do you have a unique class for everything, with styles duplicated between classes?

@MoOx
Copy link

MoOx commented Jun 3, 2015

@markdalgleish For my concern no. Shared styles are just shared via a component. So no shared styles using extends.

@markdalgleish
Copy link
Contributor

We've tried to push towards components for everything, but we still share low-level, building-block styles across classes for things like heading text style, since that sort of thing changes between breakpoints and we want our site to work without JavaScript.

@MoOx
Copy link

MoOx commented Jun 3, 2015

An options to disable import/extends should not be a big deal don't you think ?

@sokra
Copy link
Member

sokra commented Jun 3, 2015

Loader should not became a many-in-one combine.

Shared styles are just shared via a component.

CSS Modules are the only way to turn nested extends into multiple classes on an element.

An options to disable import/extends should not be a big deal don't you think ?

I agree with all of this. I think there is a missunderstanding what CSS Modules are.

Here is what they are NOT:

  • They are not a one-to-many transform. extends doesn't change your CSS. One input CSS still compiles to one chunk of output CSS. Most importantly CSS Modules doesn't copy CSS rules from other modules. In fact while compiling one CSS Module referenced modules are not even read.
  • You are not forced to use them. If you don't use import/export/extends, it doesn't have any effect on your CSS. (Of course we can also offer an option to disable it, if you really want it.)

Historically you may have written your components this way: (I assume you are already using local-scoped CSS, as this was not a discussion point)

/* buttons.css */
.buttonBase {
  border: 1px solid black;
  background: #999;
}
.buttonBase:hover {
  border: 2px solid black;
  background: #eee;
}
.bigButton {
  font-size: 200%;
}
/* MyComponent.css */
.signInButton {
  font-weight: 200%;
}
// MyComponent.js
import buttonStyle from "buttons.css";
import style from "MyComponent.css";

// (HTML layout) including this line
<button className={buttonStyle.buttonBase + " " + style.signInButton}>Sign in</button>

I interpreted @MoOx comment as he is doing it that way:

Shared styles are just shared via a component. So no shared styles using extends.

If you have two separate roles: developer and designer you could shared the responsibilities in this way:

  • designer: MyComponent.css, buttons.css
  • developer: MyComponent.js

Ok wait. What if the designer wants to use the .bigButton style from buttons.css for the Sign in button. That's propably his responsibility. But the fragment he need to change is in MyComponent.js:

- className={buttonStyle.buttonBase + " " + style.signInButton}
+ className={buttonStyle.buttonBase + " " + buttonStyle.bigButton + " " + style.signInButton}

Here CSS Modules come into play:

All extends does is to move the className part from MyComponent.js into the MyComponent.css. It does it with a syntax that looks similar to compile-to-css languages, but technically it's fundamental different.

Here the same component using extends:

/* buttons.css */
.buttonBase {
  border: 1px solid black;
  background: #999;
}
.buttonBase:hover {
  border: 2px solid black;
  background: #eee;
}
.bigButton {
  extends: buttonBase;
  font-size: 200%;
}
/* MyComponent.css */
.signInButton {
  extends: buttonBase from "button.css";
  font-weight: 200%;
}
// MyComponent.js
import style from "MyComponent.css";

// (HTML layout) including this line
<button className={style.signInButton}>Sign in</button>

(Four lines where changed: .bigButton { extends: buttonBase;, .signInButton { extends: buttonBase ..., the className line and I removed the import buttonStyles... line)

Now everything that concerns the styling is in the CSS file (and in CSS syntax) and layout is in the JS file.

Same question: What if the designer wants to use the .bigButton style from buttons.css for the Sign in button.

  .signInButton {
-   extends: buttonBase from "button.css";
+   extends: bigButton from "button.css";

Technically the resulting CSS and HTML is exactly the same. CSS styles are still shared by including mulitple class names in the HTML class attribute. Just the place where you insert the used shared class names has changed (In my optinion in a better way).

I could add a list of benefits, but I don't have time for this, so it's left as an (easy) challenge to the reader.


Also take a look at @gaearon's https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

You propably can apply the same pattern to CSS Modules.

MyComponent.css is a smart style. buttons.css is a dumb style.

@ai
Copy link
Contributor

ai commented Jun 3, 2015

@markdalgleish yeap, I mean only about extend and import. But even CSS Modules already exists in separated project ;).

@necolas
Copy link

necolas commented Jun 3, 2015

This is what I am thinking at the moment. Let me know if you have any thoughts about it.

What matters to me more than the output from extends is the complexity it creates. By nature it couples parts of the application, whether its a good idea or not. There's a tendency for us to over-optimize and look for patterns between superficially similar things. Extending UI creates a visual coupling; in my experience, the majority of the time this slows iteration speed and causes bugs. It's much simpler to compose than to extend. I think examples like extending a "base button" are minority cases, and you can often use component composition to achieve the same result (that's been our strategy).

I worked on TweetDeck a few years ago, and at the time it used extends a lot, making the app extremely difficult to work on – and there were only 4 of us working on it at the time. You can easily end up with large, multi-directional inheritance chains and the base classes ossify, since any change to them ripples throughout the app. We chose to rewrite the code to remove the entanglement.

(honest question...) do you have a unique class for everything, with styles duplicated between classes?

Yes. I assume you're getting at file size concerns. Using components has done a lot to limit the size of our CSS (and JS); I don't think a bit of duplication is a problem in CSS anymore than in JS. Most components don't look the same. And we've encapsulated common sources of unwanted repetition across components into specific, low-level components – Text, View, Image, Grid. I think that further optimizations to reduce any duplication of repeated margin or color declarations (for example) in the production CSS could be better done by tooling, rather than by developers in the source code. If it is handled by tools, then you could produce adhoc coupling in the compiled output, without the need to couple things in the source code: https://gist.github.com/necolas/54b3baebd0e772e39f0e. It's harder than leaving it to authors – and i'm not sure how possible – but in the meantime, I don't want to re-complicate our CSS with extends to shave a few KB off a bundle.

Since any feature that is available will find its way into a team's codebase, I'd rather make it impossible for extends to creep without intent.

@geelen
Copy link
Author

geelen commented Jun 4, 2015

I think @necolas is right on the money here. We need to be very careful about separating the opinionated stuff (CSS Modules) away from the generic stuff (the loaders). I'm doing the same with JSPM — a CSS Modules and a raw CSS loader that both share the same core but have very different user-facing functionality.

I'm going to put together a low-level spec that I think all the loaders should support unconditionally, since it goes right at the heart of what it means to dynamically load CSS. But it won't include any user-facing syntax, it will just be the glue that allows us CSS Modules people (and anyone else experimenting with CSS syntax) the ability to inform how the loader runs.

@geelen
Copy link
Author

geelen commented Jun 4, 2015

I've just pushed my first crack at a specification of the loader format: https://github.com/css-modules/css-modules/blob/loader-format-documentation/INTERCHANGE_FORMAT.md (discussion in the PR linked above).

@necolas
Copy link

necolas commented Jun 4, 2015

I guess I am still wondering why the approach is to move JS features into CSS rather than move CSS authoring into JS. The local-by-default hack is really useful for pulling in a CSS file (especially existing external packages) and de-globalizing it. That's a cool idea; already started using it. But every time I think about style as a layer in the component – and the sharing of values between the style and other calculations – I end up back at using JS, with CSS being a compile target.

With a postcss approach, it's possible to define CSS variables in JS, but then you have to feed those variables into the CSS in the webpack config (to the postcss-custom-properties or equivalent plugin). It's not modular either since everything is shared by default, and you can't define local variables in a way that can be shared between a component's JS and CSS. And there's no provision for that kind of JS-to-CSS strategy in the W3C specs – it's only possible because of the tools we've written.

Doing everything is JS seems to solve the import/export/merge/variables problems more simply, and doesn't prevent the kind of compiled CSS output you get from the current css-loader. Something like https://github.com/js-next/react-style is great but you have to buy into a larger mind-shift (unsupported CSS features), it's primarily aimed at inline styles, doesn't hash the names like css-loader, and I don't think the webpack plugin works with code-splitting. So it's not simply a way to author local CSS in JS, which seems to be what we want and an easier first step.

Is there something I'm missing? Would it need a convention like authoring the JS-styles in a separate file and with a specific extension to match against?

@geelen
Copy link
Author

geelen commented Jun 5, 2015

Yeah, I think we're seeing somewhat of a division in gut-feel — I think the future lies in authoring something very CSS-like and consuming it dynamically in JS, you and others appear to go for authoring some or all styles in JS and using CSS as the output. I honestly have no idea which is better, but my gut feel is that authoring in CSS preserves something that authoring in JS lacks, and CSS Modules as a project is about meeting somewhere in the middle.

But this issue isn't that one. This issue is about the capability of CSS to be interpreted by the loader and have dynamically-linked symbols, not the correctness in doing so. Without the former, we can't explore the latter.


P.S. there's nothing stopping us from allowing JS -> CSS symbol injection. Consider the following:

module.exports = { brown: "#700"; }
:import("./colors.js") {
  __brown: brown;
}
.MyComponent {
  color: __brown;
}

Obviously, that's the low-level format, so that would be the compile target of some higher-level syntax, probably, maybe something like color: ref(brown from "./colors.js");. But who knows, that's the whole point of this change — let's give CSS a tiny amount of dynamism and see what it lets us do!

@necolas
Copy link

necolas commented Jun 5, 2015

OK. I think there is value in the css-loader and postcss approach in order to hash classes in legacy CSS code and apply them in JS. I'm going to start using that. But the last import example looks like JS to me.

sokra added a commit that referenced this issue Jun 16, 2015
Use new CSS Modules postcss stuff

fixes #54
fixes #60
@sokra sokra mentioned this issue Jun 16, 2015
sokra added a commit that referenced this issue Jun 18, 2015
Use new CSS Modules postcss stuff

fixes #54
fixes #60
@sokra sokra closed this as completed in #78 Jun 18, 2015
@geelen
Copy link
Author

geelen commented Jun 20, 2015

💖 🎉 P A R T Y T I M E S 🎉 💖

koistya pushed a commit to koistya/css-loader that referenced this issue Nov 19, 2015
…l-on-non-latin1-chars

Wrap sourceMap JSON in unescape and encodeURIComponent.
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

Successfully merging a pull request may close this issue.

8 participants