├── .eslintignore ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── main.yml │ └── prs.yml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── CODEOWNERS ├── Changelog.md ├── babel-test.js ├── babel.js ├── css.d.ts ├── css.js ├── global.d.ts ├── index.d.ts ├── index.js ├── lib ├── style-transform.js └── stylesheet.js ├── license.md ├── macro.d.ts ├── macro.js ├── package.json ├── readme.md ├── src ├── .babelrc ├── _constants.js ├── _utils.js ├── babel-external.js ├── babel-test.js ├── babel.js ├── index.js ├── lib │ ├── hash.js │ ├── style-transform.js │ └── stylesheet.js ├── macro.js ├── style.js ├── stylesheet-registry.js └── webpack.js ├── style.d.ts ├── style.js ├── test ├── .babelrc ├── __snapshots__ │ ├── attribute.js.snap │ ├── external.js.snap │ ├── index.js.snap │ ├── macro.js.snap │ ├── plugins.js.snap │ └── styles.js.snap ├── _read.js ├── _transform.js ├── attribute.js ├── external.js ├── fixtures │ ├── absent.js │ ├── attribute-generation-classname-rewriting.js │ ├── attribute-generation-modes.js │ ├── class.js │ ├── component-attribute.js │ ├── conflicts.js │ ├── css-tag-same-file.js │ ├── different-jsx-ids.js │ ├── dynamic-element-class.js │ ├── dynamic-element-external.js │ ├── dynamic-element.js │ ├── dynamic-this-value-in-arrow.js │ ├── expressions.js │ ├── external-stylesheet-global.js │ ├── external-stylesheet-multi-line.js │ ├── external-stylesheet.js │ ├── fragment.js │ ├── global.js │ ├── ignore.js │ ├── macro.js │ ├── mixed-global-scoped.js │ ├── multiple-jsx.js │ ├── nested-style-tags.js │ ├── non-styled-jsx-style.js │ ├── not-styled-jsx-tagged-templates.js │ ├── plugins │ │ ├── another-plugin.js │ │ ├── invalid-plugin.js │ │ ├── multiple-options.js │ │ ├── options.js │ │ └── plugin.js │ ├── simple-fragment.js │ ├── source-maps.js │ ├── stateless.js │ ├── styles-external-invalid.js │ ├── styles-external-invalid2.js │ ├── styles.js │ ├── styles2.js │ ├── transform.css │ ├── whitespace.js │ └── with-plugins.js ├── helpers │ ├── babel-test.macro.js │ └── with-mock.js ├── index.js ├── index.ts ├── macro.js ├── plugins.js ├── snapshots │ ├── attribute.js.md │ ├── attribute.js.snap │ ├── external.js.md │ ├── external.js.snap │ ├── index.js.md │ ├── index.js.snap │ ├── macro.js.md │ ├── macro.js.snap │ ├── plugins.js.md │ ├── plugins.js.snap │ ├── styles.js.md │ └── styles.js.snap ├── styles.js ├── stylesheet-registry.js └── stylesheet.js ├── tsconfig.json ├── webpack.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | #### Do you want to request a _feature_ or report a _bug_? 12 | 13 | #### What is the current behavior? 14 | 15 | #### If the current behavior is a bug, please provide the steps to reproduce and possibly a minimal demo or testcase in the form of a Next.js app, CodeSandbox URL or similar 16 | 17 | #### What is the expected behavior? 18 | 19 | #### Environment (include versions) 20 | 21 | - Version of styled-jsx (or next.js if it's being used): 22 | - Browser: 23 | - OS: 24 | 25 | #### Did this work in previous versions? 26 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - alpha 8 | - beta 9 | tags: 10 | - '!*' 11 | pull_request: 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Begin CI... 19 | uses: actions/checkout@v4 20 | 21 | - name: Use Node 20 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20.x 25 | 26 | - name: Install dependencies 27 | run: yarn install --frozen-lockfile 28 | env: 29 | CI: true 30 | 31 | - name: Build 32 | run: yarn build 33 | env: 34 | CI: true 35 | 36 | - name: Lint 37 | run: yarn lint 38 | env: 39 | CI: true 40 | 41 | - name: Test 42 | run: yarn test 43 | env: 44 | CI: true 45 | 46 | - name: Test types 47 | run: yarn test-types 48 | env: 49 | CI: true 50 | 51 | - name: Release 52 | if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/alpha' || github.ref == 'refs/heads/beta') 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | NPM_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }} 56 | run: yarn semantic-release 57 | -------------------------------------------------------------------------------- /.github/workflows/prs.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint PR' 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | 9 | jobs: 10 | main: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: amannn/action-semantic-pull-request@v2.1.0 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # build output 5 | dist 6 | out 7 | 8 | # logs 9 | npm-debug.log 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | save-exact = true 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { "singleQuote": true, "semi": false } 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # optionally request a review from @rauchg, @nkzawa, @leo manually 2 | * @huozhi 3 | * @ijjk 4 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [5.0.0] 4 | 5 | ### Features 6 | 7 | - Introduce contextual styles (#744) 8 | - Opt-in react 18 insertion effect hook when available (#753) 9 | - Fallback to module level registry in browser (#768) 10 | 11 | ### Improvements 12 | 13 | - Make JSXStyle return a noop if the registry context is not provided (#749) 14 | - Fix typings of `nonce` property 15 | - Pre-compile dependencies to reduce install size/time (#770) 16 | 17 | ### BREAKING CHANGES 18 | 19 | #### APIs 20 | 21 | - `styled-jsx/server` import path is deprecated 22 | - `flush` and `flushToHTML` from `styled-jsx/server` APIs are deprecated 23 | - New component `` is introduced 24 | - New APIs `useStyleRegistry` and `createStyleRegistry` are introduced 25 | 26 | #### Usage 27 | 28 | If you're only using styled-jsx purely client side, nothing will effect you. 29 | If you're using styled-jsx inside Next.js without customization, Next.js will automatically handle the changes for you. 30 | 31 | If you have your own customization with styled-jsx in Next.js, for example you have a custom `_document`: 32 | By default, doing this will let Next.js collect styles and pass them down. 33 | 34 | ```jsx 35 | class Document extends React.Component { 36 | static async getInitialProps(ctx) { 37 | return await ctx.defaultGetInitialProps(ctx) 38 | } 39 | } 40 | ``` 41 | 42 | Or for instance you're passing `nonce` property in `getInitialProps` of `_document`, this will let you configure it: 43 | 44 | ```diff 45 | class Document extends React.Component { 46 | static async getInitialProps(ctx) { 47 | - return await ctx.defaultGetInitialProps(ctx) 48 | + return await ctx.defaultGetInitialProps(ctx, { nonce }) 49 | } 50 | } 51 | ``` 52 | 53 | If you're building the SSR solution yourself with other frameworks, please checkout the **Server-Side Rendering** section in readme. 54 | 55 | ## [4.0.1] 56 | 57 | - Mark `@babel/core` as optional peer dependency 58 | 59 | ## [4.0.0] 60 | 61 | - Use react hooks to manage styles injection (#720) 62 | 63 | ### BREAKING CHANGES 64 | 65 | - Drop support for react versions < 16.8.0 66 | 67 | ### Improvements 68 | 69 | - Drop babel 6 support (#730) 70 | - Auto publish alpha/beta versions 71 | 72 | ## [3.4.x] 73 | 74 | ### Improvements 75 | 76 | - Typing support 77 | - Inject unique \_JSXStyle identifier 78 | - Hide webpack loader warnings 79 | - Refactor the import helpers 80 | -------------------------------------------------------------------------------- /babel-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-ignore */ 2 | module.exports = require('./dist/babel').test() 3 | -------------------------------------------------------------------------------- /babel.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/babel').default 2 | -------------------------------------------------------------------------------- /css.d.ts: -------------------------------------------------------------------------------- 1 | // Definitions by: @types/styled-jsx 2 | 3 | declare module 'styled-jsx/css' { 4 | import type { JSX } from "react"; 5 | 6 | function css(chunks: TemplateStringsArray, ...args: any[]): JSX.Element 7 | namespace css { 8 | export function global( 9 | chunks: TemplateStringsArray, 10 | ...args: any[] 11 | ): JSX.Element 12 | export function resolve( 13 | chunks: TemplateStringsArray, 14 | ...args: any[] 15 | ): { className: string; styles: JSX.Element } 16 | } 17 | export = css 18 | } 19 | -------------------------------------------------------------------------------- /css.js: -------------------------------------------------------------------------------- 1 | function notTranspiledError(name) { 2 | throw new Error( 3 | 'styled-jsx/css: if you are getting this error it means that your `' + 4 | name + 5 | '` tagged template literals were not transpiled.' 6 | ) 7 | } 8 | 9 | function css() { 10 | notTranspiledError('css') 11 | } 12 | 13 | css.global = function() { 14 | notTranspiledError('global') 15 | } 16 | 17 | css.resolve = function() { 18 | notTranspiledError('resolve') 19 | } 20 | 21 | module.exports = css 22 | module.exports.global = css.global 23 | module.exports.resolve = css.resolve 24 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | declare module 'react' { 4 | interface StyleHTMLAttributes extends HTMLAttributes { 5 | jsx?: boolean 6 | global?: boolean 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | 6 | declare module 'styled-jsx' { 7 | import type { JSX } from "react"; 8 | 9 | export type StyledJsxStyleRegistry = { 10 | styles(options?: { nonce?: string }): JSX.Element[] 11 | flush(): void 12 | add(props: any): void 13 | remove(props: any): void 14 | } 15 | export function useStyleRegistry(): StyledJsxStyleRegistry 16 | export function StyleRegistry({ 17 | children, 18 | registry 19 | }: { 20 | children: JSX.Element | import('react').ReactNode 21 | registry?: StyledJsxStyleRegistry 22 | }): JSX.Element 23 | export function createStyleRegistry(): StyledJsxStyleRegistry 24 | } 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/index') 2 | -------------------------------------------------------------------------------- /lib/style-transform.js: -------------------------------------------------------------------------------- 1 | /* eslint-ignore */ 2 | module.exports = require('../dist/lib/style-transform') 3 | -------------------------------------------------------------------------------- /lib/stylesheet.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../dist/lib/stylesheet') 2 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /macro.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'styled-jsx/macro' { 2 | import type { JSX } from "react"; 3 | 4 | namespace macro { 5 | function resolve( 6 | chunks: TemplateStringsArray, 7 | ...args: any[] 8 | ): { 9 | className: string 10 | styles: JSX.Element 11 | } 12 | } 13 | 14 | export = macro 15 | } 16 | -------------------------------------------------------------------------------- /macro.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/babel').macro() 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styled-jsx", 3 | "version": "0.0.0-development", 4 | "license": "MIT", 5 | "repository": "vercel/styled-jsx", 6 | "description": "Full CSS support for JSX without compromises", 7 | "files": [ 8 | "dist", 9 | "lib", 10 | "global.d.ts", 11 | "index.d.ts", 12 | "index.js", 13 | "babel.js", 14 | "babel-test.js", 15 | "style.js", 16 | "style.d.ts", 17 | "macro.js", 18 | "macro.d.ts", 19 | "css.js", 20 | "css.d.ts", 21 | "webpack.js", 22 | "license.md" 23 | ], 24 | "typings": "./index.d.ts", 25 | "scripts": { 26 | "build-babel": "bunchee src/babel.js -f cjs -e babel-plugin-macros --runtime node -o dist/babel/index.js", 27 | "build": "rm -rf dist && rm -rf out && yarn build-webpack && yarn build-index && yarn build-babel", 28 | "build-webpack": "bunchee src/webpack.js -f cjs --runtime node -o dist/webpack/index.js", 29 | "build-index": "bunchee src/index.js -f cjs --runtime node -o dist/index/index.js", 30 | "test": "ava", 31 | "test-types": "tsc --project tsconfig.json --noEmit", 32 | "lint": "eslint ./src", 33 | "format": "prettier --write \"./{src,test}/**/*.{js,css}\"", 34 | "prepublishOnly": "yarn build && yarn test && yarn lint --quiet" 35 | }, 36 | "husky": { 37 | "hooks": { 38 | "pre-commit": "pretty-quick --staged" 39 | } 40 | }, 41 | "ava": { 42 | "require": [ 43 | "@babel/register" 44 | ] 45 | }, 46 | "eslintConfig": { 47 | "env": { 48 | "node": true, 49 | "browser": true, 50 | "es6": true 51 | }, 52 | "extends": [ 53 | "eslint:recommended", 54 | "prettier" 55 | ], 56 | "parserOptions": { 57 | "ecmaVersion": 11, 58 | "sourceType": "module" 59 | }, 60 | "rules": { 61 | "no-empty": 0, 62 | "capitalized-comments": 0, 63 | "valid-jsdoc": 0, 64 | "prefer-destructuring": 0, 65 | "padding-line-between-statements": 0 66 | } 67 | }, 68 | "devDependencies": { 69 | "@babel/cli": "7.18.10", 70 | "@babel/core": "7.12.3", 71 | "@babel/plugin-proposal-object-rest-spread": "7.12.1", 72 | "@babel/plugin-syntax-jsx": "7.14.5", 73 | "@babel/plugin-transform-arrow-functions": "7.12.1", 74 | "@babel/plugin-transform-modules-commonjs": "7.12.1", 75 | "@babel/plugin-transform-runtime": "7.12.1", 76 | "@babel/preset-env": "7.12.1", 77 | "@babel/preset-react": "7.12.5", 78 | "@babel/register": "7.12.1", 79 | "@babel/runtime": "7.12.5", 80 | "@babel/types": "7.15.0", 81 | "@types/react": "18.3.3", 82 | "ava": "4.3.1", 83 | "babel-plugin-macros": "2.8.0", 84 | "bunchee": "2.1.5", 85 | "convert-source-map": "1.7.0", 86 | "eslint": "7.32.0", 87 | "eslint-config-prettier": "4.0.0", 88 | "husky": "4.3.0", 89 | "loader-utils": "1.4.2", 90 | "prettier": "1.16.4", 91 | "pretty-quick": "3.1.0", 92 | "react": "17.0.1", 93 | "react-dom": "17.0.1", 94 | "semantic-release": "17.2.2", 95 | "source-map": "0.7.3", 96 | "string-hash": "1.1.3", 97 | "stylis": "3.5.4", 98 | "stylis-rule-sheet": "0.0.10", 99 | "typescript": "~5.0.0" 100 | }, 101 | "peerDependencies": { 102 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" 103 | }, 104 | "peerDependenciesMeta": { 105 | "@babel/core": { 106 | "optional": true 107 | }, 108 | "babel-plugin-macros": { 109 | "optional": true 110 | } 111 | }, 112 | "release": { 113 | "branches": [ 114 | "main", 115 | "alpha", 116 | "beta" 117 | ] 118 | }, 119 | "engines": { 120 | "node": ">= 12.0.0" 121 | }, 122 | "keywords": [ 123 | "babel-plugin-macros", 124 | "vercel", 125 | "zeit", 126 | "css-in-js", 127 | "css" 128 | ], 129 | "dependencies": { 130 | "client-only": "0.0.1" 131 | }, 132 | "packageManager": "yarn@1.22.22" 133 | } 134 | -------------------------------------------------------------------------------- /src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "loose": true }] 4 | ], 5 | "plugins": [ 6 | ["@babel/plugin-proposal-object-rest-spread", { "loose": true }] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/_constants.js: -------------------------------------------------------------------------------- 1 | export const GLOBAL_ATTRIBUTE = 'global' 2 | export const STYLE_ATTRIBUTE = 'jsx' 3 | export const STYLE_COMPONENT = '_JSXStyle' 4 | export const STYLE_COMPONENT_DYNAMIC = 'dynamic' 5 | export const STYLE_COMPONENT_ID = 'id' 6 | -------------------------------------------------------------------------------- /src/babel-external.js: -------------------------------------------------------------------------------- 1 | import * as t from '@babel/types' 2 | 3 | import { 4 | getJSXStyleInfo, 5 | processCss, 6 | cssToBabelType, 7 | validateExternalExpressions, 8 | getScope, 9 | computeClassNames, 10 | makeStyledJsxTag, 11 | setStateOptions 12 | } from './_utils' 13 | 14 | const isModuleExports = t.buildMatchMemberExpression('module.exports') 15 | 16 | export function processTaggedTemplateExpression({ 17 | type, 18 | path, 19 | file, 20 | splitRules, 21 | plugins, 22 | vendorPrefixes, 23 | sourceMaps, 24 | styleComponentImportName 25 | }) { 26 | const templateLiteral = path.get('quasi') 27 | let scope 28 | 29 | // Check whether there are undefined references or 30 | // references to this.something (e.g. props or state). 31 | // We allow dynamic styles only when resolving styles. 32 | if (type !== 'resolve') { 33 | validateExternalExpressions(templateLiteral) 34 | } else if (!path.scope.path.isProgram()) { 35 | scope = getScope(path) 36 | } 37 | 38 | const stylesInfo = getJSXStyleInfo(templateLiteral, scope) 39 | 40 | const { staticClassName, className } = computeClassNames( 41 | [stylesInfo], 42 | undefined, 43 | styleComponentImportName 44 | ) 45 | 46 | const styles = processCss( 47 | { 48 | ...stylesInfo, 49 | staticClassName, 50 | file, 51 | isGlobal: type === 'global', 52 | plugins, 53 | vendorPrefixes, 54 | sourceMaps 55 | }, 56 | { splitRules } 57 | ) 58 | 59 | if (type === 'resolve') { 60 | const { hash, css, expressions } = styles 61 | path.replaceWith( 62 | // { 63 | // styles: <_JSXStyle ... />, 64 | // className: 'jsx-123' 65 | // } 66 | t.objectExpression([ 67 | t.objectProperty( 68 | t.identifier('styles'), 69 | makeStyledJsxTag(hash, css, expressions, styleComponentImportName) 70 | ), 71 | t.objectProperty(t.identifier('className'), className) 72 | ]) 73 | ) 74 | return 75 | } 76 | 77 | const id = path.parentPath.node.id 78 | const baseExportName = id ? id.name : 'default' 79 | let parentPath = 80 | baseExportName === 'default' 81 | ? path.parentPath 82 | : path.findParent( 83 | path => 84 | path.isVariableDeclaration() || 85 | (path.isAssignmentExpression() && 86 | isModuleExports(path.get('left').node)) 87 | ) 88 | 89 | if (baseExportName !== 'default' && !parentPath.parentPath.isProgram()) { 90 | parentPath = parentPath.parentPath 91 | } 92 | 93 | const css = cssToBabelType(styles.css) 94 | const newPath = t.isArrayExpression(css) 95 | ? css 96 | : t.newExpression(t.identifier('String'), [css]) 97 | 98 | // default exports 99 | 100 | if (baseExportName === 'default') { 101 | const defaultExportIdentifier = path.scope.generateUidIdentifier( 102 | 'defaultExport' 103 | ) 104 | parentPath.insertBefore( 105 | t.variableDeclaration('const', [ 106 | t.variableDeclarator(defaultExportIdentifier, newPath) 107 | ]) 108 | ) 109 | 110 | parentPath.insertBefore(addHash(defaultExportIdentifier, styles.hash)) 111 | path.replaceWith(defaultExportIdentifier) 112 | return 113 | } 114 | 115 | // local and named exports 116 | 117 | parentPath.insertAfter(addHash(t.identifier(baseExportName), styles.hash)) 118 | path.replaceWith(newPath) 119 | } 120 | 121 | function addHash(exportIdentifier, hash) { 122 | const value = typeof hash === 'string' ? t.stringLiteral(hash) : hash 123 | return t.expressionStatement( 124 | t.assignmentExpression( 125 | '=', 126 | t.memberExpression(exportIdentifier, t.identifier('__hash')), 127 | value 128 | ) 129 | ) 130 | } 131 | 132 | export const visitor = { 133 | ImportDeclaration(path, state) { 134 | // import css from 'styled-jsx/css' 135 | if (path.node.source.value !== 'styled-jsx/css') { 136 | return 137 | } 138 | 139 | // Find all the imported specifiers. 140 | // e.g import css, { global, resolve } from 'styled-jsx/css' 141 | // -> ['css', 'global', 'resolve'] 142 | const specifiersNames = path.node.specifiers.map( 143 | specifier => specifier.local.name 144 | ) 145 | specifiersNames.forEach(tagName => { 146 | // Get all the reference paths i.e. the places that use the tagName above 147 | // eg. 148 | // css`div { color: red }` 149 | // css.global`div { color: red }` 150 | // global`div { color: red ` 151 | const binding = path.scope.getBinding(tagName) 152 | 153 | if (!binding || !Array.isArray(binding.referencePaths)) { 154 | return 155 | } 156 | 157 | // Produces an object containing all the TaggedTemplateExpression paths detected. 158 | // The object contains { scoped, global, resolve } 159 | const taggedTemplateExpressions = binding.referencePaths 160 | .map(ref => ref.parentPath) 161 | .reduce( 162 | (result, path) => { 163 | let taggedTemplateExpression 164 | if (path.isTaggedTemplateExpression()) { 165 | // css`` global`` resolve`` 166 | taggedTemplateExpression = path 167 | } else if ( 168 | path.parentPath && 169 | path.isMemberExpression() && 170 | path.parentPath.isTaggedTemplateExpression() 171 | ) { 172 | // This part is for css.global`` or css.resolve`` 173 | // using the default import css 174 | taggedTemplateExpression = path.parentPath 175 | } else { 176 | return result 177 | } 178 | 179 | const tag = taggedTemplateExpression.get('tag') 180 | const id = tag.isIdentifier() 181 | ? tag.node.name 182 | : tag.get('property').node.name 183 | 184 | if (result[id]) { 185 | result[id].push(taggedTemplateExpression) 186 | } else { 187 | result.scoped.push(taggedTemplateExpression) 188 | } 189 | 190 | return result 191 | }, 192 | { 193 | scoped: [], 194 | global: [], 195 | resolve: [] 196 | } 197 | ) 198 | 199 | let hasJSXStyle = false 200 | 201 | const { vendorPrefixes, sourceMaps } = state.opts 202 | 203 | Object.keys(taggedTemplateExpressions).forEach(type => 204 | taggedTemplateExpressions[type].forEach(path => { 205 | hasJSXStyle = true 206 | // Process each css block 207 | processTaggedTemplateExpression({ 208 | type, 209 | path, 210 | file: state.file, 211 | splitRules: 212 | typeof state.opts.optimizeForSpeed === 'boolean' 213 | ? state.opts.optimizeForSpeed 214 | : process.env.NODE_ENV === 'production', 215 | plugins: state.plugins, 216 | vendorPrefixes, 217 | sourceMaps, 218 | styleComponentImportName: state.styleComponentImportName 219 | }) 220 | }) 221 | ) 222 | 223 | const hasCssResolve = 224 | hasJSXStyle && taggedTemplateExpressions.resolve.length > 0 225 | 226 | // When using the `resolve` helper we need to add an import 227 | // for the _JSXStyle component `styled-jsx/style` 228 | if (hasCssResolve) { 229 | state.file.hasCssResolve = true 230 | } 231 | }) 232 | 233 | // Finally remove the import 234 | path.remove() 235 | } 236 | } 237 | 238 | export default function() { 239 | return { 240 | Program(path, state) { 241 | setStateOptions(state) 242 | }, 243 | ...visitor 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/babel-test.js: -------------------------------------------------------------------------------- 1 | import jsx from '@babel/plugin-syntax-jsx' 2 | 3 | export default function() { 4 | return { 5 | inherits: jsx, 6 | visitor: { 7 | JSXOpeningElement(path) { 8 | const el = path.node 9 | const { name } = el.name || {} 10 | 11 | if (name !== 'style') { 12 | return 13 | } 14 | 15 | el.attributes = el.attributes.filter(a => { 16 | const name = a.name.name 17 | return name !== 'jsx' && name !== 'global' 18 | }) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/babel.js: -------------------------------------------------------------------------------- 1 | import jsx from '@babel/plugin-syntax-jsx' 2 | 3 | import { visitor as externalStylesVisitor } from './babel-external' 4 | import { 5 | isGlobalEl, 6 | isStyledJsx, 7 | findStyles, 8 | makeStyledJsxTag, 9 | getJSXStyleInfo, 10 | computeClassNames, 11 | addClassName, 12 | getScope, 13 | processCss, 14 | createReactComponentImportDeclaration, 15 | setStateOptions 16 | } from './_utils' 17 | import { STYLE_COMPONENT } from './_constants' 18 | 19 | import { default as babelMacro } from './macro' 20 | import { default as babelTest } from './babel-test' 21 | 22 | export function macro() { 23 | return babelMacro(require('babel-plugin-macros')) 24 | } 25 | 26 | export function test() { 27 | return babelTest 28 | } 29 | 30 | export default function({ types: t }) { 31 | const jsxVisitors = { 32 | JSXOpeningElement(path, state) { 33 | const el = path.node 34 | const { name } = el.name || {} 35 | 36 | if (!state.hasJSXStyle) { 37 | return 38 | } 39 | 40 | if (state.ignoreClosing === null) { 41 | // We keep a counter of elements inside so that we 42 | // can keep track of when we exit the parent to reset state 43 | // note: if we wished to add an option to turn off 44 | // selectors to reach parent elements, it would suffice to 45 | // set this to `1` and do an early return instead 46 | state.ignoreClosing = 0 47 | } 48 | 49 | const tag = path.get('name') 50 | 51 | if ( 52 | name && 53 | name !== 'style' && 54 | name !== state.styleComponentImportName && 55 | (name.charAt(0) !== name.charAt(0).toUpperCase() || 56 | Object.values(path.scope.bindings).some(binding => 57 | binding.referencePaths.some(r => r === tag) 58 | )) 59 | ) { 60 | if (state.className) { 61 | addClassName(path, state.className) 62 | } 63 | } 64 | 65 | state.ignoreClosing++ 66 | // Next visit will be: JSXElement exit() 67 | }, 68 | JSXElement: { 69 | enter(path, state) { 70 | if (state.hasJSXStyle !== null) { 71 | return 72 | } 73 | 74 | const styles = findStyles(path) 75 | 76 | if (styles.length === 0) { 77 | return 78 | } 79 | 80 | state.styles = [] 81 | state.externalStyles = [] 82 | 83 | const scope = getScope(path) 84 | 85 | for (const style of styles) { 86 | // Compute children excluding whitespace 87 | const children = style.get('children').filter( 88 | c => 89 | t.isJSXExpressionContainer(c.node) || 90 | // Ignore whitespace around the expression container 91 | (t.isJSXText(c.node) && c.node.value.trim() !== '') 92 | ) 93 | 94 | if (children.length !== 1) { 95 | throw path.buildCodeFrameError( 96 | `Expected one child under ` + 97 | `JSX Style tag, but got ${children.length} ` + 98 | `(eg: )` 99 | ) 100 | } 101 | 102 | const child = children[0] 103 | 104 | if (!t.isJSXExpressionContainer(child)) { 105 | throw path.buildCodeFrameError( 106 | `Expected a child of ` + 107 | `type JSXExpressionContainer under JSX Style tag ` + 108 | `(eg: ), got ${child.type}` 109 | ) 110 | } 111 | 112 | const expression = child.get('expression') 113 | 114 | if (t.isIdentifier(expression)) { 115 | const idName = expression.node.name 116 | if (expression.scope.hasBinding(idName)) { 117 | const externalStylesIdentifier = t.identifier(idName) 118 | const isGlobal = isGlobalEl(style.get('openingElement').node) 119 | state.externalStyles.push([ 120 | t.memberExpression( 121 | externalStylesIdentifier, 122 | t.identifier('__hash') 123 | ), 124 | externalStylesIdentifier, 125 | isGlobal 126 | ]) 127 | continue 128 | } 129 | 130 | throw path.buildCodeFrameError( 131 | `The Identifier ` + 132 | `\`${expression.getSource()}\` is either \`undefined\` or ` + 133 | `it is not an external StyleSheet reference i.e. ` + 134 | `it doesn't come from an \`import\` or \`require\` statement` 135 | ) 136 | } 137 | 138 | if ( 139 | !t.isTemplateLiteral(expression) && 140 | !t.isStringLiteral(expression) 141 | ) { 142 | throw path.buildCodeFrameError( 143 | `Expected a template ` + 144 | `literal or String literal as the child of the ` + 145 | `JSX Style tag (eg: ),` + 146 | ` but got ${expression.type}` 147 | ) 148 | } 149 | 150 | state.styles.push(getJSXStyleInfo(expression, scope)) 151 | } 152 | 153 | let externalJsxId 154 | if (state.externalStyles.length > 0) { 155 | const expressions = state.externalStyles 156 | // Remove globals 157 | .filter(s => !s[2]) 158 | .map(s => s[0]) 159 | 160 | const expressionsLength = expressions.length 161 | 162 | if (expressionsLength === 0) { 163 | externalJsxId = null 164 | } else { 165 | // Construct a template literal of this form: 166 | // `jsx-${styles.__scopedHash} jsx-${otherStyles.__scopedHash}` 167 | externalJsxId = t.templateLiteral( 168 | [ 169 | t.templateElement({ raw: 'jsx-', cooked: 'jsx-' }), 170 | ...[...new Array(expressionsLength - 1).fill(null)].map(() => 171 | t.templateElement({ raw: ' jsx-', cooked: ' jsx-' }) 172 | ), 173 | t.templateElement({ raw: '', cooked: '' }, true) 174 | ], 175 | expressions 176 | ) 177 | } 178 | } 179 | 180 | if (state.styles.length > 0 || externalJsxId) { 181 | const { staticClassName, className } = computeClassNames( 182 | state.styles, 183 | externalJsxId, 184 | state.styleComponentImportName 185 | ) 186 | state.className = className 187 | state.staticClassName = staticClassName 188 | } 189 | 190 | state.hasJSXStyle = true 191 | state.file.hasJSXStyle = true 192 | 193 | // Next visit will be: JSXOpeningElement 194 | }, 195 | exit(path, state) { 196 | const isGlobal = isGlobalEl(path.node.openingElement) 197 | 198 | if (state.hasJSXStyle && !--state.ignoreClosing && !isGlobal) { 199 | state.hasJSXStyle = null 200 | state.className = null 201 | state.externalJsxId = null 202 | } 203 | 204 | if (!state.hasJSXStyle || !isStyledJsx(path)) { 205 | return 206 | } 207 | 208 | if (state.ignoreClosing > 1) { 209 | let styleTagSrc 210 | try { 211 | styleTagSrc = path.getSource() 212 | } catch (error) {} 213 | 214 | throw path.buildCodeFrameError( 215 | 'Detected nested style tag' + 216 | (styleTagSrc ? `: \n\n${styleTagSrc}\n\n` : ' ') + 217 | 'styled-jsx only allows style tags ' + 218 | 'to be direct descendants (children) of the outermost ' + 219 | 'JSX element i.e. the subtree root.' 220 | ) 221 | } 222 | 223 | if ( 224 | state.externalStyles.length > 0 && 225 | path.get('children').filter(child => { 226 | if (!t.isJSXExpressionContainer(child)) { 227 | return false 228 | } 229 | 230 | const expression = child.get('expression') 231 | return expression && expression.isIdentifier() 232 | }).length === 1 233 | ) { 234 | const [id, css] = state.externalStyles.shift() 235 | 236 | path.replaceWith( 237 | makeStyledJsxTag(id, css, [], state.styleComponentImportName) 238 | ) 239 | return 240 | } 241 | 242 | const { vendorPrefixes, sourceMaps } = state.opts 243 | const stylesInfo = { 244 | ...state.styles.shift(), 245 | file: state.file, 246 | staticClassName: state.staticClassName, 247 | isGlobal, 248 | plugins: state.plugins, 249 | vendorPrefixes, 250 | sourceMaps 251 | } 252 | const splitRules = 253 | typeof state.opts.optimizeForSpeed === 'boolean' 254 | ? state.opts.optimizeForSpeed 255 | : process.env.NODE_ENV === 'production' 256 | 257 | const { hash, css, expressions } = processCss(stylesInfo, { 258 | splitRules 259 | }) 260 | 261 | path.replaceWith( 262 | makeStyledJsxTag( 263 | hash, 264 | css, 265 | expressions, 266 | state.styleComponentImportName 267 | ) 268 | ) 269 | } 270 | } 271 | } 272 | 273 | // only apply JSXFragment visitor if supported 274 | if (t.isJSXFragment) { 275 | jsxVisitors.JSXFragment = jsxVisitors.JSXElement 276 | jsxVisitors.JSXOpeningFragment = { 277 | enter(path, state) { 278 | if (!state.hasJSXStyle) { 279 | return 280 | } 281 | 282 | if (state.ignoreClosing === null) { 283 | // We keep a counter of elements inside so that we 284 | // can keep track of when we exit the parent to reset state 285 | // note: if we wished to add an option to turn off 286 | // selectors to reach parent elements, it would suffice to 287 | // set this to `1` and do an early return instead 288 | state.ignoreClosing = 0 289 | } 290 | 291 | state.ignoreClosing++ 292 | } 293 | } 294 | } 295 | 296 | const visitors = { 297 | inherits: jsx, 298 | visitor: { 299 | Program: { 300 | enter(path, state) { 301 | setStateOptions(state) 302 | state.hasJSXStyle = null 303 | state.ignoreClosing = null 304 | state.file.hasJSXStyle = false 305 | state.file.hasCssResolve = false 306 | // create unique identifier for _JSXStyle component 307 | state.styleComponentImportName = path.scope.generateUidIdentifier( 308 | STYLE_COMPONENT 309 | ).name 310 | 311 | // we need to beat the arrow function transform and 312 | // possibly others so we traverse from here or else 313 | // dynamic values in classNames could be incorrect 314 | path.traverse(jsxVisitors, state) 315 | 316 | // Transpile external styles 317 | path.traverse(externalStylesVisitor, state) 318 | }, 319 | exit(path, state) { 320 | if (!state.file.hasJSXStyle && !state.file.hasCssResolve) { 321 | return 322 | } 323 | state.file.hasJSXStyle = true 324 | const importDeclaration = createReactComponentImportDeclaration(state) 325 | path.unshiftContainer('body', importDeclaration) 326 | } 327 | } 328 | } 329 | } 330 | 331 | return visitors 332 | } 333 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'client-only' 2 | 3 | export { 4 | StyleRegistry, 5 | createStyleRegistry, 6 | useStyleRegistry 7 | } from './stylesheet-registry' 8 | 9 | export { default as style } from './style' 10 | -------------------------------------------------------------------------------- /src/lib/hash.js: -------------------------------------------------------------------------------- 1 | import hashString from 'string-hash' 2 | 3 | const sanitize = rule => rule.replace(/\/style/gi, '\\/style') 4 | const cache = {} 5 | 6 | /** 7 | * computeId 8 | * 9 | * Compute and memoize a jsx id from a basedId and optionally props. 10 | */ 11 | export function computeId(baseId, props) { 12 | if (!props) { 13 | return `jsx-${baseId}` 14 | } 15 | 16 | const propsToString = String(props) 17 | const key = baseId + propsToString 18 | 19 | if (!cache[key]) { 20 | cache[key] = `jsx-${hashString(`${baseId}-${propsToString}`)}` 21 | } 22 | 23 | return cache[key] 24 | } 25 | 26 | /** 27 | * computeSelector 28 | * 29 | * Compute and memoize dynamic selectors. 30 | */ 31 | export function computeSelector(id, css) { 32 | const selectoPlaceholderRegexp = /__jsx-style-dynamic-selector/g 33 | // Sanitize SSR-ed CSS. 34 | // Client side code doesn't need to be sanitized since we use 35 | // document.createTextNode (dev) and the CSSOM api sheet.insertRule (prod). 36 | if (typeof window === 'undefined') { 37 | css = sanitize(css) 38 | } 39 | 40 | const idcss = id + css 41 | if (!cache[idcss]) { 42 | cache[idcss] = css.replace(selectoPlaceholderRegexp, id) 43 | } 44 | 45 | return cache[idcss] 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/style-transform.js: -------------------------------------------------------------------------------- 1 | import Stylis from 'stylis' 2 | import stylisRuleSheet from 'stylis-rule-sheet' 3 | 4 | const stylis = new Stylis() 5 | 6 | function disableNestingPlugin(...args) { 7 | let [context, , , parent = [], line, column] = args 8 | if (context === 2) { 9 | // replace null characters and trim 10 | // eslint-disable-next-line no-control-regex 11 | parent = (parent[0] || '').replace(/\u0000/g, '').trim() 12 | if (parent.length > 0 && parent.charAt(0) !== '@') { 13 | throw new Error( 14 | `Nesting detected at ${line}:${column}. ` + 15 | 'Unfortunately nesting is not supported by styled-jsx.' 16 | ) 17 | } 18 | } 19 | } 20 | 21 | let generator 22 | let filename 23 | let offset 24 | 25 | function sourceMapsPlugin(...args) { 26 | const [context, , , , line, column, length] = args 27 | 28 | // Pre-processed, init source map 29 | if (context === -1 && generator !== undefined) { 30 | generator.addMapping({ 31 | generated: { 32 | line: 1, 33 | column: 0 34 | }, 35 | source: filename, 36 | original: offset 37 | }) 38 | 39 | return 40 | } 41 | 42 | // Post-processed 43 | if (context === -2 && generator !== undefined) { 44 | generator = undefined 45 | offset = undefined 46 | filename = undefined 47 | 48 | return 49 | } 50 | 51 | // Selector/property, update source map 52 | if ((context === 1 || context === 2) && generator !== undefined) { 53 | generator.addMapping({ 54 | generated: { 55 | line: 1, 56 | column: length 57 | }, 58 | source: filename, 59 | original: { 60 | line: line + offset.line, 61 | column: column + offset.column 62 | } 63 | }) 64 | } 65 | } 66 | 67 | /** 68 | * splitRulesPlugin 69 | * Used to split a blob of css into an array of rules 70 | * that can inserted via sheet.insertRule 71 | */ 72 | let splitRules = [] 73 | 74 | const splitRulesPlugin = stylisRuleSheet(rule => { 75 | splitRules.push(rule) 76 | }) 77 | 78 | stylis.use(disableNestingPlugin) 79 | stylis.use(sourceMapsPlugin) 80 | stylis.use(splitRulesPlugin) 81 | stylis.set({ 82 | cascade: false, 83 | compress: true 84 | }) 85 | 86 | /** 87 | * Public transform function 88 | * 89 | * @param {String} hash 90 | * @param {String} styles 91 | * @param {Object} settings 92 | * @return {string} 93 | */ 94 | function transform(hash, styles, settings = {}) { 95 | generator = settings.generator 96 | offset = settings.offset 97 | filename = settings.filename 98 | splitRules = [] 99 | 100 | stylis.set({ 101 | prefix: 102 | typeof settings.vendorPrefixes === 'boolean' 103 | ? settings.vendorPrefixes 104 | : true 105 | }) 106 | 107 | stylis(hash, styles) 108 | 109 | if (settings.splitRules) { 110 | return splitRules 111 | } 112 | 113 | return splitRules.join('') 114 | } 115 | 116 | export default transform 117 | -------------------------------------------------------------------------------- /src/lib/stylesheet.js: -------------------------------------------------------------------------------- 1 | /* 2 | Based on Glamor's sheet 3 | https://github.com/threepointone/glamor/blob/667b480d31b3721a905021b26e1290ce92ca2879/src/sheet.js 4 | */ 5 | 6 | const isProd = 7 | typeof process !== 'undefined' && 8 | process.env && 9 | process.env.NODE_ENV === 'production' 10 | const isString = o => Object.prototype.toString.call(o) === '[object String]' 11 | 12 | export default class StyleSheet { 13 | constructor({ name = 'stylesheet', optimizeForSpeed = isProd } = {}) { 14 | invariant(isString(name), '`name` must be a string') 15 | this._name = name 16 | this._deletedRulePlaceholder = `#${name}-deleted-rule____{}` 17 | 18 | invariant( 19 | typeof optimizeForSpeed === 'boolean', 20 | '`optimizeForSpeed` must be a boolean' 21 | ) 22 | this._optimizeForSpeed = optimizeForSpeed 23 | this._serverSheet = undefined 24 | this._tags = [] 25 | this._injected = false 26 | this._rulesCount = 0 27 | 28 | const node = 29 | typeof window !== 'undefined' && 30 | document.querySelector('meta[property="csp-nonce"]') 31 | this._nonce = node ? node.getAttribute('content') : null 32 | } 33 | 34 | setOptimizeForSpeed(bool) { 35 | invariant( 36 | typeof bool === 'boolean', 37 | '`setOptimizeForSpeed` accepts a boolean' 38 | ) 39 | 40 | invariant( 41 | this._rulesCount === 0, 42 | 'optimizeForSpeed cannot be when rules have already been inserted' 43 | ) 44 | this.flush() 45 | this._optimizeForSpeed = bool 46 | this.inject() 47 | } 48 | 49 | isOptimizeForSpeed() { 50 | return this._optimizeForSpeed 51 | } 52 | 53 | inject() { 54 | invariant(!this._injected, 'sheet already injected') 55 | this._injected = true 56 | if (typeof window !== 'undefined' && this._optimizeForSpeed) { 57 | this._tags[0] = this.makeStyleTag(this._name) 58 | this._optimizeForSpeed = 'insertRule' in this.getSheet() 59 | if (!this._optimizeForSpeed) { 60 | if (!isProd) { 61 | console.warn( 62 | 'StyleSheet: optimizeForSpeed mode not supported falling back to standard mode.' 63 | ) 64 | } 65 | 66 | this.flush() 67 | this._injected = true 68 | } 69 | 70 | return 71 | } 72 | 73 | this._serverSheet = { 74 | cssRules: [], 75 | insertRule: (rule, index) => { 76 | if (typeof index === 'number') { 77 | this._serverSheet.cssRules[index] = { cssText: rule } 78 | } else { 79 | this._serverSheet.cssRules.push({ cssText: rule }) 80 | } 81 | 82 | return index 83 | }, 84 | deleteRule: index => { 85 | this._serverSheet.cssRules[index] = null 86 | } 87 | } 88 | } 89 | 90 | getSheetForTag(tag) { 91 | if (tag.sheet) { 92 | return tag.sheet 93 | } 94 | 95 | // this weirdness brought to you by firefox 96 | for (let i = 0; i < document.styleSheets.length; i++) { 97 | if (document.styleSheets[i].ownerNode === tag) { 98 | return document.styleSheets[i] 99 | } 100 | } 101 | } 102 | 103 | getSheet() { 104 | return this.getSheetForTag(this._tags[this._tags.length - 1]) 105 | } 106 | 107 | insertRule(rule, index) { 108 | invariant(isString(rule), '`insertRule` accepts only strings') 109 | 110 | if (typeof window === 'undefined') { 111 | if (typeof index !== 'number') { 112 | index = this._serverSheet.cssRules.length 113 | } 114 | 115 | this._serverSheet.insertRule(rule, index) 116 | return this._rulesCount++ 117 | } 118 | 119 | if (this._optimizeForSpeed) { 120 | const sheet = this.getSheet() 121 | if (typeof index !== 'number') { 122 | index = sheet.cssRules.length 123 | } 124 | 125 | // this weirdness for perf, and chrome's weird bug 126 | // https://stackoverflow.com/questions/20007992/chrome-suddenly-stopped-accepting-insertrule 127 | try { 128 | sheet.insertRule(rule, index) 129 | } catch (error) { 130 | if (!isProd) { 131 | console.warn( 132 | `StyleSheet: illegal rule: \n\n${rule}\n\nSee https://stackoverflow.com/q/20007992 for more info` 133 | ) 134 | } 135 | 136 | return -1 137 | } 138 | } else { 139 | const insertionPoint = this._tags[index] 140 | this._tags.push(this.makeStyleTag(this._name, rule, insertionPoint)) 141 | } 142 | 143 | return this._rulesCount++ 144 | } 145 | 146 | replaceRule(index, rule) { 147 | if (this._optimizeForSpeed || typeof window === 'undefined') { 148 | const sheet = 149 | typeof window !== 'undefined' ? this.getSheet() : this._serverSheet 150 | if (!rule.trim()) { 151 | rule = this._deletedRulePlaceholder 152 | } 153 | 154 | if (!sheet.cssRules[index]) { 155 | // @TBD Should we throw an error? 156 | return index 157 | } 158 | 159 | sheet.deleteRule(index) 160 | 161 | try { 162 | sheet.insertRule(rule, index) 163 | } catch (error) { 164 | if (!isProd) { 165 | console.warn( 166 | `StyleSheet: illegal rule: \n\n${rule}\n\nSee https://stackoverflow.com/q/20007992 for more info` 167 | ) 168 | } 169 | 170 | // In order to preserve the indices we insert a deleteRulePlaceholder 171 | sheet.insertRule(this._deletedRulePlaceholder, index) 172 | } 173 | } else { 174 | const tag = this._tags[index] 175 | invariant(tag, `old rule at index \`${index}\` not found`) 176 | tag.textContent = rule 177 | } 178 | 179 | return index 180 | } 181 | 182 | deleteRule(index) { 183 | if (typeof window === 'undefined') { 184 | this._serverSheet.deleteRule(index) 185 | return 186 | } 187 | 188 | if (this._optimizeForSpeed) { 189 | this.replaceRule(index, '') 190 | } else { 191 | const tag = this._tags[index] 192 | invariant(tag, `rule at index \`${index}\` not found`) 193 | tag.parentNode.removeChild(tag) 194 | this._tags[index] = null 195 | } 196 | } 197 | 198 | flush() { 199 | this._injected = false 200 | this._rulesCount = 0 201 | if (typeof window !== 'undefined') { 202 | this._tags.forEach(tag => tag && tag.parentNode.removeChild(tag)) 203 | this._tags = [] 204 | } else { 205 | // simpler on server 206 | this._serverSheet.cssRules = [] 207 | } 208 | } 209 | 210 | cssRules() { 211 | if (typeof window === 'undefined') { 212 | return this._serverSheet.cssRules 213 | } 214 | 215 | return this._tags.reduce((rules, tag) => { 216 | if (tag) { 217 | rules = rules.concat( 218 | Array.prototype.map.call(this.getSheetForTag(tag).cssRules, rule => 219 | rule.cssText === this._deletedRulePlaceholder ? null : rule 220 | ) 221 | ) 222 | } else { 223 | rules.push(null) 224 | } 225 | 226 | return rules 227 | }, []) 228 | } 229 | 230 | makeStyleTag(name, cssString, relativeToTag) { 231 | if (cssString) { 232 | invariant( 233 | isString(cssString), 234 | 'makeStyleTag accepts only strings as second parameter' 235 | ) 236 | } 237 | 238 | const tag = document.createElement('style') 239 | if (this._nonce) tag.setAttribute('nonce', this._nonce) 240 | tag.type = 'text/css' 241 | tag.setAttribute(`data-${name}`, '') 242 | 243 | if (cssString) { 244 | tag.appendChild(document.createTextNode(cssString)) 245 | } 246 | 247 | const head = document.head || document.getElementsByTagName('head')[0] 248 | 249 | if (relativeToTag) { 250 | head.insertBefore(tag, relativeToTag) 251 | } else { 252 | head.appendChild(tag) 253 | } 254 | 255 | return tag 256 | } 257 | 258 | get length() { 259 | return this._rulesCount 260 | } 261 | } 262 | 263 | function invariant(condition, message) { 264 | if (!condition) { 265 | throw new Error(`StyleSheet: ${message}.`) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/macro.js: -------------------------------------------------------------------------------- 1 | import { processTaggedTemplateExpression } from './babel-external' 2 | import { 3 | setStateOptions, 4 | createReactComponentImportDeclaration 5 | } from './_utils' 6 | import { STYLE_COMPONENT } from './_constants' 7 | 8 | export default ({ createMacro, MacroError }) => { 9 | return createMacro(styledJsxMacro) 10 | 11 | function styledJsxMacro({ references, state }) { 12 | setStateOptions(state) 13 | 14 | // Holds a reference to all the lines where strings are tagged using the `css` tag name. 15 | // We print a warning at the end of the macro in case there is any reference to css, 16 | // because `css` is generally used as default import name for 'styled-jsx/css'. 17 | // People who want to migrate from this macro to pure styled-jsx might have name conflicts issues. 18 | const cssReferences = [] 19 | 20 | // references looks like this 21 | // { 22 | // default: [path, path], 23 | // resolve: [path], 24 | // } 25 | Object.keys(references).forEach(refName => { 26 | // Enforce `resolve` as named import so people 27 | // can only import { resolve } from 'styled-jsx/macro' 28 | // or an alias of it eg. { resolve as foo } 29 | if (refName !== 'default' && refName !== 'resolve') { 30 | throw new MacroError( 31 | `Imported an invalid named import: ${refName}. Please import: resolve` 32 | ) 33 | } 34 | 35 | // Start processing the references for refName 36 | references[refName].forEach(path => { 37 | // We grab the parent path. Eg. 38 | // path -> css 39 | // path.parenPath -> css`div { color: red }` 40 | let templateExpression = path.parentPath 41 | 42 | // templateExpression member expression? 43 | // path -> css 44 | // path.parentPath -> css.resolve 45 | if (templateExpression.isMemberExpression()) { 46 | // grab .resolve 47 | const tagPropertyName = templateExpression.get('property').node.name 48 | // Member expressions are only valid on default imports 49 | // eg. import css from 'styled-jsx/macro' 50 | if (refName !== 'default') { 51 | throw new MacroError( 52 | `Can't use named import ${ 53 | path.node.name 54 | } as a member expression: ${ 55 | path.node.name 56 | }.${tagPropertyName}\`div { color: red }\` Please use it directly: ${ 57 | path.node.name 58 | }\`div { color: red }\`` 59 | ) 60 | } 61 | 62 | // Otherwise enforce `css.resolve` 63 | if (tagPropertyName !== 'resolve') { 64 | throw new MacroError( 65 | `Using an invalid tag: ${tagPropertyName}. Please use ${ 66 | templateExpression.get('object').node.name 67 | }.resolve` 68 | ) 69 | } 70 | 71 | // Grab the TaggedTemplateExpression 72 | // i.e. css.resolve`div { color: red }` 73 | templateExpression = templateExpression.parentPath 74 | } else { 75 | if (refName === 'default') { 76 | const { name } = path.node 77 | throw new MacroError( 78 | `Can't use default import directly eg. ${name}\`div { color: red }\`. Please use ${name}.resolve\`div { color: red }\` instead.` 79 | ) 80 | } 81 | 82 | if (path.node.name === 'css') { 83 | // If the path node name is `css` we push it to the references above to emit a warning later. 84 | cssReferences.push(path.node.loc.start.line) 85 | } 86 | } 87 | 88 | if (!state.styleComponentImportName) { 89 | const programPath = path.findParent(p => p.isProgram()) 90 | state.styleComponentImportName = programPath.scope.generateUidIdentifier( 91 | STYLE_COMPONENT 92 | ).name 93 | const importDeclaration = createReactComponentImportDeclaration(state) 94 | programPath.unshiftContainer('body', importDeclaration) 95 | } 96 | 97 | // Finally transform the path :) 98 | processTaggedTemplateExpression({ 99 | type: 'resolve', 100 | path: templateExpression, 101 | file: state.file, 102 | splitRules: 103 | typeof state.opts.optimizeForSpeed === 'boolean' 104 | ? state.opts.optimizeForSpeed 105 | : process.env.NODE_ENV === 'production', 106 | plugins: state.plugins, 107 | vendorPrefixes: state.opts.vendorPrefixes, 108 | sourceMaps: state.opts.sourceMaps, 109 | styleComponentImportName: state.styleComponentImportName 110 | }) 111 | }) 112 | }) 113 | 114 | if (cssReferences.length > 0) { 115 | console.warn( 116 | `styled-jsx - Warning - We detected that you named your tag as \`css\` at lines: ${cssReferences.join( 117 | ', ' 118 | )}.\n` + 119 | 'This tag name is usually used as default import name for `styled-jsx/css`.\n' + 120 | 'Porting macro code to pure styled-jsx in the future might be a bit problematic.' 121 | ) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/style.js: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useRef } from 'react' 2 | import { useStyleRegistry, createStyleRegistry } from './stylesheet-registry' 3 | import { computeId } from './lib/hash' 4 | 5 | // Opt-into the new `useInsertionEffect` API in React 18, fallback to `useLayoutEffect`. 6 | // https://github.com/reactwg/react-18/discussions/110 7 | const useInsertionEffect = React.useInsertionEffect || useLayoutEffect 8 | 9 | const defaultRegistry = 10 | typeof window !== 'undefined' ? createStyleRegistry() : undefined 11 | export default function JSXStyle(props) { 12 | const registry = defaultRegistry ? defaultRegistry : useStyleRegistry() 13 | const insertionEffectCalled = useRef(false) 14 | 15 | // `registry` might not exist while server-side rendering 16 | if (!registry) { 17 | return null 18 | } 19 | 20 | if (typeof window === 'undefined') { 21 | registry.add(props) 22 | return null 23 | } 24 | 25 | useInsertionEffect(() => { 26 | // ReactDOM removes all DOM during hydration in certain cases 27 | if (!document.head) { 28 | return 29 | } 30 | registry.add(props) 31 | insertionEffectCalled.current = true 32 | return () => { 33 | insertionEffectCalled.current = false 34 | registry.remove(props) 35 | } 36 | }, [props.id, String(props.dynamic)]) 37 | 38 | useLayoutEffect(() => { 39 | if (!document.head || insertionEffectCalled.current) { 40 | return 41 | } 42 | registry.add(props) 43 | return () => { 44 | registry.remove(props) 45 | } 46 | // props.children can be string[], will be striped since id is identical 47 | }, [props.id, String(props.dynamic)]) 48 | 49 | return null 50 | } 51 | 52 | JSXStyle.dynamic = info => { 53 | return info 54 | .map(tagInfo => { 55 | const baseId = tagInfo[0] 56 | const props = tagInfo[1] 57 | return computeId(baseId, props) 58 | }) 59 | .join(' ') 60 | } 61 | -------------------------------------------------------------------------------- /src/stylesheet-registry.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, createContext } from 'react' 2 | 3 | import DefaultStyleSheet from './lib/stylesheet' 4 | import { computeId, computeSelector } from './lib/hash' 5 | 6 | function mapRulesToStyle(cssRules, options = {}) { 7 | return cssRules.map(args => { 8 | const id = args[0] 9 | const css = args[1] 10 | return React.createElement('style', { 11 | id: `__${id}`, 12 | // Avoid warnings upon render with a key 13 | key: `__${id}`, 14 | nonce: options.nonce ? options.nonce : undefined, 15 | dangerouslySetInnerHTML: { 16 | __html: css 17 | } 18 | }) 19 | }) 20 | } 21 | export class StyleSheetRegistry { 22 | constructor({ styleSheet = null, optimizeForSpeed = false } = {}) { 23 | this._sheet = 24 | styleSheet || 25 | new DefaultStyleSheet({ 26 | name: 'styled-jsx', 27 | optimizeForSpeed 28 | }) 29 | 30 | this._sheet.inject() 31 | if (styleSheet && typeof optimizeForSpeed === 'boolean') { 32 | this._sheet.setOptimizeForSpeed(optimizeForSpeed) 33 | this._optimizeForSpeed = this._sheet.isOptimizeForSpeed() 34 | } 35 | 36 | this._fromServer = undefined 37 | this._indices = {} 38 | this._instancesCounts = {} 39 | } 40 | 41 | add(props) { 42 | if (undefined === this._optimizeForSpeed) { 43 | this._optimizeForSpeed = Array.isArray(props.children) 44 | this._sheet.setOptimizeForSpeed(this._optimizeForSpeed) 45 | this._optimizeForSpeed = this._sheet.isOptimizeForSpeed() 46 | } 47 | 48 | if (typeof window !== 'undefined' && !this._fromServer) { 49 | this._fromServer = this.selectFromServer() 50 | this._instancesCounts = Object.keys(this._fromServer).reduce( 51 | (acc, tagName) => { 52 | acc[tagName] = 0 53 | return acc 54 | }, 55 | {} 56 | ) 57 | } 58 | 59 | const { styleId, rules } = this.getIdAndRules(props) 60 | 61 | // Deduping: just increase the instances count. 62 | if (styleId in this._instancesCounts) { 63 | this._instancesCounts[styleId] += 1 64 | return 65 | } 66 | 67 | const indices = rules 68 | .map(rule => this._sheet.insertRule(rule)) 69 | // Filter out invalid rules 70 | .filter(index => index !== -1) 71 | 72 | this._indices[styleId] = indices 73 | this._instancesCounts[styleId] = 1 74 | } 75 | 76 | remove(props) { 77 | const { styleId } = this.getIdAndRules(props) 78 | invariant( 79 | styleId in this._instancesCounts, 80 | `styleId: \`${styleId}\` not found` 81 | ) 82 | this._instancesCounts[styleId] -= 1 83 | 84 | if (this._instancesCounts[styleId] < 1) { 85 | const tagFromServer = this._fromServer && this._fromServer[styleId] 86 | if (tagFromServer) { 87 | tagFromServer.parentNode.removeChild(tagFromServer) 88 | delete this._fromServer[styleId] 89 | } else { 90 | this._indices[styleId].forEach(index => this._sheet.deleteRule(index)) 91 | delete this._indices[styleId] 92 | } 93 | 94 | delete this._instancesCounts[styleId] 95 | } 96 | } 97 | 98 | update(props, nextProps) { 99 | this.add(nextProps) 100 | this.remove(props) 101 | } 102 | 103 | flush() { 104 | this._sheet.flush() 105 | this._sheet.inject() 106 | this._fromServer = undefined 107 | this._indices = {} 108 | this._instancesCounts = {} 109 | } 110 | 111 | cssRules() { 112 | const fromServer = this._fromServer 113 | ? Object.keys(this._fromServer).map(styleId => [ 114 | styleId, 115 | this._fromServer[styleId] 116 | ]) 117 | : [] 118 | const cssRules = this._sheet.cssRules() 119 | 120 | return fromServer.concat( 121 | Object.keys(this._indices) 122 | .map(styleId => [ 123 | styleId, 124 | this._indices[styleId] 125 | .map(index => cssRules[index].cssText) 126 | .join(this._optimizeForSpeed ? '' : '\n') 127 | ]) 128 | // filter out empty rules 129 | .filter(rule => Boolean(rule[1])) 130 | ) 131 | } 132 | 133 | styles(options) { 134 | return mapRulesToStyle(this.cssRules(), options) 135 | } 136 | 137 | getIdAndRules(props) { 138 | const { children: css, dynamic, id } = props 139 | 140 | if (dynamic) { 141 | const styleId = computeId(id, dynamic) 142 | return { 143 | styleId, 144 | rules: Array.isArray(css) 145 | ? css.map(rule => computeSelector(styleId, rule)) 146 | : [computeSelector(styleId, css)] 147 | } 148 | } 149 | 150 | return { 151 | styleId: computeId(id), 152 | rules: Array.isArray(css) ? css : [css] 153 | } 154 | } 155 | 156 | /** 157 | * selectFromServer 158 | * 159 | * Collects style tags from the document with id __jsx-XXX 160 | */ 161 | selectFromServer() { 162 | const elements = Array.prototype.slice.call( 163 | document.querySelectorAll('[id^="__jsx-"]') 164 | ) 165 | 166 | return elements.reduce((acc, element) => { 167 | const id = element.id.slice(2) 168 | acc[id] = element 169 | return acc 170 | }, {}) 171 | } 172 | } 173 | 174 | function invariant(condition, message) { 175 | if (!condition) { 176 | throw new Error(`StyleSheetRegistry: ${message}.`) 177 | } 178 | } 179 | 180 | export const StyleSheetContext = createContext(null) 181 | StyleSheetContext.displayName = 'StyleSheetContext' 182 | 183 | export function createStyleRegistry() { 184 | return new StyleSheetRegistry() 185 | } 186 | 187 | export function StyleRegistry({ registry: configuredRegistry, children }) { 188 | const rootRegistry = useContext(StyleSheetContext) 189 | const [registry] = useState( 190 | () => rootRegistry || configuredRegistry || createStyleRegistry() 191 | ) 192 | 193 | return React.createElement( 194 | StyleSheetContext.Provider, 195 | { value: registry }, 196 | children 197 | ) 198 | } 199 | 200 | export function useStyleRegistry() { 201 | return useContext(StyleSheetContext) 202 | } 203 | -------------------------------------------------------------------------------- /src/webpack.js: -------------------------------------------------------------------------------- 1 | import loaderUtils from 'loader-utils' 2 | 3 | const types = ['scoped', 'global', 'resolve'] 4 | 5 | export default function(content) { 6 | if (this.cacheable) this.cacheable() 7 | this.addDependency(this.resourcePath) 8 | const options = Object.assign({}, loaderUtils.getOptions(this)) 9 | 10 | if (!options.type) { 11 | options.type = 'scoped' 12 | } 13 | 14 | // Calls type with the current file name. 15 | if (typeof options.type === 'function') { 16 | options.type = options.type(this.resourcePath, { 17 | query: loaderUtils.parseQuery(this.resourceQuery || '?') || {} 18 | }) 19 | } 20 | 21 | if (!types.includes(options.type)) { 22 | return this.callback( 23 | 'The given `type` option is invalid. \n\n' + 24 | `Expected:\n One of scoped|global|resolve \n\n` + 25 | 'Actual:\n ' + 26 | options.type 27 | ) 28 | } 29 | 30 | // Allows to define the type for each individual file using a CSS comment. 31 | const commentType = content.match(/\/*\s*@styled-jsx=(scoped|global|resolve)/) 32 | if (commentType) { 33 | options.type = commentType[1] 34 | } 35 | 36 | let output = `import css from 'styled-jsx/css';\n\nconst styles = css` 37 | 38 | if (options.type === 'global') { 39 | // css.global`` 40 | output += '.global' 41 | } else if (options.type === 'resolve') { 42 | // css.resolve`` 43 | output += '.resolve' 44 | } 45 | // default css`` 46 | 47 | // Escape backticks and backslashes: “`” ⇒ “\`”, “\” ⇒ “\\” 48 | // (c) https://github.com/coox/styled-jsx-css-loader/blob/97a38e90dddf2c4b066e9247db0612c8f95302de/index.js#L6 49 | output += `\`${content.replace( 50 | /[`\\]/g, 51 | match => '\\' + match 52 | )}\`;\n\nexport default styles;` 53 | 54 | this.callback(null, output) 55 | } 56 | -------------------------------------------------------------------------------- /style.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'styled-jsx/style' { 2 | export default function JSXStyle(props: any): null 3 | } 4 | -------------------------------------------------------------------------------- /style.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/index').style 2 | -------------------------------------------------------------------------------- /test/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-object-rest-spread", 5 | "@babel/plugin-transform-runtime" 6 | ], 7 | "sourceMaps": false 8 | } 9 | -------------------------------------------------------------------------------- /test/__snapshots__/attribute.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`generate attribute for mixed modes (global, static, dynamic) 1`] = ` 4 | "import _JSXStyle from 'styled-jsx/style'; 5 | import styles from './styles'; 6 | 7 | const styles2 = require('./styles2'); 8 | 9 | // external only 10 | export const Test1 = () =>
11 |

external only

12 | <_JSXStyle id={styles.__hash}>{styles} 13 | <_JSXStyle id={styles2.__hash}>{styles2} 14 |
; 15 | 16 | // external and static 17 | export const Test2 = () =>
18 |

external and static

19 | <_JSXStyle id={\\"2982525546\\"}>{\\"p.jsx-2982525546{color:red;}\\"} 20 | <_JSXStyle id={styles.__hash}>{styles} 21 |
; 22 | 23 | // external and dynamic 24 | export const Test3 = ({ color }) =>
25 |

external and dynamic

26 | <_JSXStyle id={\\"1947484460\\"} dynamic={[color]}>{\`p.__jsx-style-dynamic-selector{color:\${color};}\`} 27 | <_JSXStyle id={styles.__hash}>{styles} 28 |
; 29 | 30 | // external, static and dynamic 31 | export const Test4 = ({ color }) =>
32 |

external, static and dynamic

33 | <_JSXStyle id={\\"3190985107\\"}>{\\"p.jsx-3190985107{display:inline-block;}\\"} 34 | <_JSXStyle id={\\"1336444426\\"} dynamic={[color]}>{\`p.__jsx-style-dynamic-selector{color:\${color};}\`} 35 | <_JSXStyle id={styles.__hash}>{styles} 36 |
; 37 | 38 | // static only 39 | export const Test5 = () =>
40 |

static only

41 | <_JSXStyle id={\\"3190985107\\"}>{\\"p.jsx-1372669040{display:inline-block;}\\"} 42 | <_JSXStyle id={\\"2982525546\\"}>{\\"p.jsx-1372669040{color:red;}\\"} 43 |
; 44 | 45 | // static and dynamic 46 | export const Test6 = ({ color }) =>
47 |

static and dynamic

48 | <_JSXStyle id={\\"3190985107\\"}>{\\"p.jsx-3190985107{display:inline-block;}\\"} 49 | <_JSXStyle id={\\"1336444426\\"} dynamic={[color]}>{\`p.__jsx-style-dynamic-selector{color:\${color};}\`} 50 |
; 51 | 52 | // dynamic only 53 | export const Test7 = ({ color }) =>
54 |

dynamic only

55 | <_JSXStyle id={\\"1947484460\\"} dynamic={[color]}>{\`p.__jsx-style-dynamic-selector{color:\${color};}\`} 56 |
; 57 | 58 | // dynamic with scoped compound variable 59 | export const Test8 = ({ color }) => { 60 | if (color) { 61 | const innerProps = { color }; 62 | 63 | return
64 |

dynamic with scoped compound variable

65 | <_JSXStyle id={\\"1791723528\\"} dynamic={[innerProps.color]}>{\`p.__jsx-style-dynamic-selector{color:\${innerProps.color};}\`} 66 |
; 67 | } 68 | }; 69 | 70 | // dynamic with compound variable 71 | export const Test9 = ({ color }) => { 72 | const innerProps = { color }; 73 | 74 | return
75 |

dynamic with compound variable

76 | <_JSXStyle id={\\"248922593\\"} dynamic={[innerProps.color]}>{\`p.__jsx-style-dynamic-selector{color:\${innerProps.color};}\`} 77 |
; 78 | }; 79 | 80 | const foo = 'red'; 81 | 82 | // dynamic with constant variable 83 | export const Test10 = () =>
84 |

dynamic with constant variable

85 | <_JSXStyle id={\\"461505126\\"}>{\`p.jsx-461505126{color:\${foo};}\`} 86 |
; 87 | 88 | // dynamic with complex scope 89 | export const Test11 = ({ color }) => { 90 | const items = Array.from({ length: 5 }).map((item, i) =>
  • 91 | <_JSXStyle id={\\"2172653867\\"} dynamic={[color]}>{\`.item.__jsx-style-dynamic-selector{color:\${color};}\`} 92 | Item #{i + 1} 93 |
  • ); 94 | 95 | return
      {items}
    ; 96 | };" 97 | `; 98 | 99 | exports[`rewrites className 1`] = ` 100 | "var _this = this; 101 | 102 | import _JSXStyle from \\"styled-jsx/style\\"; 103 | export default (() => { 104 | const Element = 'div'; 105 | return
    106 |
    107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 |
    114 |
    115 |
    116 |
    117 |
    118 |
    119 |
    120 |
    121 |
    122 |
    123 |
    124 |
    125 |
    126 |
    127 |
    128 |
    129 |
    130 |
    131 |
    132 |
    133 |
    134 |
    135 |
    136 |
    137 |
    138 |
    139 |
    140 | 141 | 142 | 143 | <_JSXStyle id={\\"2886504620\\"}>{\\"div.jsx-2886504620{color:red;}\\"} 144 |
    ; 145 | });" 146 | `; 147 | -------------------------------------------------------------------------------- /test/__snapshots__/external.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`(optimized) transpiles external stylesheets (CommonJS modules) 1`] = ` 4 | "const _defaultExport = ['div.jsx-2292456818{font-size:3em;}']; 5 | _defaultExport.__hash = '2292456818'; 6 | 7 | 8 | module.exports = _defaultExport;" 9 | `; 10 | 11 | exports[`(optimized) transpiles external stylesheets 1`] = ` 12 | "import _JSXStyle from 'styled-jsx/style'; 13 | 14 | import colors, { size } from './constants'; 15 | const color = 'red'; 16 | 17 | const bar = ['div.jsx-2141779268{font-size:3em;}']; 18 | 19 | bar.__hash = '2141779268'; 20 | const baz = ['div{font-size:3em;}']; 21 | 22 | baz.__hash = '2141779268'; 23 | a.__hash = '262929833'; 24 | const a = [\`div{font-size:\${size}em;}\`]; 25 | 26 | export const uh = bar; 27 | 28 | export const foo = [\`div.jsx-2299908427{color:\${color};}\`]; 29 | 30 | foo.__hash = '2299908427'; 31 | ({ 32 | styles: <_JSXStyle id={\\"1329679275\\"}>{[\`div.jsx-1329679275{color:\${colors.green.light};}\`, 'a.jsx-1329679275{color:red;}']}, 33 | className: 'jsx-1329679275' 34 | }); 35 | 36 | const b = { 37 | styles: <_JSXStyle id={\\"1329679275\\"}>{[\`div.jsx-1329679275{color:\${colors.green.light};}\`, 'a.jsx-1329679275{color:red;}']}, 38 | className: 'jsx-1329679275' 39 | }; 40 | 41 | const dynamic = colors => { 42 | const b = { 43 | styles: <_JSXStyle id={\\"3325296745\\"} dynamic={[colors.green.light]}>{[\`div.__jsx-style-dynamic-selector{color:\${colors.green.light};}\`, 'a.__jsx-style-dynamic-selector{color:red;}']}, 44 | className: _JSXStyle.dynamic([['3325296745', [colors.green.light]]]) 45 | }; 46 | }; 47 | 48 | export default { 49 | styles: <_JSXStyle id={\\"3290112549\\"}>{['div.jsx-3290112549{font-size:3em;}', \`p.jsx-3290112549{color:\${color};}\`]}, 50 | className: 'jsx-3290112549' 51 | };" 52 | `; 53 | 54 | exports[`Makes sure that style nodes are not re-used 1`] = ` 55 | "\\"use strict\\"; 56 | 57 | var _style = _interopRequireDefault(require(\\"styled-jsx/style\\")); 58 | 59 | var _App = _interopRequireDefault(require(\\"./App.styles\\")); 60 | 61 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 62 | 63 | function Test() { 64 | return
    65 | <_style.default id={_App.default.__hash}>{_App.default} 66 |
    ; 67 | }" 68 | `; 69 | 70 | exports[`does not transpile non-styled-jsx tagged teplate literals 1`] = ` 71 | "import css from 'hell'; 72 | 73 | const color = 'red'; 74 | 75 | const bar = css\` 76 | div { 77 | font-size: 3em; 78 | } 79 | \`; 80 | export const uh = bar; 81 | 82 | export const foo = css\`div { color: \${color}}\`; 83 | 84 | export default css\` 85 | div { 86 | font-size: 3em; 87 | } 88 | p { 89 | color: \${color}; 90 | } 91 | \`; 92 | 93 | const Title = styled.h1\` 94 | color: red; 95 | font-size: 50px; 96 | \`; 97 | 98 | const AnotherTitle = Title.extend\`color: blue;\`; 99 | 100 | export const Component = () => My page;" 101 | `; 102 | 103 | exports[`injects JSXStyle for nested scope 1`] = ` 104 | "import _JSXStyle from 'styled-jsx/style'; 105 | 106 | 107 | function test() { 108 | ({ 109 | styles: <_JSXStyle id={\\"2886504620\\"}>{\\"div.jsx-2886504620{color:red;}\\"}, 110 | className: 'jsx-2886504620' 111 | }); 112 | }" 113 | `; 114 | 115 | exports[`transpiles external stylesheets (CommonJS modules) 1`] = ` 116 | "const _defaultExport = new String('div.jsx-2292456818{font-size:3em;}'); 117 | 118 | _defaultExport.__hash = '2292456818'; 119 | 120 | 121 | module.exports = _defaultExport;" 122 | `; 123 | 124 | exports[`transpiles external stylesheets 1`] = ` 125 | "import _JSXStyle from 'styled-jsx/style'; 126 | 127 | import colors, { size } from './constants'; 128 | const color = 'red'; 129 | 130 | const bar = new String('div.jsx-2141779268{font-size:3em;}'); 131 | 132 | bar.__hash = '2141779268'; 133 | const baz = new String('div{font-size:3em;}'); 134 | 135 | baz.__hash = '2141779268'; 136 | a.__hash = '262929833'; 137 | const a = new String(\`div{font-size:\${size}em;}\`); 138 | 139 | export const uh = bar; 140 | 141 | export const foo = new String(\`div.jsx-2299908427{color:\${color};}\`); 142 | 143 | foo.__hash = '2299908427'; 144 | ({ 145 | styles: <_JSXStyle id={\\"1329679275\\"}>{\`div.jsx-1329679275{color:\${colors.green.light};}a.jsx-1329679275{color:red;}\`}, 146 | className: 'jsx-1329679275' 147 | }); 148 | 149 | const b = { 150 | styles: <_JSXStyle id={\\"1329679275\\"}>{\`div.jsx-1329679275{color:\${colors.green.light};}a.jsx-1329679275{color:red;}\`}, 151 | className: 'jsx-1329679275' 152 | }; 153 | 154 | const dynamic = colors => { 155 | const b = { 156 | styles: <_JSXStyle id={\\"3325296745\\"} dynamic={[colors.green.light]}>{\`div.__jsx-style-dynamic-selector{color:\${colors.green.light};}a.__jsx-style-dynamic-selector{color:red;}\`}, 157 | className: _JSXStyle.dynamic([['3325296745', [colors.green.light]]]) 158 | }; 159 | }; 160 | 161 | export default { 162 | styles: <_JSXStyle id={\\"3290112549\\"}>{\`div.jsx-3290112549{font-size:3em;}p.jsx-3290112549{color:\${color};}\`}, 163 | className: 'jsx-3290112549' 164 | };" 165 | `; 166 | 167 | exports[`use external stylesheet and dynamic element 1`] = ` 168 | "import _JSXStyle from \\"styled-jsx/style\\"; 169 | import styles from './styles2'; 170 | 171 | export default (({ level = 1 }) => { 172 | const Element = \`h\${level}\`; 173 | 174 | return 175 |

    dynamic element

    176 | <_JSXStyle id={styles.__hash}>{styles} 177 |
    ; 178 | });" 179 | `; 180 | 181 | exports[`use external stylesheets (global only) 1`] = ` 182 | "import _JSXStyle from 'styled-jsx/style'; 183 | import styles, { foo as styles3 } from './styles'; 184 | 185 | const styles2 = require('./styles2'); 186 | 187 | export default (() =>
    188 |

    test

    189 |
    woot
    190 |

    woot

    191 | <_JSXStyle id={styles2.__hash}>{styles2} 192 | <_JSXStyle id={styles3.__hash}>{styles3} 193 | <_JSXStyle id={styles.__hash}>{styles} 194 |
    );" 195 | `; 196 | 197 | exports[`use external stylesheets (multi-line) 1`] = ` 198 | "import _JSXStyle from 'styled-jsx/style'; 199 | import styles from './styles'; 200 | 201 | export default (() =>
    202 |

    test

    203 | <_JSXStyle id={styles.__hash}>{styles} 204 |
    );" 205 | `; 206 | 207 | exports[`use external stylesheets 1`] = ` 208 | "import _JSXStyle from 'styled-jsx/style'; 209 | import styles from './styles'; 210 | const styles2 = require('./styles2'); 211 | import { foo as styles3 } from './styles'; 212 | 213 | export default (() =>
    214 |

    test

    215 |

    woot

    216 | <_JSXStyle id={styles2.__hash}>{styles2} 217 | <_JSXStyle id={styles3.__hash}>{styles3} 218 |
    woot
    219 | <_JSXStyle id={\\"1646697228\\"}>{\\"p.jsx-1646697228{color:red;}div.jsx-1646697228{color:green;}\\"} 220 | <_JSXStyle id={styles.__hash}>{styles} 221 |
    ); 222 | 223 | export const Test = () =>
    224 |

    test

    225 |

    woot

    226 | <_JSXStyle id={styles3.__hash}>{styles3} 227 |
    woot
    228 | <_JSXStyle id={\\"1646697228\\"}>{\\"p.jsx-1646697228{color:red;}div.jsx-1646697228{color:green;}\\"} 229 |
    ;" 230 | `; 231 | -------------------------------------------------------------------------------- /test/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`generates source maps 1`] = ` 4 | "import _JSXStyle from 'styled-jsx/style'; 5 | export default (() =>
    6 |

    test

    7 |

    woot

    8 | <_JSXStyle id={\\"2743241663\\"}>{\\"p.jsx-2743241663{color:red;}\\\\n/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNvdXJjZS1tYXBzLmpzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUlnQixBQUNjLFVBQUMiLCJmaWxlIjoic291cmNlLW1hcHMuanMiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZGVmYXVsdCAoKSA9PiAoXG4gIDxkaXY+XG4gICAgPHA+dGVzdDwvcD5cbiAgICA8cD53b290PC9wPlxuICAgIDxzdHlsZSBqc3g+eydwIHsgY29sb3I6IHJlZCB9J308L3N0eWxlPlxuICA8L2Rpdj5cbilcbiJdfQ== */\\\\n/*@ sourceURL=source-maps.js */\\"} 9 |
    );" 10 | `; 11 | 12 | exports[`ignores when attribute is absent 1`] = ` 13 | "const a = () =>
    14 |

    hi

    15 | 16 |
    ;" 17 | `; 18 | 19 | exports[`ignores whitespace around expression container 1`] = ` 20 | "import _JSXStyle from 'styled-jsx/style'; 21 | export default (() =>
    22 |

    test

    23 |

    woot

    24 |

    woot

    25 | <_JSXStyle id={\\"2743241663\\"}>{\\"p.jsx-2743241663{color:red;}\\"} 26 |
    );" 27 | `; 28 | 29 | exports[`mixed global and scoped 1`] = ` 30 | "import _JSXStyle from 'styled-jsx/style'; 31 | const Test = () => <_JSXStyle id={\\"2743241663\\"}>{\\"p{color:red;}\\"}; 32 | 33 | export default (() =>
    34 |

    test

    35 | <_JSXStyle id={\\"4269072806\\"}>{\\"body{background:red;}\\"} 36 | <_JSXStyle id={\\"2743241663\\"}>{\\"p.jsx-2673076688{color:red;}\\"} 37 |
    );" 38 | `; 39 | 40 | exports[`should have different jsx ids 1`] = ` 41 | "import _JSXStyle from 'styled-jsx/style'; 42 | const color = 'red'; 43 | const otherColor = 'green'; 44 | 45 | const A = () =>
    46 |

    test

    47 | <_JSXStyle id={\\"57381496\\"}>{\`p.jsx-57381496{color:\${color};}\`} 48 |
    ; 49 | 50 | const B = () =>
    51 |

    test

    52 | <_JSXStyle id={\\"3099245642\\"}>{\`p.jsx-3099245642{color:\${otherColor};}\`} 53 |
    ; 54 | 55 | export default (() => );" 59 | `; 60 | 61 | exports[`should not add the data-jsx attribute to components instances 1`] = ` 62 | "import _JSXStyle from \\"styled-jsx/style\\"; 63 | const Test = () =>
    64 | test 65 | 66 | <_JSXStyle id={\\"2529315885\\"}>{\\"span.jsx-2529315885{color:red;}\\"} 67 |
    ;" 68 | `; 69 | 70 | exports[`works with class 1`] = ` 71 | "import _JSXStyle from \\"styled-jsx/style\\"; 72 | export default class { 73 | 74 | render() { 75 | return
    76 |

    test

    77 | <_JSXStyle id={\\"2101845350\\"}>{\\"p.jsx-2101845350{color:red;}\\"} 78 |
    ; 79 | } 80 | 81 | }" 82 | `; 83 | 84 | exports[`works with css tagged template literals in the same file 1`] = ` 85 | "import _JSXStyle from 'styled-jsx/style'; 86 | 87 | 88 | export default (({ children }) =>
    89 |

    {children}

    90 | <_JSXStyle id={styles.__hash}>{styles} 91 |
    ); 92 | 93 | const styles = new String('p.jsx-2587355013{color:red;}'); 94 | 95 | styles.__hash = '2587355013'; 96 | class Test extends React.Component { 97 | render() { 98 | return
    99 |

    {this.props.children}

    100 | <_JSXStyle id={styles.__hash}>{styles} 101 |
    ; 102 | } 103 | }" 104 | `; 105 | 106 | exports[`works with dynamic element 1`] = ` 107 | "import _JSXStyle from \\"styled-jsx/style\\"; 108 | export default (({ level = 1 }) => { 109 | const Element = \`h\${level}\`; 110 | 111 | return 112 |

    dynamic element

    113 | <_JSXStyle id={\\"1253978709\\"}>{\\".root.jsx-1253978709{background:red;}\\"} 114 |
    ; 115 | }); 116 | 117 | export const TestLowerCase = ({ level = 1 }) => { 118 | const element = \`h\${level}\`; 119 | 120 | return 121 |

    dynamic element

    122 | <_JSXStyle id={\\"1253978709\\"}>{\\".root.jsx-1253978709{background:red;}\\"} 123 |
    ; 124 | }; 125 | 126 | const Element2 = 'div'; 127 | export const Test2 = () => { 128 | return 129 |

    dynamic element

    130 | <_JSXStyle id={\\"1253978709\\"}>{\\".root.jsx-1253978709{background:red;}\\"} 131 |
    ; 132 | };" 133 | `; 134 | 135 | exports[`works with dynamic element in class 1`] = ` 136 | "import _JSXStyle from 'styled-jsx/style'; 137 | export default class { 138 | render() { 139 | const Element = 'div'; 140 | 141 | return 142 |

    dynamic element

    143 | <_JSXStyle id={\\"1800172487\\"}>{\\".root.jsx-1800172487{background:red;}\\"} 144 |
    ; 145 | } 146 | } 147 | 148 | const Element2 = 'div'; 149 | export const Test2 = class { 150 | render() { 151 | return 152 |

    dynamic element

    153 | <_JSXStyle id={\\"1800172487\\"}>{\\".root.jsx-1800172487{background:red;}\\"} 154 |
    ; 155 | } 156 | };" 157 | `; 158 | 159 | exports[`works with expressions in template literals 1`] = ` 160 | "import _JSXStyle from 'styled-jsx/style'; 161 | const darken = c => c; 162 | const color = 'red'; 163 | const otherColor = 'green'; 164 | const mediumScreen = '680px'; 165 | const animationDuration = '200ms'; 166 | const animationName = 'my-cool-animation'; 167 | const obj = { display: 'block' }; 168 | 169 | export default (({ display }) =>
    170 |

    test

    171 | <_JSXStyle id={\\"1003380713\\"}>{\`p.\${color}.jsx-3922596756{color:\${otherColor};display:\${obj.display};}\`} 172 | <_JSXStyle id={\\"2743241663\\"}>{\\"p.jsx-3922596756{color:red;}\\"} 173 | <_JSXStyle id={\\"602592955\\"}>{\`body{background:\${color};}\`} 174 | <_JSXStyle id={\\"602592955\\"}>{\`body{background:\${color};}\`} 175 | <_JSXStyle id={\\"128557999\\"}>{\`p.jsx-3922596756{color:\${color};}\`} 176 | <_JSXStyle id={\\"128557999\\"}>{\`p.jsx-3922596756{color:\${color};}\`} 177 | <_JSXStyle id={\\"2622100973\\"}>{\`p.jsx-3922596756{color:\${darken(color)};}\`} 178 | <_JSXStyle id={\\"1167419394\\"}>{\`p.jsx-3922596756{color:\${darken(color) + 2};}\`} 179 | <_JSXStyle id={\\"4052509549\\"}>{\`@media (min-width:\${mediumScreen}){p.jsx-3922596756{color:green;}p.jsx-3922596756{color:\${\`red\`};}}p.jsx-3922596756{color:red;}\`} 180 | <_JSXStyle id={\\"2824547816\\"}>{\`p.jsx-3922596756{-webkit-animation-duration:\${animationDuration};animation-duration:\${animationDuration};}\`} 181 | <_JSXStyle id={\\"417951176\\"}>{\`p.jsx-3922596756{-webkit-animation:\${animationDuration} forwards \${animationName};animation:\${animationDuration} forwards \${animationName};}div.jsx-3922596756{background:\${color};}\`} 182 | 183 | <_JSXStyle id={\\"1795062918\\"} dynamic={[display ? 'block' : 'none']}>{\`span.__jsx-style-dynamic-selector{display:\${display ? 'block' : 'none'};}\`} 184 |
    );" 185 | `; 186 | 187 | exports[`works with global styles 1`] = ` 188 | "import _JSXStyle from 'styled-jsx/style'; 189 | const Test = () =>
    190 | <_JSXStyle id={\\"2209073070\\"}>{\\"body{color:red;}:hover{color:red;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-animation:foo 1s ease-out;animation:foo 1s ease-out;}div a{display:none;}[data-test]>div{color:red;}\\"} 191 |
    ; 192 | 193 | const Test2 = () => <_JSXStyle id={\\"2743241663\\"}>{\\"p{color:red;}\\"};" 194 | `; 195 | 196 | exports[`works with multiple jsx blocks 1`] = ` 197 | "import _JSXStyle from \\"styled-jsx/style\\"; 198 | const attrs = { 199 | id: 'test' 200 | }; 201 | 202 | const Test1 = () =>
    203 | test 204 | 205 | <_JSXStyle id={\\"2529315885\\"}>{\\"span.jsx-2529315885{color:red;}\\"} 206 |
    ; 207 | 208 | const Test2 = () => test; 209 | 210 | const Test3 = () =>
    211 | test 212 | <_JSXStyle id={\\"2529315885\\"}>{\\"span.jsx-2529315885{color:red;}\\"} 213 |
    ; 214 | 215 | export default class { 216 | render() { 217 | return
    218 |

    test

    219 | <_JSXStyle id={\\"2101845350\\"}>{\\"p.jsx-2101845350{color:red;}\\"} 220 |
    ; 221 | } 222 | }" 223 | `; 224 | 225 | exports[`works with non styled-jsx styles 1`] = ` 226 | "import _JSXStyle from 'styled-jsx/style'; 227 | export default (() =>
    228 |

    woot

    229 | 230 | <_JSXStyle id={\\"2743241663\\"}>{\\"p.jsx-2743241663{color:red;}\\"} 231 |
    );" 232 | `; 233 | 234 | exports[`works with stateless 1`] = ` 235 | "import _JSXStyle from 'styled-jsx/style'; 236 | export default (() =>
    237 |

    test

    238 |

    woot

    239 |

    woot

    240 | <_JSXStyle id={\\"2743241663\\"}>{\\"p.jsx-2743241663{color:red;}\\"} 241 |
    );" 242 | `; 243 | -------------------------------------------------------------------------------- /test/__snapshots__/macro.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`can alias the named import 1`] = ` 4 | "import _JSXStyle from 'styled-jsx/style'; 5 | 6 | 7 | ({ 8 | styles: <_JSXStyle id={\\"2886504620\\"}>{\\"div.jsx-2886504620{color:red;}\\"}, 9 | className: 'jsx-2886504620' 10 | });" 11 | `; 12 | 13 | exports[`injects JSXStyle for nested scope 1`] = ` 14 | "import _JSXStyle from 'styled-jsx/style'; 15 | 16 | 17 | function test() { 18 | ({ 19 | styles: <_JSXStyle id={\\"2886504620\\"}>{\\"div.jsx-2886504620{color:red;}\\"}, 20 | className: 'jsx-2886504620' 21 | }); 22 | }" 23 | `; 24 | 25 | exports[`transpiles correctly 1`] = ` 26 | "import _JSXStyle from 'styled-jsx/style'; 27 | 28 | 29 | const { className, styles } = { 30 | styles: <_JSXStyle id={\\"2052294191\\"}>{\\"div.jsx-2052294191{color:red;}\\"}, 31 | className: 'jsx-2052294191' 32 | }; 33 | 34 | const dynamicStyles = props => ({ 35 | styles: <_JSXStyle id={\\"290194820\\"} dynamic={[props.color]}>{\`div.__jsx-style-dynamic-selector{color:\${props.color};}\`}, 36 | className: _JSXStyle.dynamic([['290194820', [props.color]]]) 37 | }); 38 | 39 | const test = { 40 | styles: <_JSXStyle id={\\"2052294191\\"}>{\\"div.jsx-2052294191{color:red;}\\"}, 41 | className: 'jsx-2052294191' 42 | }; 43 | 44 | const dynamicStyles2 = props => ({ 45 | styles: <_JSXStyle id={\\"290194820\\"} dynamic={[props.color]}>{\`div.__jsx-style-dynamic-selector{color:\${props.color};}\`}, 46 | className: _JSXStyle.dynamic([['290194820', [props.color]]]) 47 | }); 48 | 49 | const ExampleComponent = props => { 50 | const { className, styles } = dynamicStyles(props); 51 | 52 | return
    53 | howdy 54 | {styles} 55 |
    ; 56 | };" 57 | `; 58 | -------------------------------------------------------------------------------- /test/__snapshots__/plugins.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`applies plugins 1`] = ` 4 | "import _JSXStyle from 'styled-jsx/style'; 5 | import styles from './styles'; 6 | const color = 'red'; 7 | 8 | export default (() =>
    9 |

    test

    10 | <_JSXStyle id={\\"3382438999\\"}>{\`span.\${color}.jsx-3382438999{color:\${otherColor};}\`} 11 | <_JSXStyle id={styles.__hash}>{styles} 12 |
    );" 13 | `; 14 | 15 | exports[`babel-test plugin strips jsx attribute 1`] = ` 16 | "import styles from './styles'; 17 | const color = 'red'; 18 | 19 | export default (() =>
    20 |

    test

    21 | 26 | 27 |
    );" 28 | `; 29 | 30 | exports[`passes options to plugins 1`] = ` 31 | "import _JSXStyle from 'styled-jsx/style'; 32 | import styles from './styles'; 33 | const color = 'red'; 34 | 35 | export default (() =>
    36 |

    test

    37 | <_JSXStyle id={\\"3382438999\\"}>{\\".test.jsx-3382438999{content:\\\\\\"{\\\\\\"foo\\\\\\":false,\\\\\\"babel\\\\\\":{\\\\\\"location\\\\\\":{\\\\\\"start\\\\\\":{\\\\\\"line\\\\\\":7,\\\\\\"column\\\\\\":16},\\\\\\"end\\\\\\":{\\\\\\"line\\\\\\":11,\\\\\\"column\\\\\\":5}},\\\\\\"vendorPrefixes\\\\\\":false,\\\\\\"sourceMaps\\\\\\":false,\\\\\\"isGlobal\\\\\\":false}}\\\\\\";}\\"} 38 | <_JSXStyle id={styles.__hash}>{styles} 39 |
    );" 40 | `; 41 | -------------------------------------------------------------------------------- /test/__snapshots__/styles.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`transpile styles with attributes 1`] = `"html.jsx-123{background-image: linear-gradient(0deg,rgba(255,255,255,0.8),rgba(255,255,255,0.8)), url(/static/background.svg);}p{color:blue;}p{color:blue;}p,a.jsx-123{color:blue;}.foo + a{color:red;}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;}p.jsx-123{color:red;}p.jsx-123{color:red;}*.jsx-123{color:blue;}[href=\\"woot\\"].jsx-123{color:red;}p.jsx-123 a.jsx-123 span.jsx-123{color:red;}p.jsx-123 span{background:blue;}p.jsx-123 a[title=\\"'w ' ' t'\\"].jsx-123{margin:auto;}p.jsx-123 span:not(.test){color:green;}p.jsx-123,h1.jsx-123{color:blue;-webkit-animation:hahaha-jsx-123 3s ease forwards infinite;animation:hahaha-jsx-123 3s ease forwards infinite;-webkit-animation-name:hahaha-jsx-123;animation-name:hahaha-jsx-123;-webkit-animation-delay:100ms;animation-delay:100ms;}p.jsx-123{-webkit-animation:hahaha-jsx-123 1s,hehehe-jsx-123 2s;animation:hahaha-jsx-123 1s,hehehe-jsx-123 2s;}p.jsx-123:hover{color:red;}p.jsx-123::before{color:red;}.jsx-123:hover{color:red;}.jsx-123::before{color:red;}.jsx-123:hover p.jsx-123{color:red;}p.jsx-123+a.jsx-123{color:red;}p.jsx-123~a.jsx-123{color:red;}p.jsx-123>a.jsx-123{color:red;}p.jsx-123>>a.jsx-123{color:red;}@-webkit-keyframes hahaha-jsx-123{from{top:0;}to{top:100;}}@keyframes hahaha-jsx-123{from{top:0;}to{top:100;}}@-webkit-keyframes hehehe-jsx-123{from{left:0;}to{left:100;}}@keyframes hehehe-jsx-123{from{left:0;}to{left:100;}}@media (min-width:500px){.test.jsx-123{color:red;}}.test.jsx-123{display:block;}.inline-flex.jsx-123{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;}.flex.jsx-123{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.test.jsx-123{box-shadow:0 0 10px black,inset 0 0 5px black;}.test[title=\\",\\"].jsx-123{display:inline-block;}.test.is-status.jsx-123 .test.jsx-123{color:red;}.a-selector.jsx-123:hover,.a-selector.jsx-123:focus{outline:none;}@media (min-width:1px) and (max-width:768px){[class*='grid__col--'].jsx-123{margin-top:12px;margin-bottom:12px;}}@media (max-width:64em){.test.jsx-123{margin-bottom:1em;}@supports (-moz-appearance:none) and (display:contents){.test.jsx-123{margin-bottom:2rem;}}}"`; 4 | -------------------------------------------------------------------------------- /test/_read.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { promises as fs } from 'fs' 3 | 4 | export default async path => { 5 | const buffer = await fs.readFile(resolve(__dirname, path)) 6 | return buffer.toString() 7 | } 8 | -------------------------------------------------------------------------------- /test/_transform.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { transform, transformFile } from '@babel/core' 3 | 4 | export default (file, opts = {}) => 5 | new Promise((resolve, reject) => { 6 | transformFile( 7 | path.resolve(__dirname, file), 8 | { 9 | babelrc: false, 10 | ...opts 11 | }, 12 | (error, data) => { 13 | if (error) { 14 | return reject(error) 15 | } 16 | 17 | resolve(data) 18 | } 19 | ) 20 | }) 21 | 22 | export const transformSource = (src, opts = {}) => 23 | new Promise((resolve, reject) => { 24 | transform( 25 | src, 26 | { 27 | babelrc: false, 28 | ...opts 29 | }, 30 | (error, result) => { 31 | if (error) { 32 | return reject(error) 33 | } 34 | 35 | resolve(result) 36 | } 37 | ) 38 | }) 39 | -------------------------------------------------------------------------------- /test/attribute.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | import test from 'ava' 3 | 4 | // Ours 5 | import plugin from '../src/babel' 6 | import _transform from './_transform' 7 | 8 | const transform = (file, opts = {}) => 9 | _transform(file, { 10 | plugins: [plugin], 11 | ...opts 12 | }) 13 | 14 | test('rewrites className', async t => { 15 | const { code } = await transform( 16 | './fixtures/attribute-generation-classname-rewriting.js' 17 | ) 18 | t.snapshot(code) 19 | }) 20 | 21 | test('generate attribute for mixed modes (global, static, dynamic)', async t => { 22 | const { code } = await transform('./fixtures/attribute-generation-modes.js') 23 | t.snapshot(code) 24 | }) 25 | -------------------------------------------------------------------------------- /test/external.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | import test from 'ava' 3 | 4 | // Ours 5 | import plugin from '../src/babel' 6 | import _transform, { transformSource as _transformSource } from './_transform' 7 | 8 | const transform = (file, opts = {}) => 9 | _transform(file, { 10 | plugins: [[plugin, opts]] 11 | }) 12 | 13 | const transformSource = (src, opts = {}) => 14 | _transformSource(src.trim(), { 15 | plugins: [[plugin, opts]], 16 | ...opts 17 | }) 18 | 19 | test('transpiles external stylesheets', async t => { 20 | const { code } = await transform('./fixtures/styles.js') 21 | t.snapshot(code) 22 | }) 23 | 24 | test('(optimized) transpiles external stylesheets', async t => { 25 | const { code } = await transform('./fixtures/styles.js', { 26 | optimizeForSpeed: true 27 | }) 28 | t.snapshot(code) 29 | }) 30 | 31 | test('transpiles external stylesheets (CommonJS modules)', async t => { 32 | const { code } = await transform('./fixtures/styles2.js') 33 | t.snapshot(code) 34 | }) 35 | 36 | test('(optimized) transpiles external stylesheets (CommonJS modules)', async t => { 37 | const { code } = await transform('./fixtures/styles2.js', { 38 | optimizeForSpeed: true 39 | }) 40 | t.snapshot(code) 41 | }) 42 | 43 | test('does not transpile non-styled-jsx tagged teplate literals', async t => { 44 | const { code } = await transform( 45 | './fixtures/not-styled-jsx-tagged-templates.js' 46 | ) 47 | t.snapshot(code) 48 | }) 49 | 50 | test('throws when using `this.something` in external stylesheets', async t => { 51 | const { message } = await t.throwsAsync(() => 52 | transform('./fixtures/styles-external-invalid.js') 53 | ) 54 | t.regex(message, /this\.props/) 55 | }) 56 | 57 | test('throws when referring an undefined value in external stylesheets', async t => { 58 | const { message } = await t.throwsAsync(() => 59 | transform('./fixtures/styles-external-invalid2.js') 60 | ) 61 | t.regex(message, /props\.color/) 62 | }) 63 | 64 | test('use external stylesheets', async t => { 65 | const { code } = await transform('./fixtures/external-stylesheet.js') 66 | t.snapshot(code) 67 | }) 68 | 69 | test('use external stylesheets (multi-line)', async t => { 70 | const { code } = await transform( 71 | './fixtures/external-stylesheet-multi-line.js' 72 | ) 73 | t.snapshot(code) 74 | }) 75 | 76 | test('use external stylesheets (global only)', async t => { 77 | const { code } = await transform('./fixtures/external-stylesheet-global.js') 78 | t.snapshot(code) 79 | }) 80 | 81 | test('injects JSXStyle for nested scope', async t => { 82 | const { code } = await transformSource(` 83 | import css from 'styled-jsx/css' 84 | 85 | function test() { 86 | css.resolve\`div { color: red }\` 87 | } 88 | `) 89 | t.snapshot(code) 90 | }) 91 | 92 | test('use external stylesheet and dynamic element', async t => { 93 | const { code } = await transform('./fixtures/dynamic-element-external.js') 94 | t.snapshot(code) 95 | }) 96 | 97 | test('Makes sure that style nodes are not re-used', async t => { 98 | const { code } = await transformSource( 99 | ` 100 | import styles from './App.styles'; 101 | 102 | function Test() { 103 | return
    104 | 105 |
    106 | } 107 | `, 108 | { 109 | babelrc: false, 110 | plugins: [plugin, '@babel/plugin-transform-modules-commonjs'] 111 | } 112 | ) 113 | 114 | t.snapshot(code) 115 | }) 116 | 117 | test('Make sure that it works with the new automatic transform', async t => { 118 | const { code } = await transformSource( 119 | ` 120 | import css from "styled-jsx/css"; 121 | 122 | const A = css.resolve\` 123 | div { 124 | color: green; 125 | } 126 | \`; 127 | 128 | export default function IndexPage() { 129 | return JSON.stringify(A); 130 | } 131 | `, 132 | { 133 | babelrc: false, 134 | presets: [['@babel/preset-react', { runtime: 'automatic' }]], 135 | plugins: [plugin] 136 | } 137 | ) 138 | 139 | t.snapshot(code) 140 | }) 141 | -------------------------------------------------------------------------------- /test/fixtures/absent.js: -------------------------------------------------------------------------------- 1 | const a = () => ( 2 |
    3 |

    hi

    4 | 5 |
    6 | ) 7 | -------------------------------------------------------------------------------- /test/fixtures/attribute-generation-classname-rewriting.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | const Element = 'div' 3 | return ( 4 |
    5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 |
    43 |
    44 |
    45 |
    46 |
    47 |
    48 |
    49 |
    50 | 51 | 52 | 53 | 54 |
    55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /test/fixtures/attribute-generation-modes.js: -------------------------------------------------------------------------------- 1 | import styles from './styles' 2 | 3 | const styles2 = require('./styles2') 4 | 5 | // external only 6 | export const Test1 = () => ( 7 |
    8 |

    external only

    9 | 10 | 11 |
    12 | ) 13 | 14 | // external and static 15 | export const Test2 = () => ( 16 |
    17 |

    external and static

    18 | 23 | 24 |
    25 | ) 26 | 27 | // external and dynamic 28 | export const Test3 = ({ color }) => ( 29 |
    30 |

    external and dynamic

    31 | 36 | 37 |
    38 | ) 39 | 40 | // external, static and dynamic 41 | export const Test4 = ({ color }) => ( 42 |
    43 |

    external, static and dynamic

    44 | 49 | 54 | 55 |
    56 | ) 57 | 58 | // static only 59 | export const Test5 = () => ( 60 |
    61 |

    static only

    62 | 67 | 72 |
    73 | ) 74 | 75 | // static and dynamic 76 | export const Test6 = ({ color }) => ( 77 |
    78 |

    static and dynamic

    79 | 84 | 89 |
    90 | ) 91 | 92 | // dynamic only 93 | export const Test7 = ({ color }) => ( 94 |
    95 |

    dynamic only

    96 | 101 |
    102 | ) 103 | 104 | // dynamic with scoped compound variable 105 | export const Test8 = ({ color }) => { 106 | if (color) { 107 | const innerProps = { color } 108 | 109 | return ( 110 |
    111 |

    dynamic with scoped compound variable

    112 | 117 |
    118 | ) 119 | } 120 | } 121 | 122 | // dynamic with compound variable 123 | export const Test9 = ({ color }) => { 124 | const innerProps = { color } 125 | 126 | return ( 127 |
    128 |

    dynamic with compound variable

    129 | 134 |
    135 | ) 136 | } 137 | 138 | const foo = 'red' 139 | 140 | // dynamic with constant variable 141 | export const Test10 = () => ( 142 |
    143 |

    dynamic with constant variable

    144 | 149 |
    150 | ) 151 | 152 | // dynamic with complex scope 153 | export const Test11 = ({ color }) => { 154 | const items = Array.from({ length: 5 }).map((item, i) => ( 155 |
  • 156 | 161 | Item #{i + 1} 162 |
  • 163 | )) 164 | 165 | return
      {items}
    166 | } 167 | -------------------------------------------------------------------------------- /test/fixtures/class.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | render() { 3 | return ( 4 |
    5 |

    test

    6 | 11 |
    12 | ) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/component-attribute.js: -------------------------------------------------------------------------------- 1 | const Test = () => ( 2 |
    3 | test 4 | 5 | 10 |
    11 | ) 12 | -------------------------------------------------------------------------------- /test/fixtures/conflicts.js: -------------------------------------------------------------------------------- 1 | export const _JSXStyle = '_JSXStyle-literal' 2 | export default function() { 3 | return ( 4 |
    5 |

    test

    6 | 11 |
    12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/css-tag-same-file.js: -------------------------------------------------------------------------------- 1 | import css from 'styled-jsx/css' 2 | 3 | export default ({ children }) => ( 4 |
    5 |

    {children}

    6 | 7 |
    8 | ) 9 | 10 | const styles = css` 11 | p { 12 | color: red; 13 | } 14 | ` 15 | 16 | class Test extends React.Component { 17 | render() { 18 | return ( 19 |
    20 |

    {this.props.children}

    21 | 22 |
    23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/different-jsx-ids.js: -------------------------------------------------------------------------------- 1 | const color = 'red' 2 | const otherColor = 'green' 3 | 4 | const A = () => ( 5 |
    6 |

    test

    7 | 12 |
    13 | ) 14 | 15 | const B = () => ( 16 |
    17 |

    test

    18 | 23 |
    24 | ) 25 | 26 | export default () => ( 27 | 31 | ) 32 | -------------------------------------------------------------------------------- /test/fixtures/dynamic-element-class.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | render() { 3 | const Element = 'div' 4 | 5 | return ( 6 | 7 |

    dynamic element

    8 | 13 |
    14 | ) 15 | } 16 | } 17 | 18 | const Element2 = 'div' 19 | export const Test2 = class { 20 | render() { 21 | return ( 22 | 23 |

    dynamic element

    24 | 29 |
    30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/fixtures/dynamic-element-external.js: -------------------------------------------------------------------------------- 1 | import styles from './styles2' 2 | 3 | export default ({ level = 1 }) => { 4 | const Element = `h${level}` 5 | 6 | return ( 7 | 8 |

    dynamic element

    9 | 10 |
    11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/dynamic-element.js: -------------------------------------------------------------------------------- 1 | export default ({ level = 1 }) => { 2 | const Element = `h${level}` 3 | 4 | return ( 5 | 6 |

    dynamic element

    7 | 12 |
    13 | ) 14 | } 15 | 16 | export const TestLowerCase = ({ level = 1 }) => { 17 | const element = `h${level}` 18 | 19 | return ( 20 | 21 |

    dynamic element

    22 | 27 |
    28 | ) 29 | } 30 | 31 | const Element2 = 'div' 32 | export const Test2 = () => { 33 | return ( 34 | 35 |

    dynamic element

    36 | 41 |
    42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /test/fixtures/dynamic-this-value-in-arrow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | export default class Index extends Component { 4 | static getInitialProps() { 5 | return { color: 'aquamarine' } 6 | } 7 | 8 | render() { 9 | return ( 10 |
    11 | {[1, 2].map(idx => ( 12 |
    13 | {[3, 4].map(idx2 => ( 14 |
    {this.props.color}
    15 | ))} 16 |
    17 | ))} 18 | {[1, 2].map(idx => ( 19 |
    20 |
    21 | {this.props.color} 22 |
    23 | 24 |
    25 |
    {this.props.color} hello there
    26 |
    27 |
    28 |
    29 |
    30 |
    31 | ))} 32 | 37 |
    38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/fixtures/expressions.js: -------------------------------------------------------------------------------- 1 | const darken = c => c 2 | const color = 'red' 3 | const otherColor = 'green' 4 | const mediumScreen = '680px' 5 | const animationDuration = '200ms' 6 | const animationName = 'my-cool-animation' 7 | const obj = { display: 'block' } 8 | 9 | export default ({ display }) => ( 10 |
    11 |

    test

    12 | 18 | 19 | 24 | 29 | 34 | 39 | 44 | 49 | 62 | 67 | 75 | 76 | 81 | 87 |
    88 | ) 89 | -------------------------------------------------------------------------------- /test/fixtures/external-stylesheet-global.js: -------------------------------------------------------------------------------- 1 | import styles, { foo as styles3 } from './styles' 2 | 3 | const styles2 = require('./styles2') 4 | 5 | export default () => ( 6 |
    7 |

    test

    8 |
    woot
    9 |

    woot

    10 | 13 | 16 | 19 |
    20 | ) 21 | -------------------------------------------------------------------------------- /test/fixtures/external-stylesheet-multi-line.js: -------------------------------------------------------------------------------- 1 | import styles from './styles' 2 | 3 | export default () => ( 4 |
    5 |

    test

    6 | 7 |
    8 | ) 9 | -------------------------------------------------------------------------------- /test/fixtures/external-stylesheet.js: -------------------------------------------------------------------------------- 1 | import styles from './styles' 2 | const styles2 = require('./styles2') 3 | import { foo as styles3 } from './styles' 4 | 5 | export default () => ( 6 |
    7 |

    test

    8 |

    woot

    9 | 12 | 13 |
    woot
    14 | 22 | 23 |
    24 | ) 25 | 26 | export const Test = () => ( 27 |
    28 |

    test

    29 |

    woot

    30 | 31 |
    woot
    32 | 40 |
    41 | ) 42 | -------------------------------------------------------------------------------- /test/fixtures/fragment.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default () => ( 4 | <> 5 |

    Testing!!!

    6 |

    Bar

    7 | <> 8 | 9 | 10 |

    hello

    11 | <> 12 |

    foo

    13 |

    bar

    14 | 15 |

    world

    16 |
    17 | 18 | 30 | 31 | ) 32 | 33 | function Component1() { 34 | return ( 35 | <> 36 |
    test
    37 | 38 | ) 39 | } 40 | 41 | function Component2() { 42 | return ( 43 |
    44 | 49 |
    50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /test/fixtures/global.js: -------------------------------------------------------------------------------- 1 | const Test = () => ( 2 |
    3 | 22 |
    23 | ) 24 | 25 | const Test2 = () => ( 26 | 29 | ) 30 | -------------------------------------------------------------------------------- /test/fixtures/ignore.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 |
    3 |

    test

    4 | 9 |
    10 | ) 11 | -------------------------------------------------------------------------------- /test/fixtures/macro.js: -------------------------------------------------------------------------------- 1 | import css, { resolve } from '../../test/helpers/babel-test.macro' 2 | 3 | const { className, styles } = resolve` 4 | div { color: red } 5 | ` 6 | 7 | const dynamicStyles = props => resolve` 8 | div { color: ${props.color} } 9 | ` 10 | 11 | const test = css.resolve` 12 | div { color: red } 13 | ` 14 | 15 | const dynamicStyles2 = props => css.resolve` 16 | div { color: ${props.color} } 17 | ` 18 | 19 | const ExampleComponent = props => { 20 | const { className, styles } = dynamicStyles(props) 21 | 22 | return ( 23 |
    24 | howdy 25 | {styles} 26 |
    27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /test/fixtures/mixed-global-scoped.js: -------------------------------------------------------------------------------- 1 | const Test = () => ( 2 | 5 | ) 6 | 7 | export default () => ( 8 |
    9 |

    test

    10 | 15 | 16 |
    17 | ) 18 | -------------------------------------------------------------------------------- /test/fixtures/multiple-jsx.js: -------------------------------------------------------------------------------- 1 | const attrs = { 2 | id: 'test' 3 | } 4 | 5 | const Test1 = () => ( 6 |
    7 | 8 | test 9 | 10 | 11 | 16 |
    17 | ) 18 | 19 | const Test2 = () => test 20 | 21 | const Test3 = () => ( 22 |
    23 | test 24 | 29 |
    30 | ) 31 | 32 | export default class { 33 | render() { 34 | return ( 35 |
    36 |

    test

    37 | 42 |
    43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/fixtures/nested-style-tags.js: -------------------------------------------------------------------------------- 1 | import styles from './styles' 2 | 3 | export default () => ( 4 |
    5 | 6 | test 7 | 14 | 15 | 20 |
    21 | ) 22 | 23 | export const Test = () => ( 24 |
    25 | 26 | test 27 | 34 | 35 | 42 | {/* this should not be transpiled */} 43 | 44 | 45 | 50 | 51 |
    52 | ) 53 | -------------------------------------------------------------------------------- /test/fixtures/non-styled-jsx-style.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 |
    3 |

    woot

    4 | 6 |
    7 | ) 8 | -------------------------------------------------------------------------------- /test/fixtures/not-styled-jsx-tagged-templates.js: -------------------------------------------------------------------------------- 1 | import css from 'hell' 2 | 3 | const color = 'red' 4 | 5 | const bar = css` 6 | div { 7 | font-size: 3em; 8 | } 9 | ` 10 | export const uh = bar 11 | 12 | export const foo = css` 13 | div { 14 | color: ${color}; 15 | } 16 | ` 17 | 18 | export default css` 19 | div { 20 | font-size: 3em; 21 | } 22 | p { 23 | color: ${color}; 24 | } 25 | ` 26 | 27 | const Title = styled.h1` 28 | color: red; 29 | font-size: 50px; 30 | ` 31 | 32 | const AnotherTitle = Title.extend` 33 | color: blue; 34 | ` 35 | 36 | export const Component = () => My page 37 | -------------------------------------------------------------------------------- /test/fixtures/plugins/another-plugin.js: -------------------------------------------------------------------------------- 1 | export default css => css.replace(/div/g, 'span') 2 | -------------------------------------------------------------------------------- /test/fixtures/plugins/invalid-plugin.js: -------------------------------------------------------------------------------- 1 | export default 'invalid' 2 | -------------------------------------------------------------------------------- /test/fixtures/plugins/multiple-options.js: -------------------------------------------------------------------------------- 1 | export default (css, settings) => { 2 | let { babel, ...s } = settings 3 | let { filename, ...b } = babel 4 | s.babel = b 5 | if (!filename) { 6 | throw new Error('filename should be defined') 7 | } 8 | return `.test { content: "${JSON.stringify(s)}"; }` 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/plugins/options.js: -------------------------------------------------------------------------------- 1 | export default (css, settings) => settings.test 2 | -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin.js: -------------------------------------------------------------------------------- 1 | export default css => `TEST (${css}) EOTEST` 2 | -------------------------------------------------------------------------------- /test/fixtures/simple-fragment.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 | <> 3 |

    This should throw in babel 6

    4 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /test/fixtures/source-maps.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 |
    3 |

    test

    4 |

    woot

    5 | 6 |
    7 | ) 8 | -------------------------------------------------------------------------------- /test/fixtures/stateless.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 |
    3 |

    test

    4 |

    woot

    5 |

    woot

    6 | 7 |
    8 | ) 9 | -------------------------------------------------------------------------------- /test/fixtures/styles-external-invalid.js: -------------------------------------------------------------------------------- 1 | import css from 'styled-jsx/css' 2 | 3 | const color = 'red' 4 | 5 | export const foo = css` 6 | div { 7 | color: ${color}; 8 | } 9 | ` 10 | 11 | const props = { color: 'red ' } 12 | 13 | export default css` 14 | div { 15 | font-size: 3em; 16 | color: ${props.color}; 17 | } 18 | p { 19 | color: ${this.props.color}; 20 | } 21 | ` 22 | -------------------------------------------------------------------------------- /test/fixtures/styles-external-invalid2.js: -------------------------------------------------------------------------------- 1 | import css from 'styled-jsx/css' 2 | 3 | const color = 'red' 4 | 5 | export const foo = css` 6 | div { 7 | color: ${color}; 8 | } 9 | ` 10 | 11 | export default css` 12 | div { 13 | font-size: 3em; 14 | } 15 | p { 16 | color: ${props.color}; 17 | } 18 | ` 19 | -------------------------------------------------------------------------------- /test/fixtures/styles.js: -------------------------------------------------------------------------------- 1 | import css, { resolve, global } from 'styled-jsx/css' 2 | import colors, { size } from './constants' 3 | const color = 'red' 4 | 5 | const bar = css` 6 | div { 7 | font-size: 3em; 8 | } 9 | ` 10 | 11 | const baz = css.global` 12 | div { 13 | font-size: 3em; 14 | } 15 | ` 16 | 17 | const a = global` 18 | div { 19 | font-size: ${size}em; 20 | } 21 | ` 22 | 23 | export const uh = bar 24 | 25 | export const foo = css` 26 | div { 27 | color: ${color}; 28 | } 29 | ` 30 | 31 | css.resolve` 32 | div { 33 | color: ${colors.green.light}; 34 | } 35 | a { color: red } 36 | ` 37 | 38 | const b = resolve` 39 | div { 40 | color: ${colors.green.light}; 41 | } 42 | a { color: red } 43 | ` 44 | 45 | const dynamic = colors => { 46 | const b = resolve` 47 | div { 48 | color: ${colors.green.light}; 49 | } 50 | a { color: red } 51 | ` 52 | } 53 | 54 | export default css.resolve` 55 | div { 56 | font-size: 3em; 57 | } 58 | p { 59 | color: ${color}; 60 | } 61 | ` 62 | -------------------------------------------------------------------------------- /test/fixtures/styles2.js: -------------------------------------------------------------------------------- 1 | import css from 'styled-jsx/css' 2 | 3 | module.exports = css` 4 | div { 5 | font-size: 3em; 6 | } 7 | ` 8 | -------------------------------------------------------------------------------- /test/fixtures/transform.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-image: linear-gradient( 3 | 0deg, 4 | rgba(255, 255, 255, 0.8), 5 | rgba(255, 255, 255, 0.8) 6 | ), 7 | url(/static/background.svg); 8 | } 9 | 10 | :global(p) { 11 | color: blue; 12 | } 13 | 14 | :global(p) { 15 | color: blue; 16 | } 17 | 18 | :global(p), 19 | a { 20 | color: blue; 21 | } 22 | 23 | :global(.foo + a) { 24 | color: red; 25 | } 26 | 27 | :global(body) { 28 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, 29 | sans-serif; 30 | } 31 | 32 | p { 33 | color: red; 34 | } 35 | 36 | p { 37 | color: red; 38 | } 39 | 40 | * { 41 | color: blue; 42 | } 43 | 44 | [href='woot'] { 45 | color: red; 46 | } 47 | 48 | p a span { 49 | color: red; 50 | } 51 | 52 | p :global(span) { 53 | background: blue; 54 | } 55 | 56 | p a[title="'w ' ' t'"] { 57 | margin: auto; 58 | } 59 | 60 | p :global(span:not(.test)) { 61 | color: green; 62 | } 63 | 64 | p, 65 | h1 { 66 | color: blue; 67 | animation: hahaha 3s ease forwards infinite; 68 | animation-name: hahaha; 69 | animation-delay: 100ms; 70 | } 71 | 72 | p { 73 | animation: hahaha 1s, hehehe 2s; 74 | } 75 | 76 | p:hover { 77 | color: red; 78 | } 79 | 80 | p::before { 81 | color: red; 82 | } 83 | 84 | :hover { 85 | color: red; 86 | } 87 | 88 | ::before { 89 | color: red; 90 | } 91 | 92 | :hover p { 93 | color: red; 94 | } 95 | 96 | p + a { 97 | color: red; 98 | } 99 | 100 | p ~ a { 101 | color: red; 102 | } 103 | 104 | p > a { 105 | color: red; 106 | } 107 | 108 | @keyframes hahaha { 109 | from { 110 | top: 0; 111 | } 112 | to { 113 | top: 100; 114 | } 115 | } 116 | 117 | @keyframes hehehe { 118 | from { 119 | left: 0; 120 | } 121 | to { 122 | left: 100; 123 | } 124 | } 125 | 126 | @media (min-width: 500px) { 127 | .test { 128 | color: red; 129 | } 130 | } 131 | 132 | .test { 133 | /* test, test */ 134 | display: block; 135 | /* 136 | 137 | test 138 | */ 139 | } 140 | 141 | .inline-flex { 142 | display: inline-flex; 143 | } 144 | 145 | .flex { 146 | display: flex; 147 | } 148 | 149 | .test { 150 | box-shadow: 0 0 10px black, inset 0 0 5px black; 151 | } 152 | 153 | .test[title=','] { 154 | display: inline-block; 155 | } 156 | 157 | .test.is-status .test { 158 | color: red; 159 | } 160 | 161 | .a-selector:hover, 162 | .a-selector:focus { 163 | outline: none; 164 | } 165 | 166 | @media (min-width: 1px) and (max-width: 768px) { 167 | [class*='grid__col--'] { 168 | margin-top: 12px; 169 | margin-bottom: 12px; 170 | } 171 | } 172 | 173 | @media (max-width: 64em) { 174 | .test { 175 | margin-bottom: 1em; 176 | } 177 | @supports (-moz-appearance: none) and (display: contents) { 178 | .test { 179 | margin-bottom: 2rem; 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /test/fixtures/whitespace.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 |
    3 |

    test

    4 |

    woot

    5 |

    woot

    6 | 7 |
    8 | ) 9 | -------------------------------------------------------------------------------- /test/fixtures/with-plugins.js: -------------------------------------------------------------------------------- 1 | import styles from './styles' 2 | const color = 'red' 3 | 4 | export default () => ( 5 |
    6 |

    test

    7 | 12 | 13 |
    14 | ) 15 | -------------------------------------------------------------------------------- /test/helpers/babel-test.macro.js: -------------------------------------------------------------------------------- 1 | import { macro } from '../../src/babel' 2 | 3 | const m = macro() 4 | console.log('m', m) 5 | export default m 6 | -------------------------------------------------------------------------------- /test/helpers/with-mock.js: -------------------------------------------------------------------------------- 1 | export default function withMock(mockFn, testFn) { 2 | return t => { 3 | const cleanUp = mockFn(t) 4 | if (typeof cleanUp !== 'function') { 5 | throw new TypeError('mockFn should return a cleanup function') 6 | } 7 | 8 | testFn(t) 9 | cleanUp(t) 10 | } 11 | } 12 | 13 | export function withMockDocument(t) { 14 | const originalDocument = globalThis.document 15 | // We need to stub a document in order to simulate the meta tag 16 | globalThis.document = { 17 | querySelector(query) { 18 | t.is(query, 'meta[property="csp-nonce"]') 19 | return { 20 | getAttribute(attr) { 21 | t.is(attr, 'content') 22 | return 'test-nonce' 23 | } 24 | } 25 | } 26 | } 27 | 28 | return () => { 29 | globalThis.document = originalDocument 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | import test from 'ava' 3 | import React from 'react' 4 | import ReactDOM from 'react-dom/server' 5 | 6 | // Ours 7 | import plugin from '../src/babel' 8 | import JSXStyle from '../src/style' 9 | import { 10 | StyleRegistry, 11 | useStyleRegistry, 12 | createStyleRegistry 13 | } from '../src/stylesheet-registry' 14 | import _transform, { transformSource as _transformSource } from './_transform' 15 | 16 | const flushToHTML = (registry, options = {}) => { 17 | const cssRules = registry.cssRules() 18 | registry.flush() 19 | return cssRules.reduce((html, args) => { 20 | const id = args[0] 21 | const css = args[1] 22 | html += `` 25 | return html 26 | }, '') 27 | } 28 | 29 | function mapCssRulesToReact(cssRules, options = {}) { 30 | return cssRules.map(args => { 31 | const id = args[0] 32 | const css = args[1] 33 | return React.createElement('style', { 34 | id: `__${id}`, 35 | // Avoid warnings upon render with a key 36 | key: `__${id}`, 37 | nonce: options.nonce ? options.nonce : undefined, 38 | dangerouslySetInnerHTML: { 39 | __html: css 40 | } 41 | }) 42 | }) 43 | } 44 | 45 | function flushToReact(registry, options = {}) { 46 | const cssRules = registry.cssRules() 47 | registry.flush() 48 | return mapCssRulesToReact(cssRules, options) 49 | } 50 | 51 | const transform = (file, opts = {}) => 52 | _transform(file, { 53 | plugins: [plugin], 54 | ...opts 55 | }) 56 | 57 | const transformSource = (src, opts = {}) => 58 | _transformSource(src.trim(), { 59 | plugins: [[plugin, opts]], 60 | ...opts 61 | }) 62 | 63 | test('handles dynamic `this` value inside of arrow function', async t => { 64 | const { code } = await transform( 65 | './fixtures/dynamic-this-value-in-arrow.js', 66 | { 67 | plugins: ['@babel/plugin-transform-arrow-functions', plugin] 68 | } 69 | ) 70 | t.snapshot(code) 71 | }) 72 | 73 | test('works with stateless', async t => { 74 | const { code } = await transform('./fixtures/stateless.js') 75 | t.snapshot(code) 76 | }) 77 | 78 | test('works with fragment', async t => { 79 | const { code } = await transform('./fixtures/fragment.js') 80 | t.snapshot(code) 81 | }) 82 | 83 | test('ignores whitespace around expression container', async t => { 84 | const { code } = await transform('./fixtures/whitespace.js') 85 | t.snapshot(code) 86 | }) 87 | 88 | test('works with class', async t => { 89 | const { code } = await transform('./fixtures/class.js') 90 | t.snapshot(code) 91 | }) 92 | 93 | test('ignores when attribute is absent', async t => { 94 | const { code } = await transform('./fixtures/absent.js') 95 | t.snapshot(code) 96 | }) 97 | 98 | test('works with global styles', async t => { 99 | const { code } = await transform('./fixtures/global.js') 100 | t.snapshot(code) 101 | }) 102 | 103 | test('generates source maps', async t => { 104 | const { code } = await transform('./fixtures/source-maps.js', { 105 | plugins: [[plugin, { sourceMaps: true }]] 106 | }) 107 | t.snapshot(code) 108 | }) 109 | 110 | test('mixed global and scoped', async t => { 111 | const { code } = await transform('./fixtures/mixed-global-scoped.js') 112 | t.snapshot(code) 113 | }) 114 | 115 | test('works with multiple jsx blocks', async t => { 116 | const { code } = await transform('./fixtures/multiple-jsx.js') 117 | t.snapshot(code) 118 | }) 119 | 120 | test('should not add the data-jsx attribute to components instances', async t => { 121 | const { code } = await transform('./fixtures/component-attribute.js') 122 | t.snapshot(code) 123 | }) 124 | 125 | test('works with expressions in template literals', async t => { 126 | const { code } = await transform('./fixtures/expressions.js') 127 | t.snapshot(code) 128 | }) 129 | 130 | test('should have different jsx ids', async t => { 131 | const { code } = await transform('./fixtures/different-jsx-ids.js') 132 | t.snapshot(code) 133 | }) 134 | 135 | test('works with non styled-jsx styles', async t => { 136 | const { code } = await transform('./fixtures/non-styled-jsx-style.js') 137 | t.snapshot(code) 138 | }) 139 | 140 | test('works with css tagged template literals in the same file', async t => { 141 | const { code } = await transform('./fixtures/css-tag-same-file.js') 142 | t.snapshot(code) 143 | }) 144 | 145 | test('works with dynamic element', async t => { 146 | const { code } = await transform('./fixtures/dynamic-element.js') 147 | t.snapshot(code) 148 | }) 149 | 150 | test('works with dynamic element in class', async t => { 151 | const { code } = await transform('./fixtures/dynamic-element-class.js') 152 | t.snapshot(code) 153 | }) 154 | 155 | test('works with existing identifier for _JSXStyle', async t => { 156 | const { code } = await transform('./fixtures/conflicts.js') 157 | t.snapshot(code) 158 | }) 159 | 160 | test('does not transpile nested style tags', async t => { 161 | const { message } = await t.throwsAsync(() => 162 | transform('./fixtures/nested-style-tags.js') 163 | ) 164 | t.regex(message, /detected nested style tag/i) 165 | }) 166 | 167 | test('works with exported jsx-style (CommonJS modules)', async t => { 168 | const { code } = await transformSource( 169 | 'module.exports = () =>

    ', 170 | { 171 | plugins: [plugin, '@babel/plugin-transform-modules-commonjs'] 172 | } 173 | ) 174 | t.snapshot(code) 175 | }) 176 | 177 | test('works with exported non-jsx style (CommonJS modules)', async t => { 178 | const { code } = await transformSource( 179 | 'module.exports = () =>

    ', 180 | { 181 | plugins: [plugin, '@babel/plugin-transform-modules-commonjs'] 182 | } 183 | ) 184 | t.snapshot(code) 185 | }) 186 | 187 | test('sever rendering with hook api', t => { 188 | const registry = createStyleRegistry() 189 | function Head() { 190 | const registry = useStyleRegistry() 191 | const styles = registry.styles() 192 | registry.flush() 193 | // should be empty and `push` won't effect styles 194 | const stylesAfterFlushed = registry.styles() 195 | styles.push(...stylesAfterFlushed) 196 | return React.createElement('head', null, styles) 197 | } 198 | 199 | function App() { 200 | const color = 'green' 201 | return React.createElement( 202 | 'div', 203 | null, 204 | React.createElement(Head), 205 | React.createElement(JSXStyle, { id: 2 }, 'div { color: blue }'), 206 | React.createElement(JSXStyle, { id: 3 }, `div { color: ${color} }`) 207 | ) 208 | } 209 | 210 | // Expected DOM string 211 | const styles = 212 | '' + 213 | '' 214 | 215 | const expected = `${styles}` 216 | 217 | const createContextualApp = type => 218 | React.createElement(StyleRegistry, { registry }, React.createElement(type)) 219 | 220 | // Render using react 221 | ReactDOM.renderToString(createContextualApp(App)) 222 | const html = ReactDOM.renderToStaticMarkup(createContextualApp(Head)) 223 | t.is(html, expected) 224 | }) 225 | 226 | test('server rendering', t => { 227 | function App() { 228 | const color = 'green' 229 | return React.createElement( 230 | 'div', 231 | null, 232 | React.createElement( 233 | JSXStyle, 234 | { 235 | id: 1 236 | }, 237 | 'p { color: red }' 238 | ), 239 | React.createElement( 240 | JSXStyle, 241 | { 242 | id: 2 243 | }, 244 | 'div { color: blue }' 245 | ), 246 | React.createElement( 247 | JSXStyle, 248 | { 249 | id: 3 250 | }, 251 | `div { color: ${color} }` 252 | ) 253 | ) 254 | } 255 | 256 | // Expected CSS 257 | const expected = 258 | '' + 259 | '' + 260 | '' 261 | 262 | const registry = createStyleRegistry() 263 | const createApp = () => 264 | React.createElement(StyleRegistry, { registry }, React.createElement(App)) 265 | 266 | // Render using react 267 | ReactDOM.renderToString(createApp()) 268 | const html = ReactDOM.renderToStaticMarkup( 269 | React.createElement('head', null, flushToReact(registry)) 270 | ) 271 | 272 | t.is(html, `${expected}`) 273 | 274 | // Assert that memory is empty 275 | t.is(0, registry.cssRules().length) 276 | t.is('', flushToHTML(registry)) 277 | 278 | // Render to html again 279 | ReactDOM.renderToString(createApp()) 280 | t.is(expected, flushToHTML(registry)) 281 | 282 | // Assert that memory is empty 283 | t.is(0, flushToReact(registry).length) 284 | t.is('', flushToHTML(registry)) 285 | }) 286 | 287 | test('server rendering with nonce', t => { 288 | function App() { 289 | const color = 'green' 290 | return React.createElement( 291 | 'div', 292 | null, 293 | React.createElement( 294 | JSXStyle, 295 | { 296 | id: 1 297 | }, 298 | 'p { color: red }' 299 | ), 300 | React.createElement( 301 | JSXStyle, 302 | { 303 | id: 2 304 | }, 305 | 'div { color: blue }' 306 | ), 307 | React.createElement( 308 | JSXStyle, 309 | { 310 | id: 3 311 | }, 312 | `div { color: ${color} }` 313 | ) 314 | ) 315 | } 316 | 317 | const registry = createStyleRegistry() 318 | const createApp = () => 319 | React.createElement(StyleRegistry, { registry }, React.createElement(App)) 320 | 321 | // Expected CSS 322 | const expected = 323 | '' + 324 | '' + 325 | '' 326 | 327 | // Render using react 328 | ReactDOM.renderToString(createApp()) 329 | const html = ReactDOM.renderToStaticMarkup( 330 | React.createElement( 331 | 'head', 332 | null, 333 | flushToReact(registry, { nonce: 'test-nonce' }) 334 | ) 335 | ) 336 | 337 | t.is(html, `${expected}`) 338 | 339 | // Assert that memory is empty 340 | t.is(0, flushToReact(registry, { nonce: 'test-nonce' }).length) 341 | t.is('', flushToHTML(registry, { nonce: 'test-nonce' })) 342 | 343 | // Render to html again 344 | ReactDOM.renderToString(createApp()) 345 | t.is(expected, flushToHTML(registry, { nonce: 'test-nonce' })) 346 | 347 | // Assert that memory is empty 348 | t.is(0, flushToReact(registry, { nonce: 'test-nonce' }).length) 349 | t.is('', flushToHTML(registry, { nonce: 'test-nonce' })) 350 | }) 351 | 352 | test('optimized styles do not contain new lines', t => { 353 | function App() { 354 | return React.createElement( 355 | 'div', 356 | null, 357 | React.createElement( 358 | JSXStyle, 359 | { 360 | id: 1 361 | }, 362 | ['p { color: red }', '.foo { color: hotpink }'] 363 | ) 364 | ) 365 | } 366 | 367 | const registry = createStyleRegistry() 368 | const createApp = () => 369 | React.createElement(StyleRegistry, { registry }, React.createElement(App)) 370 | 371 | ReactDOM.renderToString(createApp()) 372 | const html = ReactDOM.renderToStaticMarkup( 373 | React.createElement('head', null, flushToReact(registry)) 374 | ) 375 | const expected = 376 | '' 377 | 378 | t.is(html, `${expected}`) 379 | }) 380 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import {} from 'styled-jsx' 2 | -------------------------------------------------------------------------------- /test/macro.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | import test from 'ava' 3 | 4 | // Ours 5 | import macros from 'babel-plugin-macros' 6 | import jsx from '@babel/plugin-syntax-jsx' 7 | import _transform, { transformSource as _transformSource } from './_transform' 8 | 9 | const transform = (file, opts = {}) => 10 | _transform(file, { 11 | plugins: [macros, jsx], 12 | ...opts 13 | }) 14 | 15 | const transformSource = (src, opts = {}) => 16 | _transformSource(src.trim(), { 17 | filename: './index.js', 18 | plugins: [macros, jsx], 19 | ...opts 20 | }) 21 | 22 | test('transpiles correctly', async t => { 23 | const { code } = await transform('./fixtures/macro.js') 24 | t.snapshot(code) 25 | }) 26 | 27 | test('throws when using the default export directly', async t => { 28 | const { message } = await t.throwsAsync(() => 29 | transformSource(` 30 | import css from './test/helpers/babel-test.macro' 31 | 32 | css\`div { color: red }\` 33 | `) 34 | ) 35 | 36 | t.regex(message, /can't use default import directly/i) 37 | }) 38 | 39 | test('throws when using the default export directly and it is not called css', async t => { 40 | const { message } = await t.throwsAsync(() => 41 | transformSource(` 42 | import foo from './test/helpers/babel-test.macro' 43 | 44 | foo\`div { color: red }\` 45 | `) 46 | ) 47 | 48 | t.regex(message, /can't use default import directly/i) 49 | }) 50 | 51 | test('throws when using the default export directly and it is not called resolve', async t => { 52 | const { message } = await t.throwsAsync(() => 53 | transformSource(` 54 | import resolve from './test/helpers/babel-test.macro' 55 | 56 | resolve\`div { color: red }\` 57 | `) 58 | ) 59 | 60 | t.regex(message, /can't use default import directly/i) 61 | }) 62 | 63 | test('throws when using an invalid method from the default export', async t => { 64 | const { message } = await t.throwsAsync(() => 65 | transformSource(` 66 | import css from './test/helpers/babel-test.macro' 67 | 68 | css.foo\`div { color: red }\` 69 | `) 70 | ) 71 | 72 | t.regex(message, /using an invalid tag/i) 73 | }) 74 | 75 | test('throws when using a named import different than resolve', async t => { 76 | const { message } = await t.throwsAsync(() => 77 | transformSource(` 78 | import { foo } from './test/helpers/babel-test.macro' 79 | 80 | foo\`div { color: red }\` 81 | `) 82 | ) 83 | 84 | t.regex(message, /imported an invalid named import/i) 85 | }) 86 | 87 | test('throws when using a named import as a member expression', async t => { 88 | const { message } = await t.throwsAsync(() => 89 | transformSource(` 90 | import { resolve } from './test/helpers/babel-test.macro' 91 | 92 | resolve.foo\`div { color: red }\` 93 | `) 94 | ) 95 | 96 | t.regex(message, /can't use named import/i) 97 | }) 98 | 99 | test('can alias the named import', async t => { 100 | const { code } = await transformSource(` 101 | import { resolve as foo } from './test/helpers/babel-test.macro' 102 | 103 | foo\`div { color: red }\` 104 | `) 105 | t.snapshot(code) 106 | }) 107 | 108 | test('injects JSXStyle for nested scope', async t => { 109 | const { code } = await transformSource(` 110 | import { resolve } from './test/helpers/babel-test.macro' 111 | 112 | function test() { 113 | resolve\`div { color: red }\` 114 | } 115 | `) 116 | t.snapshot(code) 117 | }) 118 | -------------------------------------------------------------------------------- /test/plugins.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | import test from 'ava' 3 | 4 | // Ours 5 | import babelPlugin from '../src/babel' 6 | import babelTestPlugin from '../src/babel-test' 7 | import { combinePlugins } from '../src/_utils' 8 | import _transform from './_transform' 9 | import testPlugin1 from './fixtures/plugins/plugin' 10 | import testPlugin2 from './fixtures/plugins/another-plugin' 11 | 12 | const transform = (file, opts = {}) => 13 | _transform(file, { 14 | plugins: [ 15 | [ 16 | babelPlugin, 17 | { plugins: [require.resolve('./fixtures/plugins/another-plugin')] } 18 | ] 19 | ], 20 | ...opts 21 | }) 22 | 23 | test('combinePlugins returns an identity function when plugins is undefined', t => { 24 | const test = 'test' 25 | const plugins = combinePlugins() 26 | t.is(plugins(test), test) 27 | }) 28 | 29 | test('combinePlugins throws if plugins is not an array', t => { 30 | t.throws(() => { 31 | combinePlugins(2) 32 | }) 33 | }) 34 | 35 | test('combinePlugins throws if plugins is not an array of strings', t => { 36 | t.throws(() => { 37 | combinePlugins(['test', 2]) 38 | }) 39 | }) 40 | 41 | test('combinePlugins throws if loaded plugins are not functions', t => { 42 | t.throws(() => { 43 | combinePlugins([ 44 | require.resolve('./fixtures/plugins/plugin'), 45 | require.resolve('./fixtures/plugins/invalid-plugin') 46 | ]) 47 | }) 48 | }) 49 | 50 | test('combinePlugins works with a single plugin', t => { 51 | const plugins = combinePlugins([require.resolve('./fixtures/plugins/plugin')]) 52 | 53 | t.is(testPlugin1('test'), plugins('test')) 54 | }) 55 | 56 | test('combinePlugins works with options', t => { 57 | const expectedOption = 'my-test' 58 | const plugins = combinePlugins([ 59 | [ 60 | require.resolve('./fixtures/plugins/options'), 61 | { 62 | test: expectedOption 63 | } 64 | ] 65 | ]) 66 | t.is(plugins(''), expectedOption) 67 | }) 68 | 69 | test('combinePlugins applies plugins left to right', t => { 70 | const plugins = combinePlugins([ 71 | require.resolve('./fixtures/plugins/plugin'), 72 | require.resolve('./fixtures/plugins/another-plugin') 73 | ]) 74 | 75 | t.is(testPlugin2(testPlugin1('test')), plugins('test')) 76 | }) 77 | 78 | test('applies plugins', async t => { 79 | const { code } = await transform('./fixtures/with-plugins.js') 80 | t.snapshot(code) 81 | }) 82 | 83 | test('babel-test plugin strips jsx attribute', async t => { 84 | const { code } = await transform('./fixtures/with-plugins.js', { 85 | plugins: [babelTestPlugin] 86 | }) 87 | 88 | t.snapshot(code) 89 | }) 90 | 91 | test('passes options to plugins', async t => { 92 | const { code } = await transform('./fixtures/with-plugins.js', { 93 | plugins: [ 94 | [ 95 | babelPlugin, 96 | { 97 | plugins: [ 98 | [require.resolve('./fixtures/plugins/options'), { foo: true }], 99 | require.resolve('./fixtures/plugins/multiple-options'), 100 | [ 101 | require.resolve('./fixtures/plugins/multiple-options'), 102 | { foo: false } 103 | ] 104 | ], 105 | vendorPrefixes: false 106 | } 107 | ] 108 | ] 109 | }) 110 | t.snapshot(code) 111 | }) 112 | 113 | test('combinePlugins throws if passing an option called `babel`', t => { 114 | t.throws(() => { 115 | combinePlugins([['test', { babel: true }]]) 116 | }) 117 | }) 118 | 119 | test('combinePlugins memoizes calls', t => { 120 | const v1 = combinePlugins([require.resolve('./fixtures/plugins/plugin')]) 121 | const v2 = combinePlugins([require.resolve('./fixtures/plugins/plugin')]) 122 | 123 | t.is(v1('test div'), v2('test div')) 124 | 125 | const v3 = combinePlugins([ 126 | require.resolve('./fixtures/plugins/plugin'), 127 | require.resolve('./fixtures/plugins/another-plugin') 128 | ]) 129 | 130 | t.not(v2('test div'), v3('test div')) 131 | }) 132 | -------------------------------------------------------------------------------- /test/snapshots/attribute.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/attribute.js` 2 | 3 | The actual snapshot is saved in `attribute.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## rewrites className 8 | 9 | > Snapshot 1 10 | 11 | `import _JSXStyle from "styled-jsx/style";␊ 12 | export default (() => {␊ 13 | const Element = 'div';␊ 14 | return
    ␊ 15 |
    ␊ 16 |
    ␊ 17 |
    ␊ 18 |
    ␊ 19 |
    ␊ 20 |
    ␊ 21 |
    ␊ 22 |
    ␊ 23 |
    ␊ 24 |
    ␊ 25 |
    ␊ 26 |
    ␊ 27 |
    ␊ 28 |
    ␊ 29 |
    ␊ 30 |
    ␊ 31 |
    ␊ 32 |
    ␊ 33 |
    ␊ 34 |
    ␊ 35 |
    ␊ 36 |
    ␊ 37 |
    ␊ 38 |
    ␊ 39 |
    ␊ 40 |
    ␊ 41 |
    ␊ 42 |
    ␊ 43 |
    ␊ 44 |
    ␊ 45 |
    ␊ 46 |
    ␊ 47 |
    ␊ 48 |
    ␊ 49 |
    ␊ 52 |
    ␊ 53 |
    ␊ 56 |
    ␊ 57 |
    ␊ 58 |
    ␊ 61 |
    ␊ 62 |
    ␊ 63 |
    ␊ 64 |
    ␊ 65 |
    ␊ 66 | ␊ 67 | ␊ 68 | ␊ 69 | <_JSXStyle id={"2886504620"}>{"div.jsx-2886504620{color:red;}"}␊ 70 |
    ;␊ 71 | });` 72 | 73 | ## generate attribute for mixed modes (global, static, dynamic) 74 | 75 | > Snapshot 1 76 | 77 | `import _JSXStyle from "styled-jsx/style";␊ 78 | import styles from './styles';␊ 79 | ␊ 80 | const styles2 = require('./styles2'); // external only␊ 81 | ␊ 82 | ␊ 83 | export const Test1 = () =>
    ␊ 84 |

    external only

    ␊ 85 | <_JSXStyle id={styles.__hash}>{styles}␊ 86 | <_JSXStyle id={styles2.__hash}>{styles2}␊ 87 |
    ; // external and static␊ 88 | ␊ 89 | export const Test2 = () =>
    ␊ 90 |

    external and static

    ␊ 91 | <_JSXStyle id={"2982525546"}>{"p.jsx-2982525546{color:red;}"}␊ 92 | <_JSXStyle id={styles.__hash}>{styles}␊ 93 |
    ; // external and dynamic␊ 94 | ␊ 95 | export const Test3 = ({␊ 96 | color␊ 97 | }) =>
    ␊ 98 |

    external and dynamic

    ␊ 99 | <_JSXStyle id={"1947484460"} dynamic={[color]}>{\`p.__jsx-style-dynamic-selector{color:${color};}\`}␊ 100 | <_JSXStyle id={styles.__hash}>{styles}␊ 101 |
    ; // external, static and dynamic␊ 102 | ␊ 103 | export const Test4 = ({␊ 104 | color␊ 105 | }) =>
    ␊ 106 |

    external, static and dynamic

    ␊ 107 | <_JSXStyle id={"3190985107"}>{"p.jsx-3190985107{display:inline-block;}"}␊ 108 | <_JSXStyle id={"1336444426"} dynamic={[color]}>{\`p.__jsx-style-dynamic-selector{color:${color};}\`}␊ 109 | <_JSXStyle id={styles.__hash}>{styles}␊ 110 |
    ; // static only␊ 111 | ␊ 112 | export const Test5 = () =>
    ␊ 113 |

    static only

    ␊ 114 | <_JSXStyle id={"3190985107"}>{"p.jsx-1372669040{display:inline-block;}"}␊ 115 | <_JSXStyle id={"2982525546"}>{"p.jsx-1372669040{color:red;}"}␊ 116 |
    ; // static and dynamic␊ 117 | ␊ 118 | export const Test6 = ({␊ 119 | color␊ 120 | }) =>
    ␊ 121 |

    static and dynamic

    ␊ 122 | <_JSXStyle id={"3190985107"}>{"p.jsx-3190985107{display:inline-block;}"}␊ 123 | <_JSXStyle id={"1336444426"} dynamic={[color]}>{\`p.__jsx-style-dynamic-selector{color:${color};}\`}␊ 124 |
    ; // dynamic only␊ 125 | ␊ 126 | export const Test7 = ({␊ 127 | color␊ 128 | }) =>
    ␊ 129 |

    dynamic only

    ␊ 130 | <_JSXStyle id={"1947484460"} dynamic={[color]}>{\`p.__jsx-style-dynamic-selector{color:${color};}\`}␊ 131 |
    ; // dynamic with scoped compound variable␊ 132 | ␊ 133 | export const Test8 = ({␊ 134 | color␊ 135 | }) => {␊ 136 | if (color) {␊ 137 | const innerProps = {␊ 138 | color␊ 139 | };␊ 140 | return
    ␊ 141 |

    dynamic with scoped compound variable

    ␊ 142 | <_JSXStyle id={"1791723528"} dynamic={[innerProps.color]}>{\`p.__jsx-style-dynamic-selector{color:${innerProps.color};}\`}␊ 143 |
    ;␊ 144 | }␊ 145 | }; // dynamic with compound variable␊ 146 | ␊ 147 | export const Test9 = ({␊ 148 | color␊ 149 | }) => {␊ 150 | const innerProps = {␊ 151 | color␊ 152 | };␊ 153 | return
    ␊ 154 |

    dynamic with compound variable

    ␊ 155 | <_JSXStyle id={"248922593"} dynamic={[innerProps.color]}>{\`p.__jsx-style-dynamic-selector{color:${innerProps.color};}\`}␊ 156 |
    ;␊ 157 | };␊ 158 | const foo = 'red'; // dynamic with constant variable␊ 159 | ␊ 160 | export const Test10 = () =>
    ␊ 161 |

    dynamic with constant variable

    ␊ 162 | <_JSXStyle id={"461505126"}>{\`p.jsx-461505126{color:${foo};}\`}␊ 163 |
    ; // dynamic with complex scope␊ 164 | ␊ 165 | export const Test11 = ({␊ 166 | color␊ 167 | }) => {␊ 168 | const items = Array.from({␊ 169 | length: 5␊ 170 | }).map((item, i) =>
  • ␊ 171 | <_JSXStyle id={"2172653867"} dynamic={[color]}>{\`.item.__jsx-style-dynamic-selector{color:${color};}\`}␊ 172 | Item #{i + 1}␊ 173 |
  • );␊ 174 | return
      {items}
    ;␊ 175 | };` 176 | -------------------------------------------------------------------------------- /test/snapshots/attribute.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/styled-jsx/d7a59379134d73afaeb98177387cd62d54d746be/test/snapshots/attribute.js.snap -------------------------------------------------------------------------------- /test/snapshots/external.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/external.js` 2 | 3 | The actual snapshot is saved in `external.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## transpiles external stylesheets 8 | 9 | > Snapshot 1 10 | 11 | `import _JSXStyle from "styled-jsx/style";␊ 12 | import colors, { size } from './constants';␊ 13 | const color = 'red';␊ 14 | const bar = new String("div.jsx-2141779268{font-size:3em;}");␊ 15 | bar.__hash = "2141779268";␊ 16 | const baz = new String("div{font-size:3em;}");␊ 17 | baz.__hash = "2141779268";␊ 18 | const a = new String(\`div{font-size:${size}em;}\`);␊ 19 | a.__hash = "262929833";␊ 20 | export const uh = bar;␊ 21 | export const foo = new String(\`div.jsx-2433716433{color:${color};}\`);␊ 22 | foo.__hash = "2433716433";␊ 23 | ({␊ 24 | styles: <_JSXStyle id={"1329679275"}>{\`div.jsx-1329679275{color:${colors.green.light};}a.jsx-1329679275{color:red;}\`},␊ 25 | className: "jsx-1329679275"␊ 26 | });␊ 27 | const b = {␊ 28 | styles: <_JSXStyle id={"1329679275"}>{\`div.jsx-1329679275{color:${colors.green.light};}a.jsx-1329679275{color:red;}\`},␊ 29 | className: "jsx-1329679275"␊ 30 | };␊ 31 | ␊ 32 | const dynamic = colors => {␊ 33 | const b = {␊ 34 | styles: <_JSXStyle id={"3325296745"} dynamic={[colors.green.light]}>{\`div.__jsx-style-dynamic-selector{color:${colors.green.light};}a.__jsx-style-dynamic-selector{color:red;}\`},␊ 35 | className: _JSXStyle.dynamic([["3325296745", [colors.green.light]]])␊ 36 | };␊ 37 | };␊ 38 | ␊ 39 | export default {␊ 40 | styles: <_JSXStyle id={"3290112549"}>{\`div.jsx-3290112549{font-size:3em;}p.jsx-3290112549{color:${color};}\`},␊ 41 | className: "jsx-3290112549"␊ 42 | };` 43 | 44 | ## (optimized) transpiles external stylesheets 45 | 46 | > Snapshot 1 47 | 48 | `import _JSXStyle from "styled-jsx/style";␊ 49 | import colors, { size } from './constants';␊ 50 | const color = 'red';␊ 51 | const bar = ["div.jsx-2141779268{font-size:3em;}"];␊ 52 | bar.__hash = "2141779268";␊ 53 | const baz = ["div{font-size:3em;}"];␊ 54 | baz.__hash = "2141779268";␊ 55 | const a = [\`div{font-size:${size}em;}\`];␊ 56 | a.__hash = "262929833";␊ 57 | export const uh = bar;␊ 58 | export const foo = [\`div.jsx-2433716433{color:${color};}\`];␊ 59 | foo.__hash = "2433716433";␊ 60 | ({␊ 61 | styles: <_JSXStyle id={"1329679275"}>{[\`div.jsx-1329679275{color:${colors.green.light};}\`, "a.jsx-1329679275{color:red;}"]},␊ 62 | className: "jsx-1329679275"␊ 63 | });␊ 64 | const b = {␊ 65 | styles: <_JSXStyle id={"1329679275"}>{[\`div.jsx-1329679275{color:${colors.green.light};}\`, "a.jsx-1329679275{color:red;}"]},␊ 66 | className: "jsx-1329679275"␊ 67 | };␊ 68 | ␊ 69 | const dynamic = colors => {␊ 70 | const b = {␊ 71 | styles: <_JSXStyle id={"3325296745"} dynamic={[colors.green.light]}>{[\`div.__jsx-style-dynamic-selector{color:${colors.green.light};}\`, "a.__jsx-style-dynamic-selector{color:red;}"]},␊ 72 | className: _JSXStyle.dynamic([["3325296745", [colors.green.light]]])␊ 73 | };␊ 74 | };␊ 75 | ␊ 76 | export default {␊ 77 | styles: <_JSXStyle id={"3290112549"}>{["div.jsx-3290112549{font-size:3em;}", \`p.jsx-3290112549{color:${color};}\`]},␊ 78 | className: "jsx-3290112549"␊ 79 | };` 80 | 81 | ## transpiles external stylesheets (CommonJS modules) 82 | 83 | > Snapshot 1 84 | 85 | `const _defaultExport = new String("div.jsx-2141779268{font-size:3em;}");␊ 86 | ␊ 87 | _defaultExport.__hash = "2141779268";␊ 88 | module.exports = _defaultExport;` 89 | 90 | ## (optimized) transpiles external stylesheets (CommonJS modules) 91 | 92 | > Snapshot 1 93 | 94 | `const _defaultExport = ["div.jsx-2141779268{font-size:3em;}"];␊ 95 | _defaultExport.__hash = "2141779268";␊ 96 | module.exports = _defaultExport;` 97 | 98 | ## does not transpile non-styled-jsx tagged teplate literals 99 | 100 | > Snapshot 1 101 | 102 | `import css from 'hell';␊ 103 | const color = 'red';␊ 104 | const bar = css\`␊ 105 | div {␊ 106 | font-size: 3em;␊ 107 | }␊ 108 | \`;␊ 109 | export const uh = bar;␊ 110 | export const foo = css\`␊ 111 | div {␊ 112 | color: ${color};␊ 113 | }␊ 114 | \`;␊ 115 | export default css\`␊ 116 | div {␊ 117 | font-size: 3em;␊ 118 | }␊ 119 | p {␊ 120 | color: ${color};␊ 121 | }␊ 122 | \`;␊ 123 | const Title = styled.h1\`␊ 124 | color: red;␊ 125 | font-size: 50px;␊ 126 | \`;␊ 127 | const AnotherTitle = Title.extend\`␊ 128 | color: blue;␊ 129 | \`;␊ 130 | export const Component = () => My page;` 131 | 132 | ## use external stylesheets 133 | 134 | > Snapshot 1 135 | 136 | `import _JSXStyle from "styled-jsx/style";␊ 137 | import styles from './styles';␊ 138 | ␊ 139 | const styles2 = require('./styles2');␊ 140 | ␊ 141 | import { foo as styles3 } from './styles';␊ 142 | export default (() =>
    ␊ 143 |

    test

    ␊ 144 |

    woot

    ␊ 145 | <_JSXStyle id={styles2.__hash}>{styles2}␊ 146 | <_JSXStyle id={styles3.__hash}>{styles3}␊ 147 |
    woot
    ␊ 148 | <_JSXStyle id={"1646697228"}>{"p.jsx-1646697228{color:red;}div.jsx-1646697228{color:green;}"}␊ 149 | <_JSXStyle id={styles.__hash}>{styles}␊ 150 |
    );␊ 151 | export const Test = () =>
    ␊ 152 |

    test

    ␊ 153 |

    woot

    ␊ 154 | <_JSXStyle id={styles3.__hash}>{styles3}␊ 155 |
    woot
    ␊ 156 | <_JSXStyle id={"1646697228"}>{"p.jsx-1646697228{color:red;}div.jsx-1646697228{color:green;}"}␊ 157 |
    ;` 158 | 159 | ## use external stylesheets (multi-line) 160 | 161 | > Snapshot 1 162 | 163 | `import _JSXStyle from "styled-jsx/style";␊ 164 | import styles from './styles';␊ 165 | export default (() =>
    ␊ 166 |

    test

    ␊ 167 | <_JSXStyle id={styles.__hash}>{styles}␊ 168 |
    );` 169 | 170 | ## use external stylesheets (global only) 171 | 172 | > Snapshot 1 173 | 174 | `import _JSXStyle from "styled-jsx/style";␊ 175 | import styles, { foo as styles3 } from './styles';␊ 176 | ␊ 177 | const styles2 = require('./styles2');␊ 178 | ␊ 179 | export default (() =>
    ␊ 180 |

    test

    ␊ 181 |
    woot
    ␊ 182 |

    woot

    ␊ 183 | <_JSXStyle id={styles2.__hash}>{styles2}␊ 184 | <_JSXStyle id={styles3.__hash}>{styles3}␊ 185 | <_JSXStyle id={styles.__hash}>{styles}␊ 186 |
    );` 187 | 188 | ## injects JSXStyle for nested scope 189 | 190 | > Snapshot 1 191 | 192 | `import _JSXStyle from "styled-jsx/style";␊ 193 | ␊ 194 | function test() {␊ 195 | ({␊ 196 | styles: <_JSXStyle id={"2886504620"}>{"div.jsx-2886504620{color:red;}"},␊ 197 | className: "jsx-2886504620"␊ 198 | });␊ 199 | }` 200 | 201 | ## use external stylesheet and dynamic element 202 | 203 | > Snapshot 1 204 | 205 | `import _JSXStyle from "styled-jsx/style";␊ 206 | import styles from './styles2';␊ 207 | export default (({␊ 208 | level = 1␊ 209 | }) => {␊ 210 | const Element = \`h${level}\`;␊ 211 | return ␊ 212 |

    dynamic element

    ␊ 213 | <_JSXStyle id={styles.__hash}>{styles}␊ 214 |
    ;␊ 215 | });` 216 | 217 | ## Makes sure that style nodes are not re-used 218 | 219 | > Snapshot 1 220 | 221 | `"use strict";␊ 222 | ␊ 223 | var _style = _interopRequireDefault(require("styled-jsx/style"));␊ 224 | ␊ 225 | var _App = _interopRequireDefault(require("./App.styles"));␊ 226 | ␊ 227 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }␊ 228 | ␊ 229 | function Test() {␊ 230 | return
    ␊ 231 | <_style.default id={_App.default.__hash}>{_App.default}␊ 232 |
    ;␊ 233 | }` 234 | 235 | ## Make sure that it works with the new automatic transform 236 | 237 | > Snapshot 1 238 | 239 | `import _JSXStyle from "styled-jsx/style";␊ 240 | import { jsx as _jsx } from "react/jsx-runtime";␊ 241 | const A = {␊ 242 | styles: /*#__PURE__*/_jsx(_JSXStyle, {␊ 243 | id: "2723508961",␊ 244 | children: "div.jsx-2723508961{color:green;}"␊ 245 | }),␊ 246 | className: "jsx-2723508961"␊ 247 | };␊ 248 | export default function IndexPage() {␊ 249 | return JSON.stringify(A);␊ 250 | }` 251 | -------------------------------------------------------------------------------- /test/snapshots/external.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/styled-jsx/d7a59379134d73afaeb98177387cd62d54d746be/test/snapshots/external.js.snap -------------------------------------------------------------------------------- /test/snapshots/index.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/styled-jsx/d7a59379134d73afaeb98177387cd62d54d746be/test/snapshots/index.js.snap -------------------------------------------------------------------------------- /test/snapshots/macro.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/macro.js` 2 | 3 | The actual snapshot is saved in `macro.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## transpiles correctly 8 | 9 | > Snapshot 1 10 | 11 | `import _JSXStyle from "styled-jsx/style";␊ 12 | const {␊ 13 | className,␊ 14 | styles␊ 15 | } = {␊ 16 | styles: <_JSXStyle id={"2052294191"}>{"div.jsx-2052294191{color:red;}"},␊ 17 | className: "jsx-2052294191"␊ 18 | };␊ 19 | ␊ 20 | const dynamicStyles = props => ({␊ 21 | styles: <_JSXStyle id={"290194820"} dynamic={[props.color]}>{\`div.__jsx-style-dynamic-selector{color:${props.color};}\`},␊ 22 | className: _JSXStyle.dynamic([["290194820", [props.color]]])␊ 23 | });␊ 24 | ␊ 25 | const test = {␊ 26 | styles: <_JSXStyle id={"2052294191"}>{"div.jsx-2052294191{color:red;}"},␊ 27 | className: "jsx-2052294191"␊ 28 | };␊ 29 | ␊ 30 | const dynamicStyles2 = props => ({␊ 31 | styles: <_JSXStyle id={"290194820"} dynamic={[props.color]}>{\`div.__jsx-style-dynamic-selector{color:${props.color};}\`},␊ 32 | className: _JSXStyle.dynamic([["290194820", [props.color]]])␊ 33 | });␊ 34 | ␊ 35 | const ExampleComponent = props => {␊ 36 | const {␊ 37 | className,␊ 38 | styles␊ 39 | } = dynamicStyles(props);␊ 40 | return
    ␊ 41 | howdy␊ 42 | {styles}␊ 43 |
    ;␊ 44 | };` 45 | 46 | ## can alias the named import 47 | 48 | > Snapshot 1 49 | 50 | `import _JSXStyle from "styled-jsx/style";␊ 51 | ({␊ 52 | styles: <_JSXStyle id={"2886504620"}>{"div.jsx-2886504620{color:red;}"},␊ 53 | className: "jsx-2886504620"␊ 54 | });` 55 | 56 | ## injects JSXStyle for nested scope 57 | 58 | > Snapshot 1 59 | 60 | `import _JSXStyle from "styled-jsx/style";␊ 61 | ␊ 62 | function test() {␊ 63 | ({␊ 64 | styles: <_JSXStyle id={"2886504620"}>{"div.jsx-2886504620{color:red;}"},␊ 65 | className: "jsx-2886504620"␊ 66 | });␊ 67 | }` 68 | -------------------------------------------------------------------------------- /test/snapshots/macro.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/styled-jsx/d7a59379134d73afaeb98177387cd62d54d746be/test/snapshots/macro.js.snap -------------------------------------------------------------------------------- /test/snapshots/plugins.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/plugins.js` 2 | 3 | The actual snapshot is saved in `plugins.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## applies plugins 8 | 9 | > Snapshot 1 10 | 11 | `import _JSXStyle from "styled-jsx/style";␊ 12 | import styles from './styles';␊ 13 | const color = 'red';␊ 14 | export default (() =>
    ␊ 15 |

    test

    ␊ 16 | <_JSXStyle id={"2799750516"} dynamic={[color, otherColor]}>{\`span.${color}.__jsx-style-dynamic-selector{color:${otherColor};}\`}␊ 17 | <_JSXStyle id={styles.__hash}>{styles}␊ 18 |
    );` 19 | 20 | ## babel-test plugin strips jsx attribute 21 | 22 | > Snapshot 1 23 | 24 | `import styles from './styles';␊ 25 | const color = 'red';␊ 26 | export default (() =>
    ␊ 27 |

    test

    ␊ 28 | ␊ 33 | ␊ 34 |
    );` 35 | 36 | ## passes options to plugins 37 | 38 | > Snapshot 1 39 | 40 | `import _JSXStyle from "styled-jsx/style";␊ 41 | import styles from './styles';␊ 42 | const color = 'red';␊ 43 | export default (() =>
    ␊ 44 |

    test

    ␊ 45 | <_JSXStyle id={"2799750516"} dynamic={[color, otherColor]}>{".test.__jsx-style-dynamic-selector{content:\\"{\\"foo\\":false,\\"babel\\":{\\"location\\":{\\"start\\":{\\"line\\":7,\\"column\\":16,\\"index\\":114},\\"end\\":{\\"line\\":11,\\"column\\":5,\\"index\\":180}},\\"vendorPrefixes\\":false,\\"sourceMaps\\":false,\\"isGlobal\\":false}}\\";}"}␊ 46 | <_JSXStyle id={styles.__hash}>{styles}␊ 47 |
    );` 48 | -------------------------------------------------------------------------------- /test/snapshots/plugins.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/styled-jsx/d7a59379134d73afaeb98177387cd62d54d746be/test/snapshots/plugins.js.snap -------------------------------------------------------------------------------- /test/snapshots/styles.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/styles.js` 2 | 3 | The actual snapshot is saved in `styles.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## transpile styles with attributes 8 | 9 | > Snapshot 1 10 | 11 | 'html.jsx-123{background-image:linear-gradient( 0deg, rgba(255,255,255,0.8), rgba(255,255,255,0.8) ), url(/static/background.svg);}p{color:blue;}p{color:blue;}p,a.jsx-123{color:blue;}.foo + a{color:red;}body{font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial, sans-serif;}p.jsx-123{color:red;}p.jsx-123{color:red;}*.jsx-123{color:blue;}[href=\'woot\'].jsx-123{color:red;}p.jsx-123 a.jsx-123 span.jsx-123{color:red;}p.jsx-123 span{background:blue;}p.jsx-123 a[title="\'w \' \' t\'"].jsx-123{margin:auto;}p.jsx-123 span:not(.test){color:green;}p.jsx-123,h1.jsx-123{color:blue;-webkit-animation:hahaha-jsx-123 3s ease forwards infinite;animation:hahaha-jsx-123 3s ease forwards infinite;-webkit-animation-name:hahaha-jsx-123;animation-name:hahaha-jsx-123;-webkit-animation-delay:100ms;animation-delay:100ms;}p.jsx-123{-webkit-animation:hahaha-jsx-123 1s,hehehe-jsx-123 2s;animation:hahaha-jsx-123 1s,hehehe-jsx-123 2s;}p.jsx-123:hover{color:red;}p.jsx-123::before{color:red;}.jsx-123:hover{color:red;}.jsx-123::before{color:red;}.jsx-123:hover p.jsx-123{color:red;}p.jsx-123+a.jsx-123{color:red;}p.jsx-123~a.jsx-123{color:red;}p.jsx-123>a.jsx-123{color:red;}@-webkit-keyframes hahaha-jsx-123{from{top:0;}to{top:100;}}@keyframes hahaha-jsx-123{from{top:0;}to{top:100;}}@-webkit-keyframes hehehe-jsx-123{from{left:0;}to{left:100;}}@keyframes hehehe-jsx-123{from{left:0;}to{left:100;}}@media (min-width:500px){.test.jsx-123{color:red;}}.test.jsx-123{display:block;}.inline-flex.jsx-123{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;}.flex.jsx-123{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.test.jsx-123{box-shadow:0 0 10px black,inset 0 0 5px black;}.test[title=\',\'].jsx-123{display:inline-block;}.test.is-status.jsx-123 .test.jsx-123{color:red;}.a-selector.jsx-123:hover,.a-selector.jsx-123:focus{outline:none;}@media (min-width:1px) and (max-width:768px){[class*=\'grid__col--\'].jsx-123{margin-top:12px;margin-bottom:12px;}}@media (max-width:64em){.test.jsx-123{margin-bottom:1em;}@supports (-moz-appearance:none) and (display:contents){.test.jsx-123{margin-bottom:2rem;}}}' 12 | -------------------------------------------------------------------------------- /test/snapshots/styles.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/styled-jsx/d7a59379134d73afaeb98177387cd62d54d746be/test/snapshots/styles.js.snap -------------------------------------------------------------------------------- /test/styles.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | import test from 'ava' 3 | 4 | // Ours 5 | import transform from '../src/lib/style-transform' 6 | import read from './_read' 7 | 8 | test('transpile styles with attributes', async t => { 9 | const src = await read('./fixtures/transform.css') 10 | t.snapshot(transform('.jsx-123', src)) 11 | }) 12 | 13 | test('throws when using nesting', t => { 14 | const fixtures = [ 15 | `div { &:hover { color: red } }`, 16 | `div { 17 | color: green; 18 | &:hover { color: red } }`, 19 | `:hover { div { color: red } }`, 20 | `@media all { 21 | div { 22 | &:hover { color: red } 23 | } 24 | }`, 25 | `* { div { color: red } 26 | &.test { 27 | color: red; 28 | } 29 | }`, 30 | `span {} 31 | .test { 32 | color: red; 33 | div& { 34 | color: red; 35 | } 36 | }` 37 | ] 38 | 39 | fixtures.forEach(fixture => { 40 | t.throws(() => transform('', fixture)) 41 | t.throws(() => transform('.jsx-123', fixture)) 42 | }) 43 | }) 44 | 45 | test("doesn't throw when using at-rules", t => { 46 | const fixtures = [ 47 | '@media (min-width: 480px) { div { color: red } }', 48 | `span {} 49 | @media (min-width: 480px) { div { color: red } }`, 50 | `@media (min-width: 480px) { div { color: red } } 51 | span {}`, 52 | `:hover {} 53 | @media (min-width: 480px) { div { color: red } }`, 54 | `:hover { color: green } 55 | @media (min-width: 480px) { div { color: red } }`, 56 | `@media (min-width: 480px) { div {} }`, 57 | `@keyframes foo { 58 | 0% { opacity: 0 } 59 | 100% { opacity: 1} 60 | } 61 | `, 62 | // Line with one space before @rule 63 | `div { color: red; } 64 | 65 | @media screen and (min-width: 480px) { 66 | div { color: red; } 67 | } 68 | `, 69 | // Line with one tab before @rule 70 | `div { color: red; } 71 | 72 | @media screen and (min-width: 480px) { 73 | div { color: red; } 74 | } 75 | ` 76 | ] 77 | 78 | fixtures.forEach(fixture => { 79 | t.notThrows(() => transform('', fixture)) 80 | t.notThrows(() => transform('.jsx-123', fixture)) 81 | }) 82 | }) 83 | 84 | test('splits rules for `optimizeForSpeed`', t => { 85 | function assert(input, expected, prefix = '') { 86 | t.deepEqual(transform(prefix, input, { splitRules: true }), expected) 87 | } 88 | 89 | assert('div { color: red }', ['div{color:red;}']) 90 | 91 | assert('div { color: red } .p { color: red }', [ 92 | 'div{color:red;}', 93 | '.p{color:red;}' 94 | ]) 95 | 96 | assert('div, span { color: red } a > .p { color: red }', [ 97 | 'div,span{color:red;}', 98 | 'a>.p{color:red;}' 99 | ]) 100 | 101 | assert( 102 | '@media (min-width: 400px) { div, span { color: red } } a > .p { color: red }', 103 | ['@media (min-width:400px){div,span{color:red;}}', 'a>.p{color:red;}'] 104 | ) 105 | 106 | assert( 107 | '@media (min-width: 400px) { div { color: red } span { color: red } } a > .p { color: red }', 108 | [ 109 | '@media (min-width:400px){div{color:red;}span{color:red;}}', 110 | 'a>.p{color:red;}' 111 | ] 112 | ) 113 | 114 | assert( 115 | `@media (min-width: 1px) and (max-width: 768px) { 116 | [class*='test__test--'] { 117 | color: red; 118 | } 119 | }`, 120 | [ 121 | `@media (min-width:1px) and (max-width:768px){[class*='test__test--']{color:red;}}` 122 | ] 123 | ) 124 | 125 | assert( 126 | 'span { color: red } @font-face { font-family: test; src: url(test.woff); } div { color: red }', 127 | [ 128 | '@font-face{font-family:test;src:url(test.woff);}', 129 | 'span{color:red;}', 130 | 'div{color:red;}' 131 | ] 132 | ) 133 | 134 | assert('@charset "UTF-8"', ['@charset "UTF-8";']) 135 | 136 | assert('@import "./test.css"', ['@import "./test.css";']) 137 | 138 | assert( 139 | ` 140 | @keyframes test { 141 | 0% { opacity: 0 } 142 | 100% { opacity: 1 } 143 | } 144 | `, 145 | [ 146 | '@-webkit-keyframes test{0%{opacity:0;}100%{opacity:1;}}', 147 | '@keyframes test{0%{opacity:0;}100%{opacity:1;}}' 148 | ] 149 | ) 150 | 151 | assert( 152 | ` 153 | @supports (display: flex) { 154 | div { display: flex; } 155 | } 156 | `, 157 | [ 158 | '@supports (display:-webkit-box) or (display:-webkit-flex) or (display:-ms-flexbox) or (display:flex){div{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}}' 159 | ] 160 | ) 161 | 162 | assert( 163 | ` 164 | @import "./test.css"; 165 | @supports (display: flex) { 166 | div { display: flex; } 167 | } 168 | div { color: red } 169 | a, div { color: red } 170 | @import "./test.css"; 171 | @media (min-width: 400px) { div, span { color: red } } 172 | `, 173 | [ 174 | '@import "./test.css";', 175 | '@import "./test.css";', 176 | '@supports (display:-webkit-box) or (display:-webkit-flex) or (display:-ms-flexbox) or (display:flex){div{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}}', 177 | 'div{color:red;}', 178 | 'a,div{color:red;}', 179 | '@media (min-width:400px){div,span{color:red;}}' 180 | ] 181 | ) 182 | 183 | assert('@namespace url(http://www.w3.org/1999/xhtml)', [ 184 | '@namespace url(http://www.w3.org/1999/xhtml);' 185 | ]) 186 | assert('@namespace svg url(http://www.w3.org/2000/svg)', [ 187 | '@namespace svg url(http://www.w3.org/2000/svg);' 188 | ]) 189 | assert('@page :first { margin: 1in; }', ['@page :first{margin:1in;}']) 190 | 191 | assert( 192 | ` 193 | div { 194 | animation: fade-in ease-in 1; 195 | animation-fill-mode: forwards; 196 | animation-duration: 500ms; 197 | opacity: 0; 198 | } 199 | @keyframes fade-in { 200 | from { 201 | opacity: 0; 202 | } 203 | 204 | to { 205 | opacity: 1; 206 | } 207 | } 208 | `, 209 | [ 210 | 'div.jsx-123{-webkit-animation:fade-in-jsx-123 ease-in 1;animation:fade-in-jsx-123 ease-in 1;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-duration:500ms;animation-duration:500ms;opacity:0;}', 211 | '@-webkit-keyframes fade-in-jsx-123{from{opacity:0;}to{opacity:1;}}', 212 | '@keyframes fade-in-jsx-123{from{opacity:0;}to{opacity:1;}}' 213 | ], 214 | '.jsx-123' 215 | ) 216 | 217 | assert( 218 | ` 219 | div { 220 | animation: fade-in ease-in 1; 221 | animation-fill-mode: forwards; 222 | animation-duration: 500ms; 223 | opacity: 0; 224 | } 225 | 226 | @keyframes fade-in { 227 | from { 228 | opacity: 0; 229 | } 230 | 231 | to { 232 | opacity: 1; 233 | } 234 | } 235 | `, 236 | [ 237 | 'div{-webkit-animation:fade-in ease-in 1;animation:fade-in ease-in 1;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-duration:500ms;animation-duration:500ms;opacity:0;}', 238 | '@-webkit-keyframes fade-in{from{opacity:0;}to{opacity:1;}}', 239 | '@keyframes fade-in{from{opacity:0;}to{opacity:1;}}' 240 | ] 241 | ) 242 | 243 | assert( 244 | `div { color: red } ::placeholder { color: green }`, 245 | [ 246 | 'div.jsx-123{color:red;}', 247 | '.jsx-123::-webkit-input-placeholder{color:green;}', 248 | '.jsx-123::-moz-placeholder{color:green;}', 249 | '.jsx-123:-ms-input-placeholder{color:green;}', 250 | '.jsx-123::placeholder{color:green;}' 251 | ], 252 | '.jsx-123' 253 | ) 254 | }) 255 | -------------------------------------------------------------------------------- /test/stylesheet-registry.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | import test from 'ava' 3 | 4 | // Ours 5 | import { StyleSheetRegistry } from '../src/stylesheet-registry' 6 | import makeSheet, { invalidRules } from './stylesheet' 7 | import withMock, { withMockDocument } from './helpers/with-mock' 8 | import { computeId, computeSelector } from '../src/lib/hash' 9 | 10 | function makeRegistry(options = { optimizeForSpeed: true }) { 11 | const registry = new StyleSheetRegistry({ 12 | styleSheet: makeSheet(options), 13 | ...options 14 | }) 15 | registry.selectFromServer = () => ({}) 16 | return registry 17 | } 18 | 19 | const cssRule = 'div { color: red }' 20 | const cssRuleAlt = 'p { color: red }' 21 | 22 | // registry.add 23 | 24 | test( 25 | 'add', 26 | withMock(withMockDocument, t => { 27 | const options = [ 28 | { optimizeForSpeed: true, isBrowser: true }, 29 | { optimizeForSpeed: false, isBrowser: true }, 30 | { optimizeForSpeed: true, isBrowser: false }, 31 | { optimizeForSpeed: false, isBrowser: false } 32 | ] 33 | 34 | options.forEach(options => { 35 | if (options.isBrowser) { 36 | globalThis.window = globalThis 37 | } 38 | 39 | const registry = makeRegistry(options) 40 | 41 | registry.add({ 42 | id: '123', 43 | children: options.optimizeForSpeed ? [cssRule] : cssRule 44 | }) 45 | 46 | t.deepEqual(registry.cssRules(), [['jsx-123', cssRule]]) 47 | 48 | // Dedupe 49 | 50 | registry.add({ 51 | id: '123', 52 | children: options.optimizeForSpeed ? [cssRule] : cssRule 53 | }) 54 | 55 | t.deepEqual(registry.cssRules(), [['jsx-123', cssRule]]) 56 | 57 | registry.add({ 58 | id: '345', 59 | children: options.optimizeForSpeed ? [cssRule] : cssRule 60 | }) 61 | 62 | t.deepEqual(registry.cssRules(), [ 63 | ['jsx-123', cssRule], 64 | ['jsx-345', cssRule] 65 | ]) 66 | 67 | if (options.optimizeForSpeed) { 68 | registry.add({ id: '456', children: [cssRule, cssRuleAlt] }) 69 | 70 | t.deepEqual(registry.cssRules(), [ 71 | ['jsx-123', cssRule], 72 | ['jsx-345', cssRule], 73 | ['jsx-456', 'div { color: red }p { color: red }'] 74 | ]) 75 | } 76 | 77 | if (options.isBrowser) { 78 | delete globalThis.window 79 | } 80 | }) 81 | }) 82 | ) 83 | 84 | test( 85 | 'add - filters out invalid rules (index `-1`)', 86 | withMock(withMockDocument, t => { 87 | globalThis.window = globalThis 88 | 89 | const registry = makeRegistry() 90 | 91 | // Insert a valid rule 92 | registry.add({ id: '123', children: [cssRule] }) 93 | 94 | // Insert an invalid rule 95 | registry.add({ id: '456', children: [invalidRules[0]] }) 96 | 97 | // Insert another valid rule 98 | registry.add({ id: '678', children: [cssRule] }) 99 | 100 | t.deepEqual(registry.cssRules(), [ 101 | ['jsx-123', 'div { color: red }'], 102 | ['jsx-678', 'div { color: red }'] 103 | ]) 104 | 105 | delete globalThis.window 106 | }) 107 | ) 108 | 109 | test( 110 | 'it does not throw when inserting an invalid rule', 111 | withMock(withMockDocument, t => { 112 | globalThis.window = globalThis 113 | 114 | const registry = makeRegistry() 115 | 116 | // Insert a valid rule 117 | registry.add({ id: '123', children: [cssRule] }) 118 | 119 | t.notThrows(() => { 120 | // Insert an invalid rule 121 | registry.add({ id: '456', children: [invalidRules[0]] }) 122 | }) 123 | 124 | t.deepEqual(registry.cssRules(), [['jsx-123', 'div { color: red }']]) 125 | 126 | delete globalThis.window 127 | }) 128 | ) 129 | 130 | test('add - sanitizes dynamic CSS on the server', t => { 131 | const registry = makeRegistry({ optimizeForSpeed: false }) 132 | 133 | registry.add({ 134 | id: '123', 135 | children: [ 136 | 'div.__jsx-style-dynamic-selector { color: red }' 137 | ], 138 | dynamic: ['red'] 139 | }) 140 | 141 | t.deepEqual(registry.cssRules(), [ 142 | [ 143 | 'jsx-1871671996', 144 | 'div.jsx-1871671996 { color: red<\\/style> }' 145 | ] 146 | ]) 147 | }) 148 | 149 | test('add - nonce is properly fetched from meta tag', t => { 150 | const originalDocument = globalThis.document 151 | // We need to stub a document in order to simulate the meta tag 152 | globalThis.document = { 153 | querySelector(query) { 154 | t.is(query, 'meta[property="csp-nonce"]') 155 | return { 156 | getAttribute(attr) { 157 | t.is(attr, 'content') 158 | return 'test-nonce' 159 | } 160 | } 161 | } 162 | } 163 | 164 | globalThis.window = globalThis 165 | 166 | const registry = makeRegistry() 167 | registry.add({ id: '123', children: [cssRule] }) 168 | 169 | t.is(registry._sheet._nonce, 'test-nonce') 170 | 171 | globalThis.document = originalDocument 172 | 173 | delete globalThis.window 174 | }) 175 | 176 | // registry.remove 177 | 178 | test( 179 | 'remove', 180 | withMock(withMockDocument, t => { 181 | const options = [ 182 | { optimizeForSpeed: true, isBrowser: true }, 183 | { optimizeForSpeed: false, isBrowser: true }, 184 | { optimizeForSpeed: true, isBrowser: false }, 185 | { optimizeForSpeed: false, isBrowser: false } 186 | ] 187 | 188 | options.forEach(options => { 189 | if (options.isBrowser) { 190 | globalThis.window = globalThis 191 | } 192 | 193 | const registry = makeRegistry(options) 194 | registry.add({ 195 | id: '123', 196 | children: options.optimizeForSpeed ? [cssRule] : cssRule 197 | }) 198 | 199 | registry.add({ 200 | id: '345', 201 | children: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt 202 | }) 203 | 204 | registry.remove({ id: '123' }) 205 | t.deepEqual(registry.cssRules(), [['jsx-345', cssRuleAlt]]) 206 | 207 | // Add a duplicate 208 | registry.add({ 209 | id: '345', 210 | children: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt 211 | }) 212 | // and remove it 213 | registry.remove({ id: '345' }) 214 | // Still in the registry 215 | t.deepEqual(registry.cssRules(), [['jsx-345', cssRuleAlt]]) 216 | // remove again 217 | registry.remove({ id: '345' }) 218 | // now the registry should be empty 219 | t.deepEqual(registry.cssRules(), []) 220 | 221 | if (options.isBrowser) { 222 | delete globalThis.window 223 | } 224 | }) 225 | }) 226 | ) 227 | 228 | // registry.update 229 | 230 | test( 231 | 'update', 232 | withMock(withMockDocument, t => { 233 | const options = [ 234 | { optimizeForSpeed: true, isBrowser: true }, 235 | { optimizeForSpeed: false, isBrowser: true }, 236 | { optimizeForSpeed: true, isBrowser: false }, 237 | { optimizeForSpeed: false, isBrowser: false } 238 | ] 239 | 240 | options.forEach(options => { 241 | if (options.isBrowser) { 242 | globalThis.window = globalThis 243 | } 244 | 245 | const registry = makeRegistry(options) 246 | registry.add({ 247 | id: '123', 248 | children: options.optimizeForSpeed ? [cssRule] : cssRule 249 | }) 250 | 251 | registry.add({ 252 | id: '123', 253 | children: options.optimizeForSpeed ? [cssRule] : cssRule 254 | }) 255 | 256 | registry.update( 257 | { id: '123' }, 258 | { 259 | id: '345', 260 | children: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt 261 | } 262 | ) 263 | // Doesn't remove when there are multiple instances of 123 264 | t.deepEqual(registry.cssRules(), [ 265 | ['jsx-123', cssRule], 266 | ['jsx-345', cssRuleAlt] 267 | ]) 268 | 269 | registry.remove({ id: '345' }) 270 | t.deepEqual(registry.cssRules(), [['jsx-123', cssRule]]) 271 | 272 | // Update again 273 | registry.update( 274 | { id: '123' }, 275 | { 276 | id: '345', 277 | children: options.optimizeForSpeed ? [cssRuleAlt] : cssRuleAlt 278 | } 279 | ) 280 | // 123 replaced with 345 281 | t.deepEqual(registry.cssRules(), [['jsx-345', cssRuleAlt]]) 282 | 283 | if (options.isBrowser) { 284 | delete globalThis.window 285 | } 286 | }) 287 | }) 288 | ) 289 | 290 | // createComputeId 291 | 292 | test( 293 | 'createComputeId', 294 | withMock(withMockDocument, t => { 295 | // without props 296 | t.is(computeId('123'), 'jsx-123') 297 | 298 | // with props 299 | t.is(computeId('123', ['test', 3, 'test']), 'jsx-1172888331') 300 | }) 301 | ) 302 | 303 | // createComputeSelector 304 | 305 | test( 306 | 'createComputeSelector', 307 | withMock(withMockDocument, t => { 308 | t.is( 309 | computeSelector( 310 | 'jsx-123', 311 | '.test {} .__jsx-style-dynamic-selector { color: red } .__jsx-style-dynamic-selector { color: red }' 312 | ), 313 | '.test {} .jsx-123 { color: red } .jsx-123 { color: red }' 314 | ) 315 | }) 316 | ) 317 | 318 | // getIdAndRules 319 | 320 | test( 321 | 'getIdAndRules', 322 | withMock(withMockDocument, t => { 323 | const utilRegistry = makeRegistry() 324 | // simple 325 | t.deepEqual( 326 | utilRegistry.getIdAndRules({ 327 | id: '123', 328 | children: '.test {} .jsx-123 { color: red } .jsx-123 { color: red }' 329 | }), 330 | { 331 | styleId: 'jsx-123', 332 | rules: ['.test {} .jsx-123 { color: red } .jsx-123 { color: red }'] 333 | } 334 | ) 335 | 336 | // dynamic 337 | t.deepEqual( 338 | utilRegistry.getIdAndRules({ 339 | id: '123', 340 | children: 341 | '.test {} .__jsx-style-dynamic-selector { color: red } .__jsx-style-dynamic-selector { color: red }', 342 | dynamic: ['test', 3, 'test'] 343 | }), 344 | { 345 | styleId: 'jsx-1172888331', 346 | rules: [ 347 | '.test {} .jsx-1172888331 { color: red } .jsx-1172888331 { color: red }' 348 | ] 349 | } 350 | ) 351 | 352 | // dynamic, css array 353 | t.deepEqual( 354 | utilRegistry.getIdAndRules({ 355 | id: '123', 356 | children: [ 357 | '.test {}', 358 | '.__jsx-style-dynamic-selector { color: red }', 359 | '.__jsx-style-dynamic-selector { color: red }' 360 | ], 361 | dynamic: ['test', 3, 'test'] 362 | }), 363 | { 364 | styleId: 'jsx-1172888331', 365 | rules: [ 366 | '.test {}', 367 | '.jsx-1172888331 { color: red }', 368 | '.jsx-1172888331 { color: red }' 369 | ] 370 | } 371 | ) 372 | }) 373 | ) 374 | -------------------------------------------------------------------------------- /test/stylesheet.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | import test from 'ava' 3 | 4 | // Ours 5 | import StyleSheet from '../src/lib/stylesheet' 6 | import withMock, { withMockDocument } from './helpers/with-mock' 7 | 8 | export const invalidRules = ['invalid rule'] 9 | 10 | export default function makeSheet(options = { optimizeForSpeed: true }) { 11 | const sheet = new StyleSheet(options) 12 | // mocks 13 | sheet.makeStyleTag = function(name, cssString) { 14 | const cssRules = cssString ? [{ cssText: cssString }] : [] 15 | const tag = { 16 | sheet: { 17 | cssRules, 18 | insertRule: (rule, index) => { 19 | if (invalidRules.includes(rule)) { 20 | throw new Error('invalid rule') 21 | } 22 | 23 | if (typeof index === 'number') { 24 | cssRules[index] = { cssText: rule } 25 | } else { 26 | cssRules.push({ cssText: rule }) 27 | } 28 | 29 | return index 30 | }, 31 | deleteRule: index => { 32 | if (options.optimizeForSpeed) { 33 | cssRules[index] = { 34 | cssText: `#${name}-deleted-rule____{}` 35 | } 36 | } else { 37 | cssRules[index] = null 38 | } 39 | }, 40 | replaceRule: (index, rule) => { 41 | cssRules[index] = { cssText: rule } 42 | } 43 | }, 44 | parentNode: { 45 | removeChild: () => {} 46 | } 47 | } 48 | 49 | let textContent = cssString 50 | Object.defineProperty(tag, 'textContent', { 51 | get: () => textContent, 52 | set: text => { 53 | textContent = text 54 | cssRules.length = 0 55 | cssRules.push({ cssText: text }) 56 | } 57 | }) 58 | 59 | return tag 60 | } 61 | 62 | return sheet 63 | } 64 | 65 | // sheet.setOptimizeForSpeed 66 | 67 | test( 68 | 'can change optimizeForSpeed only when the stylesheet is empty', 69 | withMock(withMockDocument, t => { 70 | globalThis.window = globalThis 71 | 72 | const sheet = makeSheet() 73 | 74 | sheet.inject() 75 | t.notThrows(() => { 76 | sheet.setOptimizeForSpeed(true) 77 | }) 78 | 79 | sheet.insertRule('div { color: red }') 80 | t.throws(() => { 81 | sheet.setOptimizeForSpeed(false) 82 | }) 83 | 84 | sheet.flush() 85 | t.notThrows(() => { 86 | sheet.setOptimizeForSpeed(false) 87 | }) 88 | 89 | delete globalThis.window 90 | }) 91 | ) 92 | 93 | // sheet.insertRule 94 | 95 | test( 96 | 'insertRule', 97 | withMock(withMockDocument, t => { 98 | const options = [ 99 | { optimizeForSpeed: true, isBrowser: true }, 100 | { optimizeForSpeed: false, isBrowser: true }, 101 | { optimizeForSpeed: true, isBrowser: false } 102 | ] 103 | 104 | options.forEach(options => { 105 | if (options.isBrowser) { 106 | globalThis.window = globalThis 107 | } 108 | 109 | const sheet = makeSheet(options) 110 | sheet.inject() 111 | 112 | sheet.insertRule('div { color: red }') 113 | t.deepEqual(sheet.cssRules(), [{ cssText: 'div { color: red }' }]) 114 | 115 | sheet.insertRule('div { color: green }') 116 | t.deepEqual(sheet.cssRules(), [ 117 | { cssText: 'div { color: red }' }, 118 | { cssText: 'div { color: green }' } 119 | ]) 120 | 121 | if (options.isBrowser) { 122 | delete globalThis.window 123 | } 124 | }) 125 | }) 126 | ) 127 | 128 | test( 129 | 'insertRule - returns the rule index', 130 | withMock(withMockDocument, t => { 131 | globalThis.window = globalThis 132 | 133 | const sheet = makeSheet() 134 | sheet.inject() 135 | 136 | let i = sheet.insertRule('div { color: red }') 137 | t.is(i, 0) 138 | 139 | i = sheet.insertRule('div { color: red }') 140 | t.is(i, 1) 141 | 142 | delete globalThis.window 143 | }) 144 | ) 145 | 146 | test( 147 | 'insertRule - handles invalid rules and returns -1 as index', 148 | withMock(withMockDocument, t => { 149 | globalThis.window = globalThis 150 | 151 | const sheet = makeSheet() 152 | sheet.inject() 153 | 154 | const i = sheet.insertRule(invalidRules[0]) 155 | t.is(i, -1) 156 | 157 | delete globalThis.window 158 | }) 159 | ) 160 | 161 | test( 162 | 'insertRule - does not fail when the css is a String object', 163 | withMock(withMockDocument, t => { 164 | globalThis.window = globalThis 165 | 166 | const sheet = makeSheet() 167 | sheet.inject() 168 | 169 | /* eslint-disable unicorn/new-for-builtins,no-new-wrappers */ 170 | 171 | sheet.insertRule(new String('div { color: red }')) 172 | t.deepEqual(sheet.cssRules(), [ 173 | { cssText: new String('div { color: red }') } 174 | ]) 175 | 176 | delete globalThis.window 177 | /* eslint-enable */ 178 | }) 179 | ) 180 | 181 | // sheet.deleteRule 182 | 183 | test( 184 | 'deleteRule', 185 | withMock(withMockDocument, t => { 186 | const options = [ 187 | { optimizeForSpeed: true, isBrowser: true }, 188 | { optimizeForSpeed: false, isBrowser: true }, 189 | { optimizeForSpeed: true, isBrowser: false }, 190 | { optimizeForSpeed: false, isBrowser: false } 191 | ] 192 | 193 | options.forEach(options => { 194 | if (options.isBrowser) { 195 | globalThis.window = globalThis 196 | } 197 | 198 | const sheet = makeSheet(options) 199 | sheet.inject() 200 | 201 | sheet.insertRule('div { color: red }') 202 | sheet.insertRule('div { color: green }') 203 | const rulesCount = sheet.length 204 | 205 | sheet.deleteRule(1) 206 | // When deleting we replace rules with placeholders to keep the indices stable. 207 | t.is(sheet.length, rulesCount) 208 | 209 | t.deepEqual(sheet.cssRules(), [{ cssText: 'div { color: red }' }, null]) 210 | 211 | if (options.isBrowser) { 212 | delete globalThis.window 213 | } 214 | }) 215 | }) 216 | ) 217 | 218 | test( 219 | 'deleteRule - does not throw when the rule at index does not exist', 220 | withMock(withMockDocument, t => { 221 | globalThis.window = globalThis 222 | 223 | const sheet = makeSheet() 224 | sheet.inject() 225 | 226 | t.notThrows(() => { 227 | sheet.deleteRule(sheet.length + 1) 228 | }) 229 | 230 | delete globalThis.window 231 | }) 232 | ) 233 | 234 | // sheet.replaceRule 235 | 236 | test( 237 | 'replaceRule', 238 | withMock(withMockDocument, t => { 239 | const options = [ 240 | { optimizeForSpeed: true, isBrowser: true }, 241 | { optimizeForSpeed: false, isBrowser: true }, 242 | { optimizeForSpeed: true, isBrowser: false }, 243 | { optimizeForSpeed: false, isBrowser: false } 244 | ] 245 | 246 | options.forEach(options => { 247 | if (options.isBrowser) { 248 | globalThis.window = globalThis 249 | } 250 | 251 | const sheet = makeSheet(options) 252 | sheet.inject() 253 | 254 | const index = sheet.insertRule('div { color: red }') 255 | sheet.replaceRule(index, 'p { color: hotpink }') 256 | 257 | t.deepEqual(sheet.cssRules(), [{ cssText: 'p { color: hotpink }' }]) 258 | 259 | if (options.isBrowser) { 260 | delete globalThis.window 261 | } 262 | }) 263 | }) 264 | ) 265 | 266 | test( 267 | 'replaceRule - handles invalid rules gracefully', 268 | withMock(withMockDocument, t => { 269 | globalThis.window = globalThis 270 | 271 | const sheet = makeSheet() 272 | sheet.inject() 273 | 274 | // Insert two rules 275 | sheet.insertRule('div { color: red }') 276 | const index = sheet.insertRule('div { color: red }') 277 | 278 | // Replace the latter with an invalid rule 279 | const i = sheet.replaceRule(index, invalidRules[0]) 280 | t.is(i, index) 281 | t.is(sheet.length, 2) 282 | 283 | // Even though replacement (insertion) failed deletion succeeded 284 | // therefore the lib must insert a delete placeholder which resolves to `null` 285 | // when `cssRules()` is called. 286 | t.deepEqual(sheet.cssRules(), [{ cssText: 'div { color: red }' }, null]) 287 | 288 | delete globalThis.window 289 | }) 290 | ) 291 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {} /* Specify a set of entries that re-map imports to additional lookup locations. */, 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": [] /* Specify type package names to be included without being referenced in a source file. */, 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 83 | 84 | /* Type Checking */ 85 | "strict": true /* Enable all strict type-checking options. */, 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": false /* Skip type checking all .d.ts files. */ 108 | }, 109 | } 110 | -------------------------------------------------------------------------------- /webpack.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | loader: require.resolve('./dist/webpack') 3 | } 4 | --------------------------------------------------------------------------------