├── 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 |
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 | [](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 |
--------------------------------------------------------------------------------