Skip to content

Commit

Permalink
feat: find css props when using createElement (#431)
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense authored Oct 8, 2019
1 parent 81e5727 commit c82a67e
Show file tree
Hide file tree
Showing 25 changed files with 654 additions and 399 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@babel/generator": "^7.3.4",
"@babel/helper-module-imports": "^7.0.0",
"@babel/template": "^7.2.2",
"@babel/traverse": "^7.6.2",
"@babel/types": "^7.3.4",
"chalk": "^2.4.2",
"common-tags": "^1.8.0",
Expand Down
213 changes: 213 additions & 0 deletions src/features/css-prop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import chalk from 'chalk';
import generate from '@babel/generator';
import { addDefault, addNamed } from '@babel/helper-module-imports';
import * as t from '@babel/types';

import buildTaggedTemplate from '../utils/buildTaggedTemplate';
import createStyleNode from '../utils/createStyleNode';
import getNameFromPath from '../utils/getNameFromPath';
import isCssTag from '../utils/isCssTag';
import { COMPONENTS, HAS_CSS_PROP, STYLES } from '../utils/Symbols';
import toVarsArray from '../utils/toVarsArray';
import trimExpressions from '../utils/trimExpressions';
import wrapInClass from '../utils/wrapInClass';

const JSX_IDENTS = Symbol('Astroturf jsx identifiers');

const isCreateElementCall = p =>
p.isCallExpression() &&
p.get('callee.property').node &&
p.get('callee.property').node.name === 'createElement';

function buildCssProp(valuePath, name, options) {
const { file, pluginOptions, isJsx } = options;
const cssState = file.get(STYLES);
const nodeMap = file.get(COMPONENTS);

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 null;
}

const displayName = `CssProp${++cssState.id}_${name}`;

let vars;
const style = createStyleNode(valuePath, displayName, {
file,
pluginOptions,
});

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

if (
exprPath.isTemplateLiteral() ||
(exprPath.isTaggedTemplateExpression() &&
isCssTag(exprPath.get('tag'), pluginOptions))
) {
const { text, imports, dynamicInterpolations } = buildTaggedTemplate({
style,
nodeMap,
...pluginOptions,
quasiPath: exprPath.isTemplateLiteral()
? exprPath
: exprPath.get('quasi'),
useCssProperties: !!pluginOptions.customCssProperties,
});

vars = toVarsArray(dynamicInterpolations);

style.imports = imports;
style.interpolations = trimExpressions(dynamicInterpolations);
style.value = imports + wrapInClass(text);
}
}

if (style.value == null) {
return null;
}

const importId = addDefault(valuePath, style.relativeFilePath);
let runtimeNode = t.arrayExpression([importId, vars].filter(Boolean));

nodeMap.set(runtimeNode.expression, style);

if (isJsx) {
runtimeNode = t.jsxExpressionContainer(runtimeNode);
}

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

if (pluginOptions.generateInterpolations)
style.code = generate(runtimeNode).code;

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

return runtimeNode;
}

const cssPropertyVisitors = {
ObjectProperty(path, state) {
const { file, pluginOptions, typeName } = state;

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

const valuePath = path.get('value');

const compiledNode = buildCssProp(valuePath, typeName, {
file,
pluginOptions,
});

if (compiledNode) {
valuePath.replaceWith(compiledNode);
state.processed = true;
}
},
};

export default {
Program: {
enter(path, state) {
// We need to re-export Fragment because of
// https://github.com/babel/babel/pull/7996#issuecomment-519653431
state[JSX_IDENTS] = {
jsx: path.scope.generateUidIdentifier('j'),
jsxFrag: path.scope.generateUidIdentifier('f'),
};
},

exit(path, state) {
if (!state.file.get(HAS_CSS_PROP)) return;

const { jsx, jsxFrag } = state[JSX_IDENTS];

const jsxPrgama = `* @jsx ${jsx.name} *`;
const jsxFragPrgama = `* @jsxFrag ${jsxFrag.name} *`;

path.addComment('leading', jsxPrgama);
path.addComment('leading', jsxFragPrgama);

addNamed(path, 'jsx', 'astroturf', { nameHint: jsx.name });
addNamed(path, 'F', 'astroturf', { nameHint: jsxFrag.name });

state.file.get(STYLES).changeset.unshift(
{ code: `/*${jsxPrgama}*/\n` },
{ code: `/*${jsxFragPrgama}*/\n\n` },
{
code: `const { jsx: ${jsx.name}, F: ${jsxFrag.name} } = require('astroturf');\n`,
},
);
},
},

CallExpression(path, state) {
const { file } = state;
const pluginOptions = state.defaultedOptions;

if (!isCreateElementCall(path)) return;

const typeName = getNameFromPath(path.get('arguments')[0]);

const propsPath = path.get('arguments')[1];

const innerState = { pluginOptions, file, processed: false, typeName };
propsPath.traverse(cssPropertyVisitors, innerState);

if (innerState.processed) {
const { jsx } = state[JSX_IDENTS];
const { changeset } = file.get(STYLES);
const callee = path.get('callee');

changeset.push({
code: jsx.name,
start: callee.node.start,
end: callee.node.end,
});

callee.replaceWith(jsx);
file.set(HAS_CSS_PROP, true);
}
},

JSXAttribute(path, state) {
const { file } = state;
const pluginOptions = state.defaultedOptions;

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

const valuePath = path.get('value');
const parentPath = path.findParent(p => p.isJSXOpeningElement());

const compiledNode = buildCssProp(
valuePath,
parentPath && getNameFromPath(parentPath.get('name')),
{
file,
pluginOptions,
isJsx: true,
},
);

if (compiledNode) {
valuePath.replaceWith(compiledNode);
file.set(HAS_CSS_PROP, true);
}
},
};
124 changes: 124 additions & 0 deletions src/features/styled-component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import get from 'lodash/get';
import generate from '@babel/generator';
import template from '@babel/template';
import * as t from '@babel/types';

import buildTaggedTemplate from '../utils/buildTaggedTemplate';
import createStyleNode from '../utils/createStyleNode';
import getDisplayName from '../utils/getDisplayName';
import hasAttrs from '../utils/hasAttrs';
import isStyledTag from '../utils/isStyledTag';
import isStyledTagShorthand from '../utils/isStyledTagShorthand';
import normalizeAttrs from '../utils/normalizeAttrs';
import { COMPONENTS, STYLES } from '../utils/Symbols';
import toVarsArray from '../utils/toVarsArray';
import trimExpressions from '../utils/trimExpressions';
import wrapInClass from '../utils/wrapInClass';

const PURE_COMMENT = '/*#__PURE__*/';

const buildImport = template('require(FILENAME);');

const buildComponent = template(
`styled(ELEMENTTYPE, OPTIONS, {
displayName: DISPLAYNAME,
styles: IMPORT,
attrs: ATTRS,
vars: VARS
})`,
);

function buildStyledComponent(path, elementType, opts) {
const { file, pluginOptions, styledAttrs, styledOptions } = opts;
const cssState = file.get(STYLES);
const nodeMap = file.get(COMPONENTS);
const displayName = getDisplayName(path, opts, null);

if (!displayName)
throw path.buildCodeFrameError(
// the expression case should always be the problem but just in case, let's avoid a potentially weird error.
path.findParent(p => p.isExpressionStatement())
? 'The output of this styled component is never used. Either assign it to a variable or export it.'
: 'Could not determine a displayName for this styled component. Each component must be uniquely identifiable, either as the default export of the module or by assigning it to a unique identifier',
);

const style = createStyleNode(path, displayName, opts);

style.isStyledComponent = true;

const { text, dynamicInterpolations, imports } = buildTaggedTemplate({
style,
nodeMap,
...opts.pluginOptions,
quasiPath: path.get('quasi'),
useCssProperties: pluginOptions.customCssProperties === true,
});

style.imports = imports;
style.interpolations = trimExpressions(dynamicInterpolations);
style.value = imports + wrapInClass(text);

const runtimeNode = buildComponent({
ELEMENTTYPE: elementType,
ATTRS: normalizeAttrs(styledAttrs),
OPTIONS: styledOptions || t.NullLiteral(),
DISPLAYNAME: t.StringLiteral(displayName),
VARS: toVarsArray(dynamicInterpolations),
IMPORT: buildImport({
FILENAME: t.StringLiteral(style.relativeFilePath),
}).expression,
});

if (pluginOptions.generateInterpolations) {
style.code = `${PURE_COMMENT}${generate(runtimeNode).code}`;
}

cssState.styles.set(style.absoluteFilePath, style);
nodeMap.set(runtimeNode.expression, style);
return runtimeNode;
}

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

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

if (isStyledTag(tagPath, pluginOptions)) {
let styledOptions, componentType, styledAttrs;

if (hasAttrs(tagPath.get('callee'))) {
styledAttrs = get(tagPath, 'node.arguments[0]');

const styled = tagPath.get('callee.object');
componentType = get(styled, 'node.arguments[0]');
styledOptions = get(styled, 'node.arguments[1]');
} else {
componentType = get(tagPath, 'node.arguments[0]');
styledOptions = get(tagPath, 'node.arguments[1]');
}

path.replaceWith(
buildStyledComponent(path, componentType, {
pluginOptions,
styledAttrs,
styledOptions,
file: state.file,
}),
);
path.addComment('leading', '#__PURE__');

// styled.button` ... `
} else if (isStyledTagShorthand(tagPath, pluginOptions)) {
const componentType = t.StringLiteral(tagPath.get('property').node.name);

path.replaceWith(
buildStyledComponent(path, componentType, {
pluginOptions,
file: state.file,
}),
);
path.addComment('leading', '#__PURE__');
}
},
};
Loading

0 comments on commit c82a67e

Please sign in to comment.