Skip to content

Commit

Permalink
feat: add css prop and clean up testing (#308)
Browse files Browse the repository at this point in the history
* add css prop and clean up testing

* snaps

* fix snaps
  • Loading branch information
jquense committed Jul 19, 2019
1 parent ad1c883 commit fa6e209
Show file tree
Hide file tree
Showing 107 changed files with 2,146 additions and 1,589 deletions.
2 changes: 1 addition & 1 deletion .eslintignore
@@ -1,4 +1,4 @@
lib/
node_modules/
test/fixtures/
test/build/
__file_snapshots__/
1 change: 0 additions & 1 deletion .gitignore
@@ -1,5 +1,4 @@
node_modules
lib
test/build
*_extracted_style.css
*.log
3 changes: 3 additions & 0 deletions .prettierignore
@@ -0,0 +1,3 @@
node_modules/
test/fixtures/
__file_snapshots__/
31 changes: 31 additions & 0 deletions README.md
Expand Up @@ -13,6 +13,7 @@
- [Usage](#usage)
- [Extensions](#extensions)
- [Component API](#component-api)
- [`css` prop](#css-prop)
- [Component API Goals and Non-Goals](#component-api-goals-and-non-goals)
- [Composition, variables, etc?](#composition-variables-etc)
- [Referring to other Components](#referring-to-other-components)
Expand Down Expand Up @@ -145,6 +146,36 @@ function Button({ primary, color, className, ...props }) {
}
```

## `css` prop

In addition to the `styled` helper, styles can be defined directly on components via the `css` prop.

```jsx
function Button({ variant, children }) {
return (
<button
variant={variant}
css={css`
color: black;
border: 1px solid black;
background-color: white;
&.variant-primary {
color: blue;
border: 1px solid blue;
}
&.variant-secondary {
color: green;
}
`}
>
children
</button>
);
}
```

Styles are still extracted to a separate file, any props matching other defined classes are passed to the `classNames()` library. At runtime `styled()` returns a React component with the static CSS classes applied. You can check out the ["runtime"](https://github.com/4Catalyzer/astroturf/blob/master/src/runtime/styled.js#L16) it just creates a component.

There are a whole bucket of caveats of course, to keep the above statically extractable, and limit runtime code.
Expand Down
20 changes: 14 additions & 6 deletions package.json
Expand Up @@ -24,7 +24,7 @@
}
},
"lint-staged": {
"*.js": [
"!(__file_snapshots__/)*.js": [
"eslint --fix",
"yarn 4c format",
"git add"
Expand All @@ -45,6 +45,10 @@
],
"setupFilesAfterEnv": [
"./test/setup.js"
],
"watchPathIgnorePatterns": [
"build",
"__file_snapshots__"
]
},
"release": {
Expand All @@ -57,16 +61,18 @@
},
"homepage": "https://github.com/4Catalyzer/astroturf#readme",
"dependencies": {
"@babel/core": "^7.3.4",
"@babel/core": "^7.5.5",
"@babel/generator": "^7.3.4",
"@babel/helper-module-imports": "^7.0.0",
"@babel/template": "^7.2.2",
"@babel/types": "^7.3.4",
"chalk": "^2.4.2",
"common-tags": "^1.8.0",
"css-loader": "^3.0.0",
"errno": "^0.1.7",
"fs-extra": "^8.0.0",
"loader-utils": "^1.2.3",
"lodash": "^4.17.11",
"lodash": "^4.17.15",
"memory-fs": "^0.4.1",
"postcss-loader": "^3.0.0",
"postcss-nested": "^4.1.1"
Expand All @@ -79,7 +85,10 @@
"@4c/cli": "^0.8.1",
"@babel/cli": "^7.2.3",
"@babel/node": "^7.2.2",
"@types/react": "^16.8.5",
"@babel/plugin-transform-react-jsx": "^7.3.0",
"@babel/preset-react": "^7.0.0",
"@types/react": "^16.8.23",
"@types/react-dom": "^16.8.4",
"babel-core": "^7.0.0-0",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.1.0",
Expand All @@ -100,14 +109,13 @@
"eslint-plugin-react": "^7.12.4",
"html-webpack-plugin": "^3.2.0",
"husky": "^3.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.1.0",
"jest-file-snapshot": "^0.3.6",
"lint-staged": "^9.0.0",
"mini-css-extract-plugin": "^0.8.0",
"prettier": "^1.16.4",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"recompose": "^0.30.0",
"rimraf": "^2.6.3",
"style-loader": "^0.23.1",
"webpack": "^4.36.1",
Expand Down
16 changes: 11 additions & 5 deletions src/loader.js
Expand Up @@ -28,14 +28,17 @@ AstroturfLoaderError.prototype.constructor = AstroturfLoaderError;
function collectStyles(src, filename, opts) {
const tagName = opts.tagName || 'css';
const styledTag = opts.styledTag || 'styled';

// quick regex as an optimization to avoid parsing each file
if (
!src.match(
new RegExp(
`(${tagName}|${styledTag}(.|\\n|\\r)+?)\\s*\`([\\s\\S]*?)\``,
'gmi',
),
)
) &&
opts.cssPropEnabled &&
!src.match(/css=("|')/g)
) {
return { styles: [] };
}
Expand All @@ -56,7 +59,7 @@ function collectStyles(src, filename, opts) {
function replaceStyleTemplates(src, locations) {
let offset = 0;

function splice(str, start, end, replace) {
function splice(str, start = 0, end = 0, replace) {
const result =
str.slice(0, start + offset) + replace + str.slice(end + offset);

Expand All @@ -74,16 +77,19 @@ function replaceStyleTemplates(src, locations) {

const LOADER_PLUGIN = Symbol('loader added VM plugin');

module.exports = function loader(content) {
module.exports = function loader(content, _, meta) {
if (this.cacheable) this.cacheable();

const options = loaderUtils.getOptions(this) || {};
const { styles = [], imports } = collectStyles(
const { styles = [], changeset } = collectStyles(
content,
this.resourcePath,
options,
);

if (meta) {
meta.styles = styles;
}
if (!styles.length) return content;

let { emitVirtualFile } = this;
Expand All @@ -104,5 +110,5 @@ module.exports = function loader(content) {
emitVirtualFile(style.absoluteFilePath, style.value);
});

return replaceStyleTemplates(content, [...imports, ...styles]);
return replaceStyleTemplates(content, changeset);
};
128 changes: 116 additions & 12 deletions src/plugin.js
Expand Up @@ -3,10 +3,12 @@ import { stripIndent } from 'common-tags';
import { outputFileSync } from 'fs-extra';
import defaults from 'lodash/defaults';
import get from 'lodash/get';
import { addNamed, addDefault } from '@babel/helper-module-imports';
import generate from '@babel/generator';
import template from '@babel/template';
import * as t from '@babel/types';

import chalk from 'chalk';
import buildTaggedTemplate from './utils/buildTaggedTemplate';
import getNameFromPath from './utils/getNameFromPath';
import normalizeAttrs from './utils/normalizeAttrs';
Expand All @@ -25,6 +27,11 @@ const buildComponent = template(
const STYLES = Symbol('Astroturf');
const COMPONENTS = Symbol('Astroturf components');
const IMPORTS = Symbol('Astroturf imports');
const HAS_CSS_PROP = Symbol('Astroturf has css prop');

const JSX_IDENTIFIER = '_AstroTurfJsx';
const PRAGMA_BODY = `* @jsx ${JSX_IDENTIFIER} *`;
const FRAG_PRAGMA_BODY = '* @jsxFrag React.Fragment *';

function getNameFromFile(fileName) {
const name = basename(fileName, extname(fileName));
Expand Down Expand Up @@ -125,7 +132,7 @@ export default function plugin() {
style.code = `require('${style.relativeFilePath}')`;

const { text, imports } = buildTaggedTemplate(
path,
path.get('quasi'),
nodeMap,
style,
opts.pluginOptions,
Expand Down Expand Up @@ -168,7 +175,7 @@ export default function plugin() {
style.isStyledComponent = true;

const { text, imports } = buildTaggedTemplate(
path,
path.get('quasi'),
nodeMap,
style,
opts.pluginOptions,
Expand Down Expand Up @@ -201,6 +208,7 @@ export default function plugin() {
if (!file.has(STYLES)) {
file.set(STYLES, {
id: 0,
changeset: [],
styles: new Map(),
});
}
Expand All @@ -212,8 +220,8 @@ export default function plugin() {

post(file) {
const { opts } = this;
let { styles, changeset } = file.get(STYLES);
const importNodes = file.get(IMPORTS);
const imports = [];

importNodes.forEach(path => {
const decl = !path.isImportDeclaration()
Expand All @@ -227,7 +235,7 @@ export default function plugin() {
path.remove();

if (opts.generateInterpolations)
imports.push({
changeset.push({
start,
end,
// if the path is just a removed specifier we need to regenerate
Expand All @@ -236,10 +244,11 @@ export default function plugin() {
});
});

let { styles } = file.get(STYLES);
styles = Array.from(styles.values());

file.metadata.astroturf = { styles, imports };
changeset = changeset.concat(styles);

file.metadata.astroturf = { styles, changeset };

if (opts.writeFiles !== false) {
styles.forEach(({ absoluteFilePath, value }) => {
Expand All @@ -249,13 +258,108 @@ export default function plugin() {
},

visitor: {
TaggedTemplateExpression(path, state) {
const pluginOptions = defaults(state.opts, {
tagName: 'css',
allowGlobal: true,
styledTag: 'styled',
Program: {
enter(_, state) {
state.defaultedOptions = defaults(state.opts, {
tagName: 'css',
allowGlobal: true,
styledTag: 'styled',
});
},
exit(path, state) {
if (!state.file.get(HAS_CSS_PROP)) return;

addNamed(path, 'jsx', 'astroturf', { nameHint: JSX_IDENTIFIER });
path.addComment('leading', PRAGMA_BODY);
path.addComment('leading', FRAG_PRAGMA_BODY);

state.file.get(STYLES).changeset.unshift(
{ code: `/*${PRAGMA_BODY}*/\n` },
{ code: `/*${FRAG_PRAGMA_BODY}*/\n\n` },
{
code: `const { jsx: ${JSX_IDENTIFIER} } = require('astroturf');\n`,
},
);
},
},

JSXAttribute(path, state) {
const { file } = state;
const pluginOptions = state.defaultedOptions;
const cssState = file.get(STYLES);
const nodeMap = file.get(COMPONENTS);

if (path.node.name.name !== 'css') return;

if (!pluginOptions.enableCssProp) {
if (!pluginOptions.noWarnings)
// eslint-disable-next-line no-console
console.warn(
chalk.yellow(
'It looks like you are trying to use the css prop with',
chalk.bold('astroturf'),
'but have not enabled it. add',
chalk.bold('enableCssProp: true'),
'to the loader or plugin options to compile the css prop.',
),
);
return;
}

const valuePath = path.get('value');
const displayName = `CssProp${++cssState.id}_${getNameFromPath(
path.findParent(p => p.isJSXOpeningElement).get('name'),
)}`;

const style = createStyleNode(valuePath, displayName, {
pluginOptions,
file: state.file,
});

if (valuePath.isStringLiteral()) {
style.value = wrapInClass(path.node.value.value);
} else if (valuePath.isJSXExpressionContainer()) {
const exprPath = valuePath.get('expression');

if (
exprPath.isTemplateLiteral() ||
(exprPath.isTaggedTemplateExpression() &&
isCssTag(exprPath.get('tag'), pluginOptions))
) {
const { text, imports } = buildTaggedTemplate(
exprPath.isTemplateLiteral() ? exprPath : exprPath.get('quasi'),
nodeMap,
style,
pluginOptions,
);
style.value = imports + wrapInClass(text);
}
}

if (style.value == null) return;

const runtimeNode = t.jsxExpressionContainer(
addDefault(valuePath, style.relativeFilePath),
);

cssState.styles.set(style.absoluteFilePath, style);

if (pluginOptions.generateInterpolations)
style.code = `{${runtimeNode.expression.name}}`;

cssState.changeset.push({
code: `const ${runtimeNode.expression.name} = require('${style.relativeFilePath}');\n`,
});

nodeMap.set(runtimeNode.expression, style);

valuePath.replaceWith(runtimeNode);
file.set(HAS_CSS_PROP, true);
},

TaggedTemplateExpression(path, state) {
const pluginOptions = state.defaultedOptions;

const tagPath = path.get('tag');

if (isStyledTag(tagPath, pluginOptions)) {
Expand Down Expand Up @@ -310,7 +414,7 @@ export default function plugin() {

ImportDeclaration: {
exit(path, state) {
const { tagName = 'css' } = state.opts;
const { tagName } = state.defaultedOptions;
const specifiers = path.get('specifiers');
const tagImport = path
.get('specifiers')
Expand Down

0 comments on commit fa6e209

Please sign in to comment.