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 support for plugins #190

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,18 @@ export default () => (
)
```

### Options

* `sourceMaps` generates source maps (default: `false`)
* `vendorPrefix` turn on/off automatic vendor prefixing (default: `true`)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

should this be a noun/plural? vendorPrefixes


## Features

- Full CSS support, no tradeoffs in power
- Runtime size of just **2kb** (gzipped, from 6kb)
- Complete isolation: Selectors, animations, keyframes
- Built-in CSS vendor prefixing
- CSS Preprocessing via Plugins
- Very fast, minimal and efficient transpilation (see below)
- High-performance runtime-CSS-injection when not server-rendering
- Future-proof: Equivalent to server-renderable "Shadow CSS"
Expand Down Expand Up @@ -113,6 +119,82 @@ module.exports = `div { color: green; }`
// module.exports = { styles: `div { color: green; }` }
```

### CSS Preprocessing via Plugins

Styles can be preprocessed via plugins.

Plugins are regular JavaScript modules that export a simple function with the following signature:

```js
(css: string, settings: Object) => string
```

Basically they accept a CSS string in input, optionally modify it and finally return it.

Plugins make it possible to use popular preprocessors like SASS, Less, Stylus, PostCSS or apply custom transformations to the styles.

To register a plugin add an option `plugins` for `styled-jsx/babel` to your `.babelrc`. `plugins` must be an array of module names or *full* paths for local plugins.

```json
{
"plugins": [
[
"styled-jsx/babel",
{ "plugins": ["my-styled-jsx-plugin-package", "/full/path/to/local/plugin"] }
]
]
}
```

In order to resolve local plugins paths you can use NodeJS' [require.resolve](https://nodejs.org/api/globals.html#globals_require_resolve).
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It would be nice if we could find a way to magically resolve paths internally.
If it is not possible then asking them to use require.resolve or anyway provide full paths is ok I guess.


Plugins are applied in definition order left to right before styles are scoped.

N.B. when applying the plugins styled-jsx replaces template literals expressions with placeholders e.g. `%%styledjsxexpression_ExprNumber%%` because otherwise CSS parsers would get invalid CSS.

#### Plugin options and settings

Users can set plugin options by registering a plugin as an array that contains
the plugin path and an options object.

```json
{
"plugins": [
[
"styled-jsx/babel",
{
"plugins": [
["my-styled-jsx-plugin-package", { "exampleOption": true }]
],
"sourceMaps": true
}
]
]
}
```

Each plugin receives a `settings` object as second argument which contains
the babel and user options:

```js
(css, settings) => { /* ... */ }
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it support async operations ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no it doesn't because of the sync nature of babel

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fwiw in styled-jsx-postcss I use a super hack to make async code work (syncronously) https://github.com/giuseppeg/styled-jsx-postcss/blob/master/src/babel.js#L98-L107

```

The `settings` object has the following shape:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this object could as well be renamed to options and change shape:

{
  // user options
  exampleOption: true,

  // babel options
  babel: { // or env
    sourceMaps: true,
    vendorPrefix: true,
  }
}


```js
{
// babel options
sourceMaps: true,
vendorPrefix: true,

// user options
options: {
exampleOption: true
}
}
```

### Targeting The Root

Notice that the parent `<div>` above also gets a `data-jsx` attribute. We do this so that
Expand Down
49 changes: 49 additions & 0 deletions src/_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,52 @@ export const addSourceMaps = (code, generator, filename) =>
convert.fromObject(generator).toComment({ multiline: true }),
`/*@ sourceURL=${filename} */`
].join('\n')

export const combinePlugins = (plugins, opts) => {
if (!plugins) {
return css => css
}

if (
!Array.isArray(plugins) ||
plugins.some(p => !Array.isArray(p) && typeof p !== 'string')
) {
throw new Error(
'`plugins` must be an array of plugins names (string) or an array `[plugin-name, {options}]`'
)
}

return plugins
.map((plugin, i) => {
let options = {}
if (Array.isArray(plugin)) {
options = plugin[1] || {}
plugin = plugin[0]
}

// eslint-disable-next-line import/no-dynamic-require
let p = require(plugin)
if (p.default) {
p = p.default
}

const type = typeof p
if (type !== 'function') {
throw new Error(
`Expected plugin ${plugins[i]} to be a function but instead got ${type}`
)
}
return {
plugin: p,
settings: {
...opts,
options
}
}
})
.reduce(
(previous, { plugin, settings }) => css =>
plugin(previous ? previous(css) : css, settings),
null
)
}
33 changes: 25 additions & 8 deletions src/babel-external.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import {
makeStyledJsxCss,
isValidCss,
makeSourceMapGenerator,
addSourceMaps
addSourceMaps,
combinePlugins
} from './_utils'

let plugins
const getCss = (path, validate = false) => {
if (!path.isTemplateLiteral() && !path.isStringLiteral()) {
return
Expand All @@ -24,7 +26,6 @@ const getCss = (path, validate = false) => {
}

const getStyledJsx = (css, opts, path) => {
const useSourceMaps = Boolean(opts.sourceMaps)
const commonHash = hash(css.modified || css)
const globalHash = `1${commonHash}`
const scopedHash = `2${commonHash}`
Expand All @@ -34,24 +35,27 @@ const getStyledJsx = (css, opts, path) => {
const prefix = `[${MARKUP_ATTRIBUTE_EXTERNAL}~="${scopedHash}"]`
const isTemplateLiteral = Boolean(css.modified)

if (useSourceMaps) {
if (opts.sourceMaps) {
const generator = makeSourceMapGenerator(opts.file)
const filename = opts.sourceFileName
const offset = path.get('loc').node.start
compiledCss = [/* global */ '', prefix].map(prefix =>
addSourceMaps(
transform(prefix, css.modified || css, {
transform(prefix, opts.plugins(css.modified || css), {
generator,
offset,
filename
filename,
vendorPrefix: opts.vendorPrefix
}),
generator,
filename
)
)
} else {
compiledCss = ['', prefix].map(prefix =>
transform(prefix, css.modified || css)
transform(prefix, opts.plugins(css.modified || css), {
vendorPrefix: opts.vendorPrefix
})
)
}
globalCss = compiledCss[0]
Expand Down Expand Up @@ -156,15 +160,28 @@ const callVisitor = (visitor, path, state) => {
const { opts } = file
visitor(path, {
validate: state.opts.validate || opts.validate,
sourceMaps: opts.sourceMaps,
sourceMaps: state.opts.sourceMaps || opts.sourceMaps,
sourceFileName: opts.sourceFileName,
file
file,
plugins,
vendorPrefix: state.opts.vendorPrefix
})
}

export default function() {
return {
visitor: {
Program(path, state) {
if (!plugins) {
const { sourceMaps, vendorPrefix } = state.opts
plugins = combinePlugins(state.opts.plugins, {
sourceMaps: sourceMaps || state.file.opts.sourceMaps,
vendorPrefix: typeof vendorPrefix === 'boolean'
? vendorPrefix
: true
})
}
},
ExportDefaultDeclaration(path, state) {
callVisitor(exportDefaultDeclarationVisitor, path, state)
},
Expand Down
33 changes: 26 additions & 7 deletions src/babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
validateExpression,
generateAttribute,
makeSourceMapGenerator,
addSourceMaps
addSourceMaps,
combinePlugins
} from './_utils'

import {
Expand All @@ -29,15 +30,18 @@ import {
MARKUP_ATTRIBUTE_EXTERNAL
} from './_constants'

let plugins
const getPrefix = id => `[${MARKUP_ATTRIBUTE}="${id}"]`
const callExternalVisitor = (visitor, path, state) => {
const { file } = state
const { opts } = file
visitor(path, {
validate: true,
sourceMaps: opts.sourceMaps,
sourceMaps: state.opts.sourceMaps || opts.sourceMaps,
sourceFileName: opts.sourceFileName,
file
file,
plugins,
vendorPrefix: state.opts.vendorPrefix
})
}

Expand Down Expand Up @@ -292,7 +296,9 @@ export default function({ types: t }) {
// We replace styles with the function call
const [id, css, loc] = state.styles.shift()

const useSourceMaps = Boolean(state.file.opts.sourceMaps)
const useSourceMaps = Boolean(
state.opts.sourceMaps || state.file.opts.sourceMaps
)
let transformedCss

if (useSourceMaps) {
Expand All @@ -301,11 +307,12 @@ export default function({ types: t }) {
transformedCss = addSourceMaps(
transform(
isGlobal ? '' : getPrefix(state.jsxId),
css.modified || css,
plugins(css.modified || css),
{
generator,
offset: loc.start,
filename
filename,
vendorPrefix: state.opts.vendorPrefix
}
),
generator,
Expand All @@ -314,7 +321,10 @@ export default function({ types: t }) {
} else {
transformedCss = transform(
isGlobal ? '' : getPrefix(state.jsxId),
css.modified || css
plugins(css.modified || css),
{
vendorPrefix: state.opts.vendorPrefix
}
)
}

Expand All @@ -334,6 +344,15 @@ export default function({ types: t }) {
state.ignoreClosing = null
state.file.hasJSXStyle = false
state.imports = []
if (!plugins) {
const { sourceMaps, vendorPrefix } = state.opts
plugins = combinePlugins(state.opts.plugins, {
sourceMaps: sourceMaps || state.file.opts.sourceMaps,
vendorPrefix: typeof vendorPrefix === 'boolean'
? vendorPrefix
: true
})
}
},
exit({ node, scope }, state) {
if (!(state.file.hasJSXStyle && !scope.hasBinding(STYLE_COMPONENT))) {
Expand Down
5 changes: 5 additions & 0 deletions src/lib/style-transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ function transform(prefix, styles, settings = {}) {
generator = settings.generator
offset = settings.offset
filename = settings.filename
stylis.set({
prefix: typeof settings.vendorPrefix === 'boolean'
? settings.vendorPrefix
: true
})

return stylis(prefix, styles)
}
Expand Down
18 changes: 18 additions & 0 deletions test/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`generates source maps (babel options) 1`] = `
"import _JSXStyle from 'styled-jsx/style';
export default (() => <div data-jsx={188072295}>
<p data-jsx={188072295}>test</p>
<p data-jsx={188072295}>woot</p>
<_JSXStyle styleId={188072295} css={\\"p[data-jsx=\\\\\\"188072295\\\\\\"]{color:red}\\\\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNvdXJjZS1tYXBzLmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUlnQixBQUNjLFdBQUMiLCJmaWxlIjoic291cmNlLW1hcHMuanMiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCAoKSA9PiAoXG4gIDxkaXY+XG4gICAgPHA+dGVzdDwvcD5cbiAgICA8cD53b290PC9wPlxuICAgIDxzdHlsZSBqc3g+eydwIHsgY29sb3I6IHJlZCB9J308L3N0eWxlPlxuICA8L2Rpdj5cbilcbiJdfQ== */\\\\n/*@ sourceURL=source-maps.js */\\"} />
</div>);"
`;

exports[`generates source maps (plugin options) 1`] = `
"import _JSXStyle from 'styled-jsx/style';
export default (() => <div data-jsx={188072295}>
<p data-jsx={188072295}>test</p>
<p data-jsx={188072295}>woot</p>
<_JSXStyle styleId={188072295} css={\\"p[data-jsx=\\\\\\"188072295\\\\\\"]{color:red}\\\\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNvdXJjZS1tYXBzLmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUlnQixBQUNjLFdBQUMiLCJmaWxlIjoic291cmNlLW1hcHMuanMiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCAoKSA9PiAoXG4gIDxkaXY+XG4gICAgPHA+dGVzdDwvcD5cbiAgICA8cD53b290PC9wPlxuICAgIDxzdHlsZSBqc3g+eydwIHsgY29sb3I6IHJlZCB9J308L3N0eWxlPlxuICA8L2Rpdj5cbilcbiJdfQ== */\\\\n/*@ sourceURL=source-maps.js */\\"} />
</div>);"
`;

exports[`generates source maps 1`] = `
"import _JSXStyle from 'styled-jsx/style';
export default (() => <div data-jsx={188072295}>
Expand Down
13 changes: 13 additions & 0 deletions test/__snapshots__/plugins.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`applies plugins 1`] = `
"import _JSXStyle from 'styled-jsx/style';
import styles from './styles';
const color = 'red';

export default (() => <div data-jsx={4216192053} data-jsx-ext={styles.__scopedHash}>
<p data-jsx={4216192053} data-jsx-ext={styles.__scopedHash}>test</p>
<_JSXStyle styleId={4216192053} css={\`span.\${color}[data-jsx=\\"4216192053\\"]{color:\${otherColor}}\`} />
<_JSXStyle styleId={styles.__scopedHash} css={styles.__scoped} />
</div>);"
`;
1 change: 1 addition & 0 deletions test/fixtures/plugins/another-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default css => css.replace(/div/g, 'span')
1 change: 1 addition & 0 deletions test/fixtures/plugins/invalid-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'invalid'
1 change: 1 addition & 0 deletions test/fixtures/plugins/options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default (css, settings) => settings.options.test
1 change: 1 addition & 0 deletions test/fixtures/plugins/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default css => `TEST (${css}) EOTEST`
10 changes: 10 additions & 0 deletions test/fixtures/with-plugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styles from './styles'
const color = 'red'

export default () => (
<div>
<p>test</p>
<style jsx>{`div.${color} { color: ${otherColor} }`}</style>
<style jsx>{styles}</style>
</div>
)
Loading