├── example ├── .gitignore ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── src │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── App.js │ ├── serviceWorker.js │ └── logo.svg ├── package.json └── README.md ├── macro.d.ts ├── src ├── babel │ ├── __tests__ │ │ ├── fixtures │ │ │ ├── constants.js │ │ │ ├── simple.jsx │ │ │ ├── merging.jsx │ │ │ ├── combining.jsx │ │ │ ├── conditional-with-dce.jsx │ │ │ ├── conditional.jsx │ │ │ ├── extraction.jsx │ │ │ ├── pseudo.jsx │ │ │ └── media-query.jsx │ │ ├── index.test.js │ │ └── __snapshots__ │ │ │ └── index.test.js.snap │ ├── utils │ │ ├── isStyles.js │ │ ├── dynamic-import-noop.js │ │ ├── process.js │ │ ├── cssToObj.js │ │ ├── hasImport.js │ │ ├── atomizer.js │ │ ├── validate.js │ │ ├── __tests__ │ │ │ └── validate.test.js │ │ ├── evaluate.js │ │ └── module.js │ ├── StyleCache.js │ ├── index.js │ └── visitors │ │ ├── CallExpression.js │ │ └── TaggedTemplateExpression.js ├── macro │ ├── __tests__ │ │ ├── index.test.zero.css │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ └── index.test.js │ └── index.js ├── index.d.ts ├── index.js └── StyleSheet.js ├── macro.js ├── .gitignore ├── LICENSE ├── package.json └── README.md /example/.gitignore: -------------------------------------------------------------------------------- 1 | *.zero.css 2 | -------------------------------------------------------------------------------- /macro.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /src/babel/__tests__/fixtures/constants.js: -------------------------------------------------------------------------------- 1 | export const TEST = 12; 2 | -------------------------------------------------------------------------------- /macro.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/macro').createCssZeroMacro(); 2 | -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigCav/css-zero/HEAD/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigCav/css-zero/HEAD/example/public/logo192.png -------------------------------------------------------------------------------- /example/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CraigCav/css-zero/HEAD/example/public/logo512.png -------------------------------------------------------------------------------- /src/macro/__tests__/index.test.zero.css: -------------------------------------------------------------------------------- 1 | .x1vong5g {color:blue} 2 | .x1dqz7z3 {color:red} 3 | .x1e4w2a9 {font-size:16px} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .rts2_cache_cjs 5 | .rts2_cache_esm 6 | .rts2_cache_umd 7 | .rts2_cache_system 8 | dist 9 | -------------------------------------------------------------------------------- /src/babel/utils/isStyles.js: -------------------------------------------------------------------------------- 1 | const isStyles = ({node}) => { 2 | return node.callee.type === 'Identifier' && node.callee.name === 'styles'; 3 | }; 4 | 5 | module.exports = isStyles; 6 | -------------------------------------------------------------------------------- /src/babel/__tests__/fixtures/simple.jsx: -------------------------------------------------------------------------------- 1 | import { css, styles } from 'css-zero'; 2 | 3 | const one = css` 4 | font-size: 16px; 5 | `; 6 | 7 | export const Component = () =>
; 8 | -------------------------------------------------------------------------------- /src/babel/__tests__/fixtures/merging.jsx: -------------------------------------------------------------------------------- 1 | import { css, styles } from 'css-zero'; 2 | 3 | const one = css` 4 | color: red; 5 | `; 6 | 7 | const another = css` 8 | color: green; 9 | `; 10 | 11 | export const Component = () =>
; 12 | -------------------------------------------------------------------------------- /src/babel/__tests__/fixtures/combining.jsx: -------------------------------------------------------------------------------- 1 | import { css, styles } from 'css-zero'; 2 | 3 | const one = css` 4 | color: red; 5 | `; 6 | 7 | const another = css` 8 | font-size: 16px; 9 | `; 10 | 11 | export const Component = () =>
; 12 | -------------------------------------------------------------------------------- /example/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/babel/__tests__/fixtures/conditional-with-dce.jsx: -------------------------------------------------------------------------------- 1 | import {css, styles} from 'css-zero'; 2 | 3 | const blue = css` 4 | color: blue; 5 | `; 6 | 7 | const base = css` 8 | color: red; 9 | font-size: 16px; 10 | `; 11 | 12 | export const LogicalAndExpression = props =>
; 13 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | type CSSProperties = { 2 | [key: string]: string | number | CSSProperties; 3 | }; 4 | 5 | export function css( 6 | strings: TemplateStringsArray, 7 | ...exprs: Array 8 | ): string; 9 | 10 | export function styles(...classNames: Array): string; 11 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | function css(strings, ...exprs) { 2 | throw new Error( 3 | 'Using the "css" tag in runtime is not supported. Make sure you have set up the Babel plugin correctly.' 4 | ); 5 | } 6 | 7 | function styles(...classes) { 8 | throw new Error( 9 | 'Using the "styles" tag in runtime is not supported. Make sure you have set up the Babel plugin correctly.' 10 | ); 11 | } 12 | 13 | exports.styles = styles; 14 | exports.css = css; 15 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/babel/utils/dynamic-import-noop.js: -------------------------------------------------------------------------------- 1 | const syntax = require('@babel/plugin-syntax-dynamic-import').default; 2 | 3 | function dynamic({types: t}) { 4 | return { 5 | inherits: syntax, 6 | 7 | visitor: { 8 | Import(path) { 9 | const noop = t.arrowFunctionExpression([], t.identifier('undefined')); 10 | 11 | path.parentPath.replaceWith( 12 | t.objectExpression([ 13 | t.objectProperty(t.identifier('then'), noop), 14 | t.objectProperty(t.identifier('catch'), noop), 15 | ]) 16 | ); 17 | }, 18 | }, 19 | }; 20 | } 21 | 22 | module.exports = dynamic; 23 | -------------------------------------------------------------------------------- /src/macro/__tests__/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`macros simple macro: simple macro 1`] = ` 4 | " 5 | import { css, styles } from '../../../macro'; 6 | 7 | const blue = css\` 8 | color: blue; 9 | \`; 10 | 11 | const base = css\` 12 | color: red; 13 | font-size: 16px; 14 | \`; 15 | 16 | export default props => ( 17 |
18 | ); 19 | 20 | ↓ ↓ ↓ ↓ ↓ ↓ 21 | 22 | import \\"./index.test.zero.css\\"; 23 | export default (props =>
); 24 | " 25 | `; 26 | -------------------------------------------------------------------------------- /src/babel/utils/process.js: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/callstack/linaria 2 | // which is MIT (c) callstack 3 | exports.nextTick = fn => setTimeout(fn, 0); 4 | 5 | exports.platform = exports.arch = exports.execPath = exports.title = 'browser'; 6 | exports.pid = 1; 7 | exports.browser = true; 8 | exports.argv = []; 9 | 10 | exports.binding = function binding() { 11 | throw new Error('No such module. (Possibly not yet loaded)'); 12 | }; 13 | 14 | exports.cwd = () => '/'; 15 | 16 | exports.exit = exports.kill = exports.chdir = exports.umask = exports.dlopen = exports.uptime = exports.memoryUsage = exports.uvCounters = () => {}; 17 | exports.features = {}; 18 | 19 | exports.env = { 20 | NODE_ENV: process.env.NODE_ENV, 21 | }; 22 | -------------------------------------------------------------------------------- /src/babel/__tests__/fixtures/conditional.jsx: -------------------------------------------------------------------------------- 1 | import { css, styles } from 'css-zero'; 2 | 3 | const blue = css` 4 | color: blue; 5 | `; 6 | 7 | const green = css` 8 | color: green; 9 | `; 10 | 11 | const base = css` 12 | color: red; 13 | font-size: 16px; 14 | `; 15 | 16 | export const LogicalAndExpression = props => ( 17 |
18 | ); 19 | 20 | export const LogicalExpressionDeterministic = () => ( 21 |
22 | ); 23 | 24 | export const IgnoreFalsey = () => ( 25 |
26 | ); 27 | 28 | export const SimpleTernaryExpression = props => ( 29 |
30 | ); 31 | -------------------------------------------------------------------------------- /src/macro/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import pluginTester from 'babel-plugin-tester'; 2 | import plugin from 'babel-plugin-macros'; 3 | 4 | pluginTester({ 5 | plugin, 6 | snapshot: true, 7 | babelOptions: { filename: __filename, parserOpts: { plugins: ['jsx'] } }, 8 | tests: [ 9 | { 10 | title: 'simple macro', 11 | code: ` 12 | import { css, styles } from '../../../macro'; 13 | 14 | const blue = css\` 15 | color: blue; 16 | \`; 17 | 18 | const base = css\` 19 | color: red; 20 | font-size: 16px; 21 | \`; 22 | 23 | export default props => ( 24 |
25 | ); 26 | `, 27 | }, 28 | ], 29 | }); 30 | -------------------------------------------------------------------------------- /src/babel/__tests__/fixtures/extraction.jsx: -------------------------------------------------------------------------------- 1 | import {css, styles} from 'css-zero'; 2 | import {TEST} from './constants'; 3 | 4 | const marginTop = 10; 5 | 6 | const staticMargin = css` 7 | margin-bottom: 2px; 8 | `; 9 | 10 | const constant = css` 11 | margin-top: ${marginTop}px; 12 | `; 13 | 14 | const constantImported = css` 15 | margin-bottom: ${TEST}px; 16 | `; 17 | 18 | export const ComponentStatic = () =>
; 19 | export const ComponentStaticInline = () => ( 20 |
27 | ); 28 | 29 | export const ComponentConstant = () =>
; 30 | export const ComponentConstantImported = () =>
; 31 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-zero-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.12.0", 7 | "react-dom": "^16.12.0", 8 | "react-scripts": "3.2.0" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | }, 31 | "devDependencies": { 32 | "css-zero": "^0.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/babel/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const core = require('@babel/core'); 3 | const dce = require('babel-plugin-remove-unused-vars'); 4 | const plugin = require('../'); 5 | 6 | expect.addSnapshotSerializer({ 7 | test: value => value && typeof value.cssZero === 'object', 8 | print: ({cssZero}) => cssZero.toString(), 9 | }); 10 | 11 | const transpile = file => 12 | core.transformFileSync(path.resolve(__dirname, file), { 13 | plugins: [plugin, dce], 14 | babelrc: false, 15 | }); 16 | 17 | it.each([ 18 | ['simple.jsx'], 19 | ['combining.jsx'], 20 | ['merging.jsx'], 21 | ['conditional.jsx'], 22 | ['conditional-with-dce.jsx'], 23 | ['pseudo.jsx'], 24 | ['media-query.jsx'], 25 | ['extraction.jsx'], 26 | ])('%s', file => { 27 | const {code, metadata} = transpile(`./fixtures/${file}`); 28 | expect(code).toMatchSnapshot(); 29 | expect(metadata).toMatchSnapshot(); 30 | }); 31 | -------------------------------------------------------------------------------- /src/StyleSheet.js: -------------------------------------------------------------------------------- 1 | module.exports = class StyleSheet { 2 | constructor() { 3 | this.rules = []; 4 | this.usage = []; 5 | } 6 | 7 | addRule(rule) { 8 | this.rules.push(rule); 9 | } 10 | 11 | trackUsage(...classNames) { 12 | this.usage.push(...classNames); 13 | } 14 | 15 | toString() { 16 | // filter unused rules 17 | const filtered = this.rules.filter(({className}) => this.usage.includes(className)); 18 | 19 | // group rules by media query 20 | const grouped = filtered.reduce((map, {selector, media, cssText}) => { 21 | // using a set to deduplicate rules within a given media query scope 22 | if (!map.has(media)) map.set(media, new Set()); 23 | map.get(media).add(`${selector} {${cssText}}`); 24 | return map; 25 | }, new Map()); 26 | 27 | // turn the rule objects into valid css rules 28 | return Array.from(grouped.entries()) 29 | .map(([media, rules]) => { 30 | const cssText = [...rules.values()].join('\n'); 31 | return media ? media + '{\n' + cssText + '\n}' : cssText; 32 | }) 33 | .join('\n'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Craig Cavalier 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. -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {css, styles} from 'css-zero/macro'; 3 | import logoUrl from './logo.svg'; 4 | 5 | const center = css` 6 | text-align: center; 7 | `; 8 | 9 | const logo = css` 10 | height: 40vmin; 11 | `; 12 | 13 | const header = css` 14 | background-color: #282c34; 15 | min-height: 100vh; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | justify-content: center; 20 | font-size: calc(10px + 2vmin); 21 | color: white; 22 | `; 23 | 24 | const link = css` 25 | color: #09d3ac; 26 | `; 27 | 28 | function App() { 29 | return ( 30 |
31 |
32 | logo 33 |

34 | Edit src/App.js and save to reload. 35 |

36 | 42 | Learn React 43 | 44 |
45 |
46 | ); 47 | } 48 | 49 | export default App; 50 | -------------------------------------------------------------------------------- /src/babel/StyleCache.js: -------------------------------------------------------------------------------- 1 | module.exports = class StyleCache { 2 | constructor() { 3 | this.styles = new Map(); 4 | this.conditionalStyles = new Map(); 5 | this.conditionallyAppliedClassNames = []; 6 | } 7 | addStyle(key, value) { 8 | this.styles.set(key, value); 9 | this.conditionalStyles.delete(key); 10 | } 11 | addConditionalStyle(key, value, test) { 12 | const current = this.styles.has(key) 13 | ? this.styles.get(key) 14 | : this.conditionalStyles.has(key) 15 | ? this.conditionalStyles.get(key) 16 | : ''; 17 | this.styles.delete(key); 18 | this.conditionalStyles.set(key, { 19 | test, 20 | consequent: value, 21 | alternate: current, 22 | }); 23 | this.conditionallyAppliedClassNames.push(value); 24 | if (typeof current === 'string') this.conditionallyAppliedClassNames.push(current); 25 | } 26 | getStyles() { 27 | return Array.from(this.styles.values()); 28 | } 29 | getConditionalStyles() { 30 | return Array.from(this.conditionalStyles.values()); 31 | } 32 | getUsedClassNames() { 33 | return [...this.styles.values(), ...this.conditionallyAppliedClassNames]; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/babel/__tests__/fixtures/pseudo.jsx: -------------------------------------------------------------------------------- 1 | import {css, styles} from 'css-zero'; 2 | 3 | const simple = css` 4 | color: red; 5 | 6 | &:hover { 7 | color: green; 8 | } 9 | `; 10 | 11 | const duplicate = css` 12 | color: black; 13 | 14 | &:hover { 15 | color: yellow; 16 | } 17 | 18 | &:hover { 19 | color: yellow; 20 | } 21 | `; 22 | 23 | const overridden = css` 24 | color: brown; 25 | 26 | &:hover { 27 | color: pink; 28 | } 29 | 30 | &:hover { 31 | color: salmon; 32 | } 33 | `; 34 | 35 | const implicit = css` 36 | :hover { 37 | color: gray; 38 | } 39 | `; 40 | 41 | const pseudoElement = css` 42 | &::before { 43 | content: ''; 44 | position: relative; 45 | } 46 | `; 47 | 48 | export const Component = () =>
; 49 | 50 | export const ComponentWithDuplicatePseudoStyle = () =>
; 51 | 52 | export const ComponentWithOverriddenPseudoStyle = () =>
; 53 | 54 | export const ComponentWithImplicitPseudoStyle = () =>
; 55 | 56 | export const ComponentWithPseudoElementStyle = () =>
; 57 | -------------------------------------------------------------------------------- /src/babel/index.js: -------------------------------------------------------------------------------- 1 | const jsx = require('@babel/plugin-syntax-jsx'); 2 | const TaggedTemplateExpression = require('./visitors/TaggedTemplateExpression'); 3 | const CallExpression = require('./visitors/CallExpression'); 4 | const StyleSheet = require('../StyleSheet'); 5 | 6 | function cssZeroBabelPlugin(babel) { 7 | const {types} = babel; 8 | return { 9 | name: 'css-zero', 10 | inherits: jsx.default, 11 | visitor: { 12 | Program: { 13 | enter(path, state) { 14 | state.styleSheet = new StyleSheet(); 15 | // We need our transforms to run before anything else 16 | // So we traverse here instead of a in a visitor 17 | path.traverse({ 18 | TaggedTemplateExpression: p => TaggedTemplateExpression(p, state, types), 19 | }); 20 | }, 21 | exit(_path, state) { 22 | const {styleSheet} = state; 23 | 24 | // Store the result as the file metadata 25 | state.file.metadata = {cssZero: styleSheet}; 26 | }, 27 | }, 28 | CallExpression(path, state) { 29 | CallExpression(path, state, types); 30 | }, 31 | }, 32 | }; 33 | } 34 | 35 | module.exports = cssZeroBabelPlugin; 36 | -------------------------------------------------------------------------------- /src/babel/__tests__/fixtures/media-query.jsx: -------------------------------------------------------------------------------- 1 | import {css, styles} from 'css-zero'; 2 | 3 | const simple = css` 4 | color: red; 5 | 6 | @media screen and (min-width: 678px) { 7 | color: green; 8 | } 9 | `; 10 | 11 | const duplicate = css` 12 | color: black; 13 | 14 | @media screen and (min-width: 678px) { 15 | color: yellow; 16 | } 17 | 18 | @media screen and (min-width: 678px) { 19 | color: yellow; 20 | } 21 | `; 22 | 23 | const overridden = css` 24 | color: brown; 25 | 26 | @media screen and (min-width: 678px) { 27 | color: pink; 28 | } 29 | 30 | @media screen and (min-width: 678px) { 31 | color: salmon; 32 | } 33 | `; 34 | 35 | const pseudo = css` 36 | color: silver; 37 | 38 | @media screen and (min-width: 678px) { 39 | &:hover { 40 | color: gold; 41 | } 42 | } 43 | `; 44 | 45 | const pseudoParent = css` 46 | color: wheat; 47 | 48 | &:hover { 49 | @media screen and (min-width: 678px) { 50 | color: sandybrown; 51 | } 52 | } 53 | `; 54 | 55 | export const Component = () =>
; 56 | 57 | export const ComponentWithDuplicateMediaStyle = () =>
; 58 | 59 | export const ComponentWithOverriddenMediaStyle = () =>
; 60 | 61 | export const ComponentWithNestedPseudo = () =>
; 62 | 63 | export const ComponentWithParentPseudo = () =>
; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-zero", 3 | "version": "0.2.0", 4 | "description": "Zero-runtime CSS-in-JS", 5 | "keywords": [ 6 | "css-in-js", 7 | "stylesheet", 8 | "css", 9 | "atomic", 10 | "zero-runtime" 11 | ], 12 | "license": "MIT", 13 | "author": "Craig Cavalier", 14 | "main": "src/index.js", 15 | "typings": "src/index.d.ts", 16 | "jest": { 17 | "testMatch": [ 18 | "**/__tests__/**/*.test.[jt]s?(x)", 19 | "**/?(*.)+(spec|test).[jt]s?(x)" 20 | ], 21 | "testPathIgnorePatterns": [ 22 | "example" 23 | ] 24 | }, 25 | "babel": { 26 | "presets": [ 27 | "@babel/preset-env", 28 | "@babel/preset-react" 29 | ] 30 | }, 31 | "prettier": { 32 | "singleQuote": true, 33 | "printWidth": 100, 34 | "trailingComma": "es5", 35 | "bracketSpacing": false 36 | }, 37 | "scripts": { 38 | "test": "jest" 39 | }, 40 | "devDependencies": { 41 | "@babel/preset-env": "^7.7.1", 42 | "@babel/preset-react": "^7.7.0", 43 | "babel-jest": "^24.9.0", 44 | "babel-plugin-remove-unused-vars": "^2.2.0", 45 | "babel-plugin-tester": "^7.0.4", 46 | "jest": "^24.9.0" 47 | }, 48 | "dependencies": { 49 | "@babel/core": "^7.7.2", 50 | "@babel/generator": "^7.7.2", 51 | "@babel/helper-module-imports": "^7.7.0", 52 | "@babel/plugin-proposal-export-namespace-from": "^7.5.2", 53 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 54 | "@babel/plugin-transform-modules-commonjs": "^7.7.0", 55 | "babel-plugin-macros": "^2.6.2", 56 | "css": "^2.2.4", 57 | "fnv1a": "^1.0.1", 58 | "stylis": "^3.5.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/babel/utils/cssToObj.js: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/jxnblk/css-to-object/ 2 | // which is MIT (c) jxnblk 3 | const {parse} = require('css'); 4 | const stylis = require('stylis'); 5 | 6 | const SEL = '_'; 7 | const SELRE = new RegExp('^' + SEL); 8 | 9 | const toObj = (css, opts) => { 10 | const wrapped = stylis(SEL, css); 11 | const ast = parse(wrapped); 12 | const obj = transform(opts)(ast.stylesheet.rules); 13 | return obj; 14 | }; 15 | 16 | const transform = opts => (rules, result = {}) => { 17 | rules.forEach(rule => { 18 | if (rule.type === 'media') { 19 | const key = '@media ' + rule.media; 20 | const decs = transform(opts)(rule.rules); 21 | result[key] = decs; 22 | return; 23 | } 24 | 25 | const [selector] = rule.selectors; 26 | const key = selector.replace(SELRE, '').trim(); 27 | 28 | if (key.length) { 29 | Object.assign(result, { 30 | [key]: getDeclarations(rule.declarations, opts), 31 | }); 32 | } else { 33 | Object.assign(result, getDeclarations(rule.declarations, opts)); 34 | } 35 | }); 36 | return result; 37 | }; 38 | 39 | const getDeclarations = (decs, opts = {}) => { 40 | const result = decs 41 | .map(d => ({ 42 | key: opts.camelCase ? camel(d.property) : d.property, 43 | value: opts.numbers ? parsePx(d.value) : d.value, 44 | })) 45 | .reduce((a, b) => { 46 | a[b.key] = b.value; 47 | return a; 48 | }, {}); 49 | return result; 50 | }; 51 | 52 | const camel = str => str.replace(/(-[a-z])/g, x => x.toUpperCase()).replace(/-/g, ''); 53 | 54 | const parsePx = val => (/px$/.test(val) ? parseFloat(val.replace(/px$/, '')) : val); 55 | 56 | module.exports = toObj; 57 | -------------------------------------------------------------------------------- /src/babel/utils/hasImport.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Verify if the binding is imported from the specified source 4 | function hasImport(t, scope, filename, identifier, source) { 5 | const binding = scope.getAllBindings()[identifier]; 6 | 7 | if (!binding) { 8 | return false; 9 | } 10 | 11 | const p = binding.path; 12 | 13 | const resolveFromFile = id => { 14 | const M = require('module'); 15 | try { 16 | return M._resolveFilename(id, { 17 | id: filename, 18 | filename, 19 | paths: M._nodeModulePaths(path.dirname(filename)), 20 | }); 21 | } catch (e) { 22 | return null; 23 | } 24 | }; 25 | 26 | const isImportingModule = value => 27 | // If the value is an exact match, assume it imports the module 28 | value === source || 29 | // Otherwise try to resolve both and check if they are the same file 30 | resolveFromFile(value) === resolveFromFile(source); 31 | 32 | if (t.isImportSpecifier(p) && t.isImportDeclaration(p.parentPath)) { 33 | return isImportingModule(p.parentPath.node.source.value); 34 | } 35 | 36 | if (t.isVariableDeclarator(p)) { 37 | if ( 38 | t.isCallExpression(p.node.init) && 39 | t.isIdentifier(p.node.init.callee) && 40 | p.node.init.callee.name === 'require' && 41 | p.node.init.arguments.length === 1 42 | ) { 43 | const node = p.node.init.arguments[0]; 44 | 45 | if (t.isStringLiteral(node)) { 46 | return isImportingModule(node.value); 47 | } 48 | 49 | if (t.isTemplateLiteral(node) && node.quasis.length === 1) { 50 | return isImportingModule(node.quasis[0].value.cooked); 51 | } 52 | } 53 | } 54 | return false; 55 | } 56 | 57 | module.exports = hasImport; 58 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/babel/utils/atomizer.js: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/jxnblk/object-style 2 | // which is MIT (c) jxnblk 3 | const fnv1a = require('fnv1a'); 4 | const cssToObj = require('./cssToObj'); 5 | const validate = require('./validate'); 6 | 7 | const AT_REG = /^@/; 8 | const AMP = /&/g; 9 | 10 | const id = seed => 'x' + fnv1a(seed).toString(36); 11 | const hyphenate = s => s.replace(/[A-Z]|^ms/g, '-$&').toLowerCase(); 12 | 13 | const createRule = (propertyName, propertyValue, selector, children, media) => { 14 | return { 15 | // key is used for deduping styles. 16 | // Using the property name alone isn't sufficient: 17 | // pseudo elements and media queries can override the base style 18 | key: media + children + propertyName, 19 | selector, 20 | cssText: propertyName + ':' + propertyValue, 21 | media, 22 | }; 23 | }; 24 | 25 | const parse = (obj, children = '', media = '') => { 26 | const rules = []; 27 | 28 | for (const key in obj) { 29 | const value = obj[key]; 30 | if (value === null || value === undefined) continue; 31 | switch (typeof value) { 32 | case 'object': 33 | if (AT_REG.test(key)) { 34 | rules.push(...parse(value, children, key)); 35 | } else { 36 | const child = key.replace(AMP, ''); 37 | rules.push(...parse(value, children + child, media)); 38 | } 39 | continue; 40 | case 'number': 41 | case 'string': 42 | const hash = id(key + value + children + media); 43 | const parentSelector = '.' + hash; 44 | const selector = children ? parentSelector + children : parentSelector; 45 | const name = hyphenate(key); 46 | const rule = createRule(name, value, selector, children, media); 47 | rules.push([hash, rule]); 48 | } 49 | } 50 | 51 | return rules; 52 | }; 53 | 54 | module.exports = css => { 55 | const obj = cssToObj(css); 56 | validate(obj); 57 | return parse(obj); 58 | }; 59 | -------------------------------------------------------------------------------- /src/babel/utils/validate.js: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/giuseppeg/style-sheet 2 | // which is MIT (c) giuseppeg 3 | 4 | module.exports = function validate(obj) { 5 | for (const k in obj) { 6 | const key = k.trim(); 7 | const value = obj[key]; 8 | if (value === null) continue; 9 | const isDeclaration = Object.prototype.toString.call(value) !== '[object Object]'; 10 | validateStr(key, isDeclaration); 11 | if (!isDeclaration) { 12 | validate(value); 13 | } else if (typeof value === 'string' && /!\s*important/.test(value)) { 14 | error('!important is not allowed'); 15 | } 16 | } 17 | }; 18 | 19 | export function validateStr(key, isDeclaration) { 20 | if (isDeclaration) { 21 | return; 22 | } 23 | 24 | if (key.charAt(0) === '@') { 25 | return; 26 | } 27 | 28 | // Selector 29 | 30 | if (key.split(',').length > 1) { 31 | error(`Invalid nested selector: '${key}'. Selectors cannot be grouped.`); 32 | } 33 | 34 | if (/:(matches|has|not|lang|any|current)/.test(key)) { 35 | error(`Detected unsupported pseudo-class: '${key}'.`); 36 | } 37 | 38 | const split = key.split(/\s*[+>~]\s*/g); 39 | 40 | switch (split.length) { 41 | case 2: 42 | if (split[0].charAt(0) !== ':') { 43 | error( 44 | `Invalid nested selector: '${key}'. ` + 45 | 'The left part of a combinator selector must be a pseudo-class eg. `:hover`.' 46 | ); 47 | } 48 | if (split[1] !== '&') { 49 | error( 50 | `Invalid nested selector: '${key}'. ` + 51 | 'The right part of a combinator selector must be `&`.' 52 | ); 53 | } 54 | break; 55 | case 1: 56 | if (split[0].indexOf(' ') > -1) { 57 | error(`Invalid nested selector: ${key}. Complex selectors are not supported.`); 58 | } 59 | break; 60 | default: 61 | error(`Invalid nested selector: ${key}.`); 62 | } 63 | 64 | if (/\[/.test(key)) { 65 | error( 66 | `Invalid selector: ${key}. Cannot use attribute selectors, please use only class selectors.` 67 | ); 68 | } 69 | } 70 | 71 | function error(message) { 72 | throw new Error(`style-sheet: ${message}`); 73 | } 74 | -------------------------------------------------------------------------------- /src/macro/index.js: -------------------------------------------------------------------------------- 1 | const {readFileSync, writeFileSync} = require('fs'); 2 | const {basename, relative} = require('path'); 3 | const {createMacro} = require('babel-plugin-macros'); 4 | const {addSideEffect} = require('@babel/helper-module-imports'); 5 | const TaggedTemplateExpression = require('../babel/visitors/TaggedTemplateExpression'); 6 | const CallExpression = require('../babel/visitors/CallExpression'); 7 | const StyleSheet = require('../StyleSheet'); 8 | 9 | const checkType = (path, type) => { 10 | if (path.parentPath.type !== type) 11 | throw new Error( 12 | `css-zero/macro/${path.node.name} can only be used as ${type}. You tried ${path.parentPath.type}.` 13 | ); 14 | }; 15 | 16 | exports.createCssZeroMacro = () => 17 | createMacro(({references, state, babel}) => { 18 | const {types} = babel; 19 | 20 | const cssRefs = references.css || []; 21 | const stylesRefs = references.styles || []; 22 | 23 | const styleSheet = new StyleSheet(); 24 | 25 | Object.assign(state, {styleSheet}); 26 | 27 | cssRefs.forEach(ref => { 28 | checkType(ref, 'TaggedTemplateExpression'); 29 | TaggedTemplateExpression(ref.parentPath, state, types); 30 | }); 31 | 32 | stylesRefs.forEach(ref => { 33 | checkType(ref, 'CallExpression'); 34 | CallExpression(ref.parentPath, state, types); 35 | }); 36 | 37 | // remove variable declarations of css``; 38 | cssRefs.forEach(ref => { 39 | ref.parentPath.parentPath.remove(); 40 | }); 41 | 42 | // if no styles have been used, there's no more work to do 43 | if (!Object.keys(styleSheet.usage).length) return; 44 | 45 | const filename = state.file.opts.filename; 46 | 47 | // choose a file to save the styles 48 | const outputFilename = relative(process.cwd(), filename.replace(/\.[^.]+$/, '.zero.css')); 49 | 50 | // include this file as an import to the referenced module, so that css-loader can pick it up at bundle-time 51 | addSideEffect(stylesRefs[0], './' + basename(outputFilename)); 52 | 53 | // combine all the used styles 54 | const cssText = styleSheet.toString(); 55 | 56 | // Read the file first to compare the content 57 | // Write the new content only if it's changed 58 | // This will prevent unnecessary reloads 59 | let currentCssText; 60 | 61 | try { 62 | currentCssText = readFileSync(outputFilename, 'utf-8'); 63 | } catch (e) { 64 | // Ignore error 65 | } 66 | 67 | // if the files hasn't changed, nothing more to do 68 | if (currentCssText === cssText) return; 69 | 70 | writeFileSync(outputFilename, cssText); 71 | }); 72 | -------------------------------------------------------------------------------- /src/babel/visitors/CallExpression.js: -------------------------------------------------------------------------------- 1 | const StyleCache = require('../StyleCache'); 2 | const isStyles = require('../utils/isStyles'); 3 | 4 | function CallExpression(path, state, types) { 5 | if (!isStyles(path)) return; 6 | 7 | const cache = new StyleCache(); 8 | const args = path.get('arguments'); 9 | 10 | args.forEach((arg, i) => { 11 | const result = arg.evaluate(); 12 | const {confident, value} = result; 13 | 14 | if (confident && value) { 15 | Object.entries(value).forEach(([key, value]) => cache.addStyle(key, value)); 16 | return; 17 | } 18 | 19 | switch (arg.type) { 20 | case 'LogicalExpression': 21 | if (arg.node.operator !== '&&') 22 | throw arg.buildCodeFrameError( 23 | `Styles argument does not support the ${arg.node.operator} operator with dynamic values.` 24 | ); 25 | const left = path.get(`arguments.${i}.left`); 26 | const right = path.get(`arguments.${i}.right`); 27 | 28 | const valueRight = right.evaluate(); 29 | 30 | if (!valueRight.confident) 31 | throw arg.buildCodeFrameError( 32 | `Styles argument only accepts boolean expressions in the form "{condition} && {css}".` 33 | ); 34 | 35 | Object.entries(valueRight.value).forEach(([key, value]) => 36 | cache.addConditionalStyle(key, value, left.node) 37 | ); 38 | 39 | return; 40 | case 'BooleanLiteral': 41 | case 'NullLiteral': { 42 | return; 43 | } 44 | case 'ConditionalExpression': 45 | default: 46 | return; 47 | } 48 | }); 49 | 50 | state.styleSheet.trackUsage(...cache.getUsedClassNames()); 51 | 52 | const expressions = cache.getConditionalStyles(); 53 | const literals = cache.getStyles().join(' '); 54 | 55 | if (!expressions.length && !literals) { 56 | path.replaceWith(types.stringLiteral('')); 57 | return; 58 | } 59 | 60 | if (!expressions.length) { 61 | path.replaceWith(types.stringLiteral(literals)); 62 | return; 63 | } 64 | 65 | const concat = (left, right) => types.binaryExpression('+', left, right); 66 | 67 | path.replaceWith( 68 | expressions.reduce( 69 | (current, {test, consequent, alternate}) => { 70 | const node = types.conditionalExpression( 71 | test, 72 | types.stringLiteral(consequent), 73 | types.stringLiteral(alternate) 74 | ); 75 | return current.type === 'NullLiteral' 76 | ? node 77 | : concat(concat(node, types.stringLiteral(' ')), current); 78 | }, 79 | !literals ? types.nullLiteral() : types.stringLiteral(literals) 80 | ) 81 | ); 82 | } 83 | 84 | module.exports = CallExpression; 85 | -------------------------------------------------------------------------------- /src/babel/utils/__tests__/validate.test.js: -------------------------------------------------------------------------------- 1 | const validate = require('../validate'); 2 | 3 | test('simple obj pass validation', () => { 4 | expect(() => 5 | validate({ 6 | color: 'red', 7 | }) 8 | ).not.toThrow(); 9 | }); 10 | 11 | test('throws when using important', () => { 12 | expect(() => 13 | validate({ 14 | color: 'red !important', 15 | }) 16 | ).toThrowError(/important/); 17 | }); 18 | 19 | test('nested: throws when grouping selectors', () => { 20 | expect(() => 21 | validate({ 22 | 'a, b': { 23 | color: 'red', 24 | }, 25 | }) 26 | ).toThrowError(/Selectors cannot be grouped/); 27 | }); 28 | 29 | test('nested: pseudo elements work', () => { 30 | expect(() => 31 | validate({ 32 | ':before': { 33 | color: 'red', 34 | }, 35 | }) 36 | ).not.toThrow(); 37 | 38 | expect(() => 39 | validate({ 40 | '::after': { 41 | color: 'red', 42 | }, 43 | }) 44 | ).not.toThrow(); 45 | }); 46 | 47 | test('nested: throws when using an unsupported pseudo-class', () => { 48 | expect(() => 49 | validate({ 50 | '&:matches(.foo)': { 51 | color: 'red', 52 | }, 53 | }) 54 | ).toThrowError(/Detected unsupported pseudo-class/); 55 | }); 56 | 57 | test('nested: the left part of a combinator must be a pseudo-class', () => { 58 | expect(() => 59 | validate({ 60 | 'foo > &': { 61 | color: 'red', 62 | }, 63 | }) 64 | ).toThrowError(/left part of a combinator selector must be a pseudo-class/); 65 | 66 | expect(() => 67 | validate({ 68 | ':hover > &': { 69 | color: 'red', 70 | }, 71 | }) 72 | ).not.toThrow(); 73 | }); 74 | 75 | test('nested: the right part of a combinator must be &', () => { 76 | expect(() => 77 | validate({ 78 | ':hover > foo': { 79 | color: 'red', 80 | }, 81 | }) 82 | ).toThrowError(/right part of a combinator selector must be `&`/); 83 | 84 | expect(() => 85 | validate({ 86 | ':hover > &': { 87 | color: 'red', 88 | }, 89 | }) 90 | ).not.toThrow(); 91 | }); 92 | 93 | test('nested: does not allow nested selectors', () => { 94 | expect(() => 95 | validate({ 96 | 'foo bar': { 97 | color: 'red', 98 | }, 99 | }) 100 | ).toThrowError(/Complex selectors are not supported/); 101 | }); 102 | 103 | test('nested: media queries work', () => { 104 | expect(() => 105 | validate({ 106 | '@media (min-width: 30px)': { 107 | color: 'red', 108 | }, 109 | }) 110 | ).not.toThrow(); 111 | }); 112 | 113 | test('nested: throws with invalid nested inside of media queries', () => { 114 | expect(() => 115 | validate({ 116 | '@media (min-width: 30px)': { 117 | ':hover > foo': { 118 | color: 'red', 119 | }, 120 | }, 121 | }) 122 | ).toThrowError(/right part of a combinator selector must be `&`/); 123 | }); 124 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `yarn build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS-Zero 2 | 3 | ## Features 4 | 5 | - All of the benefits of writing CSS-in-JS, but with **zero runtime code** 6 | - Write your styles with familiar CSS Syntax 7 | - Generates optimized, atomic CSS with no duplicated style rules 8 | - Style resolution based on order of application, rather than the cascade 9 | - Zero config server-side rendering for applications that support CSS Modules 10 | - Easy composition of styles, with property name collisions eliminated via static analysis 11 | - Theme support via CSS variables, allowing the cost of theming to be proportional to the size of the color palette 12 | - Fast parsing of styles, with CSS downloaded and parsed separately from JS. 13 | - Works without JavaScript, as styles are extracted at build-time. 14 | 15 | These benefits are in addition to the more general benefits of using CSS-in-JS: 16 | 17 | - Scoped selectors to avoid accidental collision of styles 18 | - Styles co-located with your component reduces context switching 19 | - Refactor with confidence when changing/removing styles 20 | - Detect unused styles with EsLint, just like normal JS variables 21 | - Declarative dynamic styling with React 22 | 23 | ## Installation 24 | 25 | Since CSS-Zero has no runtime, it can be installed purely as a devDependency: 26 | 27 | ``` 28 | npm install css-zero --save-dev 29 | ``` 30 | 31 | ## Setup 32 | 33 | The simplest way to run CSS-Zero in a React application is using our Babel Macro: 34 | 35 | ```jsx 36 | import {css, styled} from 'css-zero/macro'; 37 | ``` 38 | 39 | For applications created using Create React App (which supports both Babel Macros and CSS Modules out-of-the-box), no further setup or configuration is needed. 40 | 41 | For usage with other front-end frameworks, CSS-Zero can be set up with our babel-plugin. 42 | 43 | ## Syntax 44 | 45 | The basic usage of CSS-Zero looks like this: 46 | 47 | ```jsx 48 | import {css, styles} from 'css-zero'; 49 | 50 | // Write your styles using the `css` tag 51 | const blue = css` 52 | color: blue; 53 | `; 54 | 55 | const base = css` 56 | color: red; 57 | font-size: 16px; 58 | `; 59 | 60 | // then use the `styles` helper to compose your styles and generate class names 61 | export default props =>
; 62 | 63 | ↓ ↓ ↓ ↓ ↓ ↓ Compiles to ↓ ↓ ↓ ↓ ↓ ↓ 64 | 65 | export default props =>
66 | 67 | // along with a the following .zero.css file: 68 | .x1vong5g {color:blue} 69 | .x1dqz7z3 {color:red} 70 | .x1e4w2a9 {font-size:16px} 71 | ``` 72 | 73 | ## Demo 74 | 75 | [![Edit CSS-Zero Create React App Example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/hello-world-ogzzo?fontsize=14&hidenavigation=1&theme=dark) 76 | 77 | ## Inspiration 78 | 79 | - [emotion](https://emotion.sh/) 80 | - [linaria](https://github.com/callstack/linaria) 81 | - [style-sheet](https://github.com/giuseppeg/style-sheet) 82 | - [Facebook stylex](https://www.youtube.com/watch?v=9JZHodNR184&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=3) 83 | - [object-style](https://github.com/jxnblk/object-style) 84 | -------------------------------------------------------------------------------- /src/babel/visitors/TaggedTemplateExpression.js: -------------------------------------------------------------------------------- 1 | const atomizer = require('../utils/atomizer'); 2 | const hasImport = require('../utils/hasImport'); 3 | const evaluate = require('../utils/evaluate'); 4 | 5 | function TaggedTemplateExpression(path, state, types) { 6 | const {quasi, tag} = path.node; 7 | 8 | let css; 9 | 10 | if ( 11 | hasImport(types, path.scope, state.file.opts.filename, 'css', 'css-zero') || 12 | hasImport(types, path.scope, state.file.opts.filename, 'css', 'css-zero/macro') || 13 | hasImport(types, path.scope, state.file.opts.filename, 'css', '../../../macro') 14 | ) { 15 | css = types.isIdentifier(tag) && tag.name === 'css'; 16 | } 17 | 18 | if (!css) { 19 | return; 20 | } 21 | 22 | const parent = path.findParent( 23 | p => types.isObjectProperty(p) || types.isJSXOpeningElement(p) || types.isVariableDeclarator(p) 24 | ); 25 | 26 | // Check if the variable is referenced anywhere for basic dead code elimination 27 | // Only works when it's assigned to a variable 28 | if (parent && types.isVariableDeclarator(parent)) { 29 | const {referencePaths} = path.scope.getBinding(parent.node.id.name); 30 | 31 | if (referencePaths.length === 0) { 32 | path.remove(); 33 | return; 34 | } 35 | } 36 | 37 | // Serialize the tagged template literal to a string 38 | let cssText = ''; 39 | 40 | const expressions = path.get('quasi').get('expressions'); 41 | 42 | quasi.quasis.forEach((el, i) => { 43 | cssText += el.value.cooked; 44 | 45 | const ex = expressions[i]; 46 | 47 | if (!ex) return; 48 | 49 | const result = ex.evaluate(); 50 | 51 | if (result.confident) { 52 | throwIfInvalid(result.value, ex); 53 | 54 | cssText += result.value; 55 | } else { 56 | // The value may be an imported variable, so try to preval the value 57 | if (types.isFunctionExpression(ex) || types.isArrowFunctionExpression(ex)) return; 58 | 59 | let evaluation; 60 | 61 | try { 62 | evaluation = evaluate(ex, types, state.file.opts.filename); 63 | } catch (e) { 64 | throw ex.buildCodeFrameError( 65 | `An error occurred when evaluating the expression: ${e.message}. Make sure you are not using a browser or Node specific API.` 66 | ); 67 | } 68 | 69 | const {value} = evaluation; 70 | 71 | throwIfInvalid(value, ex); 72 | 73 | cssText += value; 74 | } 75 | }); 76 | 77 | const rules = atomizer(cssText); 78 | 79 | rules.forEach(([className, {selector, cssText, media}]) => { 80 | state.styleSheet.addRule({ 81 | className, 82 | selector, 83 | cssText, 84 | media, 85 | }); 86 | }); 87 | 88 | // replace initial template expression with 89 | path.replaceWith( 90 | types.objectExpression( 91 | rules.map(([className, {key}]) => 92 | types.objectProperty(types.stringLiteral(key), types.stringLiteral(className)) 93 | ) 94 | ) 95 | ); 96 | } 97 | 98 | function throwIfInvalid(value, ex) { 99 | if (typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))) { 100 | return; 101 | } 102 | 103 | const stringified = typeof value === 'object' ? JSON.stringify(value) : String(value); 104 | 105 | throw ex.buildCodeFrameError( 106 | `The expression evaluated to '${stringified}', which is probably a mistake. If you want it to be inserted into CSS, explicitly cast or transform the value to a string, e.g. - 'String(${ 107 | generator(ex.node).code 108 | })'.` 109 | ); 110 | } 111 | 112 | module.exports = TaggedTemplateExpression; 113 | -------------------------------------------------------------------------------- /src/babel/__tests__/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`combining.jsx 1`] = `"export const Component = () =>
;"`; 4 | 5 | exports[`combining.jsx 2`] = ` 6 | .x1dqz7z3 {color:red} 7 | .x1e4w2a9 {font-size:16px} 8 | `; 9 | 10 | exports[`conditional.jsx 1`] = ` 11 | "export const LogicalAndExpression = props =>
; 12 | export const LogicalExpressionDeterministic = () =>
; 13 | export const IgnoreFalsey = () =>
; 14 | export const SimpleTernaryExpression = () =>
;" 15 | `; 16 | 17 | exports[`conditional.jsx 2`] = ` 18 | .x1vong5g {color:blue} 19 | .x1dqz7z3 {color:red} 20 | .x1e4w2a9 {font-size:16px} 21 | `; 22 | 23 | exports[`conditional-with-dce.jsx 1`] = `"export const LogicalAndExpression = props =>
;"`; 24 | 25 | exports[`conditional-with-dce.jsx 2`] = ` 26 | .x1vong5g {color:blue} 27 | .x1dqz7z3 {color:red} 28 | .x1e4w2a9 {font-size:16px} 29 | `; 30 | 31 | exports[`extraction.jsx 1`] = ` 32 | "export const ComponentStatic = () =>
; 33 | export const ComponentStaticInline = () =>
; 34 | export const ComponentConstant = () =>
; 35 | export const ComponentConstantImported = () =>
;" 36 | `; 37 | 38 | exports[`extraction.jsx 2`] = ` 39 | .x1yl4wq1 {margin-bottom:2px} 40 | .xfehp64 {margin-top:10px} 41 | .x2c67um {margin-bottom:12px} 42 | .x1dk9diw {margin-bottom:1px} 43 | `; 44 | 45 | exports[`media-query.jsx 1`] = ` 46 | "export const Component = () =>
; 47 | export const ComponentWithDuplicateMediaStyle = () =>
; 48 | export const ComponentWithOverriddenMediaStyle = () =>
; 49 | export const ComponentWithNestedPseudo = () =>
; 50 | export const ComponentWithParentPseudo = () =>
;" 51 | `; 52 | 53 | exports[`media-query.jsx 2`] = ` 54 | .x1dqz7z3 {color:red} 55 | .x1ug2cuz {color:black} 56 | .x17bvl1g {color:brown} 57 | .xfxv4bd {color:silver} 58 | .xashdej {color:wheat} 59 | @media screen and (min-width:678px){ 60 | .xr3sc1d {color:green} 61 | .x1rt9ji8 {color:yellow} 62 | .x1vx1kxu {color:salmon} 63 | .x1p5ktpm:hover {color:gold} 64 | .x4fean5:hover {color:sandybrown} 65 | } 66 | `; 67 | 68 | exports[`merging.jsx 1`] = `"export const Component = () =>
;"`; 69 | 70 | exports[`merging.jsx 2`] = `.xp5623v {color:green}`; 71 | 72 | exports[`pseudo.jsx 1`] = ` 73 | "export const Component = () =>
; 74 | export const ComponentWithDuplicatePseudoStyle = () =>
; 75 | export const ComponentWithOverriddenPseudoStyle = () =>
; 76 | export const ComponentWithImplicitPseudoStyle = () =>
; 77 | export const ComponentWithPseudoElementStyle = () =>
;" 78 | `; 79 | 80 | exports[`pseudo.jsx 2`] = ` 81 | .x1dqz7z3 {color:red} 82 | .x1jz5mb:hover {color:green} 83 | .x1ug2cuz {color:black} 84 | .x17vggq8:hover {color:yellow} 85 | .x17bvl1g {color:brown} 86 | .xjhh7o6:hover {color:salmon} 87 | .xd095qv:hover {color:gray} 88 | .xwc7jr3::before {content:''} 89 | .xaxzk95::before {position:relative} 90 | `; 91 | 92 | exports[`simple.jsx 1`] = `"export const Component = () =>
;"`; 93 | 94 | exports[`simple.jsx 2`] = `.x1e4w2a9 {font-size:16px}`; 95 | -------------------------------------------------------------------------------- /example/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/babel/utils/evaluate.js: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/callstack/linaria 2 | // which is MIT (c) callstack 3 | const generator = require('@babel/generator').default; 4 | const babel = require('@babel/core'); 5 | const Module = require('./module'); 6 | 7 | const isAdded = (requirements, path) => { 8 | if (requirements.some(req => req.path === path)) { 9 | return true; 10 | } 11 | 12 | if (path.parentPath) { 13 | return isAdded(requirements, path.parentPath); 14 | } 15 | 16 | return false; 17 | }; 18 | 19 | const resolve = (path, t, requirements) => { 20 | const binding = path.scope.getBinding(path.node.name); 21 | 22 | if ( 23 | path.isReferenced() && 24 | binding && 25 | binding.kind !== 'param' && 26 | !isAdded(requirements, binding.path) 27 | ) { 28 | let result; 29 | 30 | switch (binding.kind) { 31 | case 'module': 32 | if (t.isImportSpecifier(binding.path)) { 33 | result = t.importDeclaration([binding.path.node], binding.path.parentPath.node.source); 34 | } else { 35 | result = binding.path.parentPath.node; 36 | } 37 | break; 38 | case 'const': 39 | case 'let': 40 | case 'var': { 41 | let decl; 42 | 43 | // Replace SequenceExpressions (expr1, expr2, expr3, ...) with the last one 44 | if (t.isSequenceExpression(binding.path.node.init)) { 45 | const {node} = binding.path; 46 | 47 | decl = t.variableDeclarator( 48 | node.id, 49 | node.init.expressions[node.init.expressions.length - 1] 50 | ); 51 | } else { 52 | decl = binding.path.node; 53 | } 54 | 55 | result = t.variableDeclaration(binding.kind, [decl]); 56 | break; 57 | } 58 | default: 59 | result = binding.path.node; 60 | break; 61 | } 62 | 63 | const {loc} = binding.path.node; 64 | 65 | requirements.push({ 66 | result, 67 | path: binding.path, 68 | start: loc.start, 69 | end: loc.end, 70 | }); 71 | 72 | binding.path.traverse({ 73 | Identifier(p) { 74 | resolve(p, t, requirements); 75 | }, 76 | }); 77 | } 78 | }; 79 | 80 | module.exports = function evaluate(path, t, filename, transformer, options) { 81 | if (t.isSequenceExpression(path)) { 82 | // We only need to evaluate the last item in a sequence expression, e.g. (a, b, c) 83 | // eslint-disable-next-line no-param-reassign 84 | path = path.get('expressions')[path.node.expressions.length - 1]; 85 | } 86 | 87 | const requirements = []; 88 | 89 | if (t.isIdentifier(path)) { 90 | resolve(path, t, requirements); 91 | } else { 92 | path.traverse({ 93 | Identifier(p) { 94 | resolve(p, t, requirements); 95 | }, 96 | }); 97 | } 98 | 99 | const expression = t.expressionStatement( 100 | t.assignmentExpression( 101 | '=', 102 | t.memberExpression(t.identifier('module'), t.identifier('exports')), 103 | path.node 104 | ) 105 | ); 106 | 107 | // Preserve source order 108 | requirements.sort((a, b) => { 109 | if (a.start.line === b.start.line) { 110 | return a.start.column - b.start.column; 111 | } 112 | 113 | return a.start.line - b.start.line; 114 | }); 115 | 116 | // We'll wrap each code in a block to avoid collisions in variable names 117 | // We separate out the imports since they cannot be inside blocks 118 | const {imports, others} = requirements.reduce( 119 | (acc, curr) => { 120 | if (t.isImportDeclaration(curr.path.parentPath)) { 121 | acc.imports.push(curr.result); 122 | } else { 123 | // Add these in reverse because we'll need to wrap in block statements in reverse 124 | acc.others.unshift(curr.result); 125 | } 126 | 127 | return acc; 128 | }, 129 | {imports: [], others: []} 130 | ); 131 | 132 | const wrapped = others.reduce( 133 | (acc, curr) => t.blockStatement([curr, acc]), 134 | t.blockStatement([expression]) 135 | ); 136 | 137 | const m = new Module(filename); 138 | 139 | m.dependencies = []; 140 | m.transform = 141 | typeof transformer !== 'undefined' 142 | ? transformer 143 | : function transform(text) { 144 | if (options && options.ignore && options.ignore.test(this.filename)) { 145 | return {code: text}; 146 | } 147 | 148 | const plugins = [ 149 | // Include these plugins to avoid extra config when using { module: false } for webpack 150 | '@babel/plugin-transform-modules-commonjs', 151 | '@babel/plugin-proposal-export-namespace-from', 152 | ]; 153 | 154 | const defaults = { 155 | caller: {name: 'css-zero', evaluate: true}, 156 | filename: this.filename, 157 | plugins: [ 158 | ...plugins.map(name => require.resolve(name)), 159 | // We don't support dynamic imports when evaluating, but don't wanna syntax error 160 | // This will replace dynamic imports with an object that does nothing 161 | require.resolve('./dynamic-import-noop'), 162 | ], 163 | }; 164 | 165 | return babel.transformSync(text, defaults); 166 | }; 167 | 168 | m.evaluate( 169 | [ 170 | // Use String.raw to preserve escapes such as '\n' in the code 171 | // Flow doesn't understand template tags: https://github.com/facebook/flow/issues/2616 172 | /* $FlowFixMe */ 173 | imports.map(node => String.raw`${generator(node).code}`).join('\n'), 174 | /* $FlowFixMe */ 175 | String.raw`${generator(wrapped).code}`, 176 | ].join('\n') 177 | ); 178 | 179 | return { 180 | value: m.exports, 181 | dependencies: m.dependencies, 182 | }; 183 | }; 184 | -------------------------------------------------------------------------------- /src/babel/utils/module.js: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/callstack/linaria 2 | // which is MIT (c) callstack 3 | const NativeModule = require('module'); 4 | const vm = require('vm'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const process = require('./process'); 8 | 9 | // Supported node builtins based on the modules polyfilled by webpack 10 | // `true` means module is polyfilled, `false` means module is empty 11 | const builtins = { 12 | assert: true, 13 | buffer: true, 14 | child_process: false, 15 | cluster: false, 16 | console: true, 17 | constants: true, 18 | crypto: true, 19 | dgram: false, 20 | dns: false, 21 | domain: true, 22 | events: true, 23 | fs: false, 24 | http: true, 25 | https: true, 26 | module: false, 27 | net: false, 28 | os: true, 29 | path: true, 30 | punycode: true, 31 | process: true, 32 | querystring: true, 33 | readline: false, 34 | repl: false, 35 | stream: true, 36 | string_decoder: true, 37 | sys: true, 38 | timers: true, 39 | tls: false, 40 | tty: true, 41 | url: true, 42 | util: true, 43 | vm: true, 44 | zlib: true, 45 | }; 46 | 47 | // Separate cache for evaled modules 48 | let cache = {}; 49 | 50 | const NOOP = () => {}; 51 | 52 | class Module { 53 | constructor(filename) { 54 | Object.defineProperties(this, { 55 | id: { 56 | value: filename, 57 | writable: false, 58 | }, 59 | filename: { 60 | value: filename, 61 | writable: false, 62 | }, 63 | paths: { 64 | value: Object.freeze(NativeModule._nodeModulePaths(path.dirname(filename))), 65 | writable: false, 66 | }, 67 | }); 68 | 69 | this.exports = {}; 70 | this.require = this.require.bind(this); 71 | this.require.resolve = this.resolve.bind(this); 72 | this.require.ensure = NOOP; 73 | this.require.cache = cache; 74 | 75 | // We support following extensions by default 76 | this.extensions = ['.json', '.js', '.jsx', '.ts', '.tsx']; 77 | } 78 | 79 | resolve(id) { 80 | const extensions = NativeModule._extensions; 81 | const added = []; 82 | 83 | try { 84 | // Check for supported extensions 85 | this.extensions.forEach(ext => { 86 | if (ext in extensions) { 87 | return; 88 | } 89 | 90 | // When an extension is not supported, add it 91 | // And keep track of it to clean it up after resolving 92 | // Use noop for the tranform function since we handle it 93 | extensions[ext] = NOOP; 94 | added.push(ext); 95 | }); 96 | 97 | return Module._resolveFilename(id, this); 98 | } finally { 99 | // Cleanup the extensions we added to restore previous behaviour 100 | added.forEach(ext => delete extensions[ext]); 101 | } 102 | } 103 | 104 | require(id) { 105 | if (id in builtins) { 106 | // The module is in the allowed list of builtin node modules 107 | // Ideally we should prevent importing them, but webpack polyfills some 108 | // So we check for the list of polyfills to determine which ones to support 109 | if (builtins[id]) { 110 | /* $FlowFixMe */ 111 | return require(id); 112 | } 113 | 114 | return null; 115 | } 116 | 117 | // Resolve module id (and filename) relatively to parent module 118 | const filename = this.resolve(id); 119 | 120 | if (filename === id && !path.isAbsolute(id)) { 121 | // The module is a builtin node modules, but not in the allowed list 122 | throw new Error( 123 | `Unable to import "${id}". Importing Node builtins is not supported in the sandbox.` 124 | ); 125 | } 126 | 127 | this.dependencies && this.dependencies.push(id); 128 | 129 | let m = cache[filename]; 130 | 131 | if (!m) { 132 | // Create the module if cached module is not available 133 | m = new Module(filename); 134 | m.transform = this.transform; 135 | 136 | // Store it in cache at this point with, otherwise 137 | // we would end up in infinite loop with cyclic dependencies 138 | cache[filename] = m; 139 | 140 | if (this.extensions.includes(path.extname(filename))) { 141 | // To evaluate the file, we need to read it first 142 | const code = fs.readFileSync(filename, 'utf-8'); 143 | 144 | if (/\.json$/.test(filename)) { 145 | // For JSON files, parse it to a JS object similar to Node 146 | m.exports = JSON.parse(code); 147 | } else { 148 | // For JS/TS files, evaluate the module 149 | // The module will be transpiled using provided transform 150 | m.evaluate(code); 151 | } 152 | } else { 153 | // For non JS/JSON requires, just export the id 154 | // This is to support importing assets in webpack 155 | // The module will be resolved by css-loader 156 | m.exports = id; 157 | } 158 | } 159 | 160 | return m.exports; 161 | } 162 | 163 | evaluate(text) { 164 | // For JavaScript files, we need to transpile it and to get the exports of the module 165 | const code = this.transform ? this.transform(text).code : text; 166 | 167 | const script = new vm.Script(code, { 168 | filename: this.filename, 169 | }); 170 | 171 | script.runInContext( 172 | vm.createContext({ 173 | global, 174 | process, 175 | module: this, 176 | exports: this.exports, 177 | require: this.require, 178 | __filename: this.filename, 179 | __dirname: path.dirname(this.filename), 180 | }) 181 | ); 182 | } 183 | } 184 | 185 | Module.invalidate = () => { 186 | cache = {}; 187 | }; 188 | 189 | // Alias to resolve the module using node's resolve algorithm 190 | // This static property can be overriden by the webpack loader 191 | // This allows us to use webpack's module resolution algorithm 192 | Module._resolveFilename = (id, options) => NativeModule._resolveFilename(id, options); 193 | 194 | module.exports = Module; 195 | -------------------------------------------------------------------------------- /example/src/logo.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------