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

Add external StyleSheets support #148

Merged
merged 27 commits into from
May 13, 2017
Merged

Add external StyleSheets support #148

merged 27 commits into from
May 13, 2017

Conversation

giuseppeg
Copy link
Collaborator

@giuseppeg giuseppeg commented Mar 12, 2017

Fixes #83

This branch introduces external stylesheets support.

External stylesheets are js modules that export a string or template literal:

const color = 'red'

export default `
  p { color: red } 
`

Which can be used as follow:

import styles from './styles'

export default () => (
  <p>
     Zeit rocks
     <style jsx>{styles}</style>
  </p>
)

Please read the

Implementation details Say that you have `components/Card/style.js`:
export default `
 div { color: red }
`

File names are unique so we can generate the hash for external files using the filename hash(path.resolve('components/Card/style.js')) and transpile the file like this:

export default `
 div[data-jsx=ext~="woot"] { color: red }
`

the ~ matches a word therefore you can have data-jsx-ext~="woot wat" where woot and wat are external stylesheets hashes.

<div data-jsx="wut" data-jsx-ext="woot wat">
 ...
 <_JSXStyle id="wat" css="div[data-jsx=\"wat\"] {font-size: 2em}" />
 <_JSXStyle id=”woot" css={referenceToTheImportedFile} />
</div>

For the sake of supporting both global and local styles the transpiled stylesheet exports an object with two keys global and local respectively for the original version (global) and scoped one. This allows people to use the same stylesheet in both modes (maybe it is overkill and we can remove it if you want).

TODOs

  • Transpile external stylesheets that pass the CSSTree validation
  • Add support for source maps
  • Add support for ES5/module.exports
  • Test edge cases and maybe in a real app
  • Add support for named exports
  • Use external files' content for hashing and export the hashes instead of hashing the filename
  • Add DOCs to README.md

@giuseppeg giuseppeg requested review from rauchg and nkzawa March 12, 2017 22:26
@giuseppeg
Copy link
Collaborator Author

You guys may want to review per commit since the branch is quite big :)
Also do you know how to suppress the linter error (it is the only reason why tests fail)?

@fatfisz
Copy link
Contributor

fatfisz commented Mar 13, 2017

@giuseppeg For that linter error you can just use elision:

const [
  id,
  ,
  externalStylesReference,
  isGlobal
] = state.externalStyles.shift()

@giuseppeg
Copy link
Collaborator Author

giuseppeg commented Mar 14, 2017

Test are passing locally and failing in CI because path.resolve('./foo.js') resolves to different absolute paths in CI, locally and therefore hash(path.resolve('./foo.js')) doesn't match. Can you guys think of a workaround? Should I mock hash?

@rauchg
Copy link
Member

rauchg commented Mar 18, 2017

wow. Amazing progress

@giuseppeg
Copy link
Collaborator Author

giuseppeg commented Mar 19, 2017

@rauchg since babel processes files alphabetically (I believe) external stylesheets are not transpiled in some cases. An example would be button.js imports some styles defined in a.js.

To work around this issue we can take one of the following approaches:

  1. We ask the user to annotate the file e.g. by adding a comment at the top of the file /* @styled-jsx */
  2. We only support external stylesheets whose extension is jsx this way we can treat the file as a stylesheet when it exports a string.
  3. We ask the users to use a sub extension like .styled.js
  4. We try to detect CSS modules by doing some checks on the exported string – in this case I'd write a module is-css-string maybe. This is the best solution in terms of DX but it can be error prone.

Edit

I decided that I will parse the string with CSSTree, if parsing / validation fails I'll skip the file – in the future we can add warnings or throw an error. CSSTree seems to be super fast.

@giuseppeg giuseppeg force-pushed the external-css-support branch 2 times, most recently from 9962373 to 5aa9592 Compare March 27, 2017 16:20

let globalCss = css.modified || css

if (validate && !isValidCss(globalCss)) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to avoid this we could require people to tag external stylesheets like so:

export default styledJsx`
  div { color: red }
`

@rauchg
Copy link
Member

rauchg commented Mar 29, 2017

since babel processes files alphabetically (I believe) external stylesheets are not transpiled in some cases

I wonder if there's a way to invoke babel from within the plugin to work around this limitation? As long as caching is enabled, it would incur in overhead for the user (double parsing)

@giuseppeg
Copy link
Collaborator Author

@rauchg I am not sure if 1) plugins get the dest path 2) if we write the file to disk after transforming it with babel-core (like we do for tests) that would conflict or be overridden by the original one (when transpiled)

@giuseppeg
Copy link
Collaborator Author

giuseppeg commented Mar 30, 2017

@rauchg @nkzawa this branch is ready for review, I still need to write some docs and I'll test this branch with next-news.

Here is a breakdown of what I did:

usage

External stylesheets are ES6 or CommonJS modules that export a string or template literal.

/* styles.js */
export default `div { background-color: red }`
/* otherStyles.js */
module.exports = "p { color: white }"

Expressions work.

Within components these can be referenced like so:

import styles from './styles'
const otherStyles = require('./otherStyles')

export default () => (
  <div>
     <p>hello</p>
     <style jsx>{styles}</style>
     <style jsx>{otherStyles}</style>
  </div>
)

Styles can also be consumed as global by adding the attribute to the style element as usual.

how this works

Instead of hashing the CSS string, I calculate the hash for each external stylesheet by using its absolute path (filename) which is always unique.

hash(filaname)

Since one may want to use multiple stylesheets I introduced a new data attribute data-jsx-ext and I am using the following attribute selector in css:

[data-jsx-ext~="a"] { /* ... */ }
[data-jsx-ext~="b"] { /* ... */ }

Notice the ~=. Such a selector matches any element whose data-jsx-ext attribute value is a list of space-separated values, one of which is exactly equal to a or b.

Meaning that in the markup we will have:

<div data-jsx-ext="a b c d">

That said the external stylesheets are rewritten to something like this

export default {
  global: `div { background-color: red }`,
  local: `div[data-jsx-ext~="597123001"] { background-color: red }`
}

And the style tags rewritten like so:

<_JSXStyle styleId={597123001} css={styles.local} />
<_JSXStyle styleId={597123003} css={styles.global} />

implementation details

You may want to review things by commit – maybe it'd make it easier to follow what is going on.

  • I moved all of the utils and constants to separate files
  • Added logic to detect external stylesheets references and transpilation – here is where I resolve the import source (filename), hash it and add it to the elements
  • Transform style tags and transpile external stylesheets (and add the { global, local } thing).

Note at this point I was keeping track of the external stylesheets references and transpile those files as I encountered them. However this has proven to be wrong because: depending on the order in which babel processes files some stylesheets might never be compiled. This is due to the fact that styles.js occurs (and is processed) before ZooComponent.js for example.

  • I solved this issue by validating exported strings and template literals with CSSTree: if a module exports a string or a template literal then I pass that string through CSSTree and if parsing doesn't fail then it must be CSS. CSSTree is very fast.

An alternative solution would be to tag the file with a comment /* @styled-jsx */ or wrap the exported strings/literal in a function (identity) export default styledJsx(`div { color: red }`).

  • Finally since hash(filename) is unique on each machine I had to strip hashes out from tests. We could find a more cool way to mock/stub the hash but I guess that his is fine.

Alternatively probably we can hash the whole file content instead of the filename and remove this issue.

If you need any more info you know where to find me :)

@giuseppeg
Copy link
Collaborator Author

Alternatively probably we can hash the whole file content instead of the filename and remove this issue.

This would add some overhead since we'd need to do i/o (read the file from disk)

@thomaslindstrom
Copy link

thomaslindstrom commented Apr 3, 2017

Just a question: Would you, with this, be able to use, eg. webpack loaders, to import the contents of a CSS file that you inject into the style tag? I mean, stylesheet in this example would be a string, so it'd be pretty much identical to exporting a string from a module?

import stylesheet from './styles.css'

export default () => (
  <div>
     <p>hello</p>
     <style jsx>{stylesheet}</style>
  </div>
)
/* styles.css */
p {
  color: red;
}

@giuseppeg
Copy link
Collaborator Author

@thomaslindstrom I am not exactly sure but in theory yes.
It'd be amazing if you could clone this branch and try locally.

@nkzawa
Copy link
Contributor

nkzawa commented Apr 7, 2017

sorry for late replay. I didn't read details of implementations yet, but ideas seem brilliant 👍

@giuseppeg
Copy link
Collaborator Author

@nkzawa no worries, I just pinged you because I would like to merge this (if we think that it is a good solution) to avoid crazy merge conflicts (I touched quite a lot)

@rauchg
Copy link
Member

rauchg commented Apr 17, 2017

@giuseppeg

  • do named exports work?
  • do multiple imported expressions work inside the same <style jsx> tag?
import { a, b } from './external-styles'
export default () => (
  <div>
    <style jsx>{ a } { b }</style>
  </div>
)

@giuseppeg
Copy link
Collaborator Author

giuseppeg commented Apr 17, 2017

@rauchg

do named exports work?

No they don't, only default exports and module.exports right now.
This is to avoid parsing every export and validating every string or template literal with csstree. If you think that it is a useful feature I can add support for it.

do multiple imported expressions work inside the same <style jsx> tag?

Not right now because of this check, we could add support for that too though.
This would also enable mixed local and external styles support.

import { a } from './external-styles'
export default () => (
  <div>
    <style jsx>
       {`div { color: red }`}
       { a }
    </style>
  </div>
)

NB. this adds complexity and could be achieved with two <style jsx> tags therefore it might not be worth implementing

@tz5514
Copy link

tz5514 commented Apr 30, 2017

Looks great! Hope that release soon.

@LarryLuTW
Copy link

I'm looking foward to see this PR be merged.

@lededje
Copy link

lededje commented May 2, 2017

What are we waiting on to get this merged? Happy to write documentation if that's all that is missing.

@giuseppeg
Copy link
Collaborator Author

Hold in the excitement folks! :D We'll merge (soon) once this branch has been reviewed and is good to go :)

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 this pull request may close these issues.

Allow for importing styles
8 participants