Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inline experiment #602

Open
wants to merge 2 commits into
base: v1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
"html-webpack-plugin": "^4.2.1",
"husky": "^4.2.5",
"jest": "^25.4.0",
"jest-diff": "^25.5.0",
"jest-file-snapshot": "^0.3.6",
"lint-staged": "^10.2.0",
"mini-css-extract-plugin": "^0.9.0",
Expand Down
190 changes: 190 additions & 0 deletions src/features/css-prop-inline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { NodePath } from '@babel/core';
import * as t from '@babel/types';

import { PluginState } from '../types';
import truthy from '../utils/truthy';

type Var = [t.StringLiteral, t.Expression, t.StringLiteral | undefined];

const isSpread = (p: NodePath<any>): p is NodePath<t.JSXSpreadAttribute> =>
p.isJSXSpreadAttribute();

const assign = (...nodes: t.Expression[]) => {
const objs = nodes.filter(truthy);
if (!objs.length) return null;
return objs.length === 1
? objs[0]
: t.callExpression(
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
objs,
);
};

const findPropIndex = <T extends NodePath<any>>(attrs: T[], key: string) =>
attrs.findIndex((a) => a.isJSXAttribute() && a.node.name.name === key);

const unwrapValue = (attr: NodePath<t.JSXAttribute>) => {
const value = attr.get('value');
return value.isJSXExpressionContainer()
? value.get('expression')
: (value as NodePath<t.StringLiteral>);
};

function classNames(nodes: t.Expression[]) {
const classes = nodes.filter(truthy);
if (!classes.length) return null;
return classes.length === 1
? classes[0]
: t.callExpression(
t.memberExpression(
t.templateLiteral(
Array.from({ length: classes.length + 1 }, (_, idx) =>
t.templateElement({
raw: idx === 0 || idx === classes.length ? '' : ' ',
}),
),
classes,
),
t.identifier('trim'),
),
[],
);
}

const falsyToString = (value?: t.Expression, op: '||' | '??' = '||') =>
value && t.logicalExpression(op, value, t.stringLiteral(''));

const pluckPropertyFromSpreads = (
attrs: NodePath<t.JSXAttribute | t.JSXSpreadAttribute>[],
start: number,
property: string,
op: '||' | '??' = '||',
) => {
const spreads = (start === -1 ? attrs : attrs.slice(start))
.filter(isSpread)
.map(({ node }) =>
t.memberExpression(node.argument, t.identifier(property)),
);

return spreads.length
? spreads
.slice(1)
.reduce(
(curr: any, next: any) => t.logicalExpression(op, next!, curr),
spreads[0],
)
: null;
};

function buildStyleAttribute(
attrs: NodePath<t.JSXAttribute | t.JSXSpreadAttribute>[],
vars: t.ArrayExpression,
) {
const idx = findPropIndex(attrs, 'style');
const style = idx === -1 ? null : (attrs[idx] as NodePath<t.JSXAttribute>);

const props = vars.elements.map((el: t.ArrayExpression) => {
const [id, value, unit] = el.elements as Var;
return t.objectProperty(
t.stringLiteral(`--${id.value}`),
unit
? t.binaryExpression(
'+',
t.callExpression(t.identifier('String'), [value]),
unit,
)
: value,
);
});
const spreadsAfterStyle = pluckPropertyFromSpreads(attrs, idx, 'style');

if (!style) {
return assign(t.objectExpression(props), spreadsAfterStyle);
}

const styleValue = unwrapValue(style);
const styleValueNode = styleValue.node as t.Expression;
let assignee = styleValueNode;

style.remove();

if (spreadsAfterStyle) {
assignee = t.logicalExpression('||', spreadsAfterStyle, assignee);
} else if (styleValue.isObjectExpression()) {
// @ts-ignore
styleValue.pushContainer('properties', props);
return styleValueNode;
}

return assign(t.objectExpression(props), assignee);
}

function buildClassNameAttribute(
attrs: NodePath<t.JSXAttribute | t.JSXSpreadAttribute>[],
rootId: t.Identifier,
variants: t.ArrayExpression,
) {
const idx = findPropIndex(attrs, 'className');
const className =
idx === -1 ? null : (attrs[idx] as NodePath<t.JSXAttribute>);

const values: any[] = [
t.memberExpression(rootId, t.identifier('cls1')),
...variants.elements,
];

if (className) {
const classNameValue = unwrapValue(className);

className.remove();
// take the explicit className attribute and coerce to a string if necessary
values.unshift(
classNameValue.isStringLiteral()
? classNameValue.node
: falsyToString(classNameValue.node as t.Expression),
);
} else {
values.unshift(
falsyToString(
pluckPropertyFromSpreads(attrs, idx, 'className', '??'),
'??',
),
);
}

return classNames(values)!;
}

export function inlineJsx(
path: NodePath<t.JSXAttribute>,
node: t.ArrayExpression,
_state: PluginState,
) {
const parent = path.parentPath as NodePath<t.JSXOpeningElement>;
const attrs = parent.get('attributes');

const [rootId, vars, variants] = node.elements as [
t.Identifier,
t.ArrayExpression,
t.ArrayExpression,
];

const style = buildStyleAttribute(attrs, vars);
const className = buildClassNameAttribute(attrs, rootId, variants);

// @ts-ignore
parent.pushContainer(
'attributes',
[
style &&
t.jsxAttribute(
t.jsxIdentifier('style'),
t.jsxExpressionContainer(style),
),
t.jsxAttribute(
t.jsxIdentifier('className'),
t.jsxExpressionContainer(className),
),
].filter(truthy),
);
}
27 changes: 15 additions & 12 deletions src/features/css-prop.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import chalk from 'chalk';
import { NodePath } from '@babel/core';
import generate from '@babel/generator';
import * as t from '@babel/types';
import chalk from 'chalk';

import { DynamicStyle, PluginState } from '../types';
import { COMPONENTS, HAS_CSS_PROP, STYLES } from '../utils/Symbols';
import addPragma from '../utils/addPragma';
import buildTaggedTemplate from '../utils/buildTaggedTemplate';
import createStyleNode from '../utils/createStyleNode';
import getNameFromPath from '../utils/getNameFromPath';
import isCreateElementCall from '../utils/isCreateElementCall';
import isCssTag from '../utils/isCssTag';
import { COMPONENTS, HAS_CSS_PROP, STYLES } from '../utils/Symbols';
import wrapInClass from '../utils/wrapInClass';
import truthy from '../utils/truthy';
import wrapInClass from '../utils/wrapInClass';
import { inlineJsx } from './css-prop-inline';

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

Expand All @@ -22,11 +24,6 @@ type CssPropPluginState = PluginState & {
};
};

export const isCreateElementCall = (p: NodePath) =>
p.isCallExpression() &&
(p.get('callee.property') as any).node &&
(p.get('callee.property') as any).node.name === 'createElement';

function buildCssProp(
valuePath: NodePath<any>,
name: string | null,
Expand Down Expand Up @@ -218,22 +215,28 @@ export default {
},

JSXAttribute(path: NodePath<t.JSXAttribute>, state: CssPropPluginState) {
const { file } = state;
const { file, defaultedOptions } = state;

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

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

const compiledNode = buildCssProp(
const compiledNode: any = buildCssProp(
valuePath,
parentPath && getNameFromPath(parentPath.get('name') as NodePath),
state,
true,
);

if (compiledNode) {
valuePath.replaceWith(compiledNode);
if (defaultedOptions.experiments.inlineCssPropOptimization) {
inlineJsx(path, compiledNode.expression, state);
path.remove();
} else {
valuePath.replaceWith(compiledNode);
}

file.set(HAS_CSS_PROP, true);
}
},
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface ResolvedOptions {
experiments: {
modularCssExternals?: boolean;
selfCompile?: boolean;
inlineCssPropOptimization?: boolean;
};
}

Expand Down
12 changes: 12 additions & 0 deletions src/utils/isCreateElementCall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as T from '@babel/types';
import { NodePath } from '@babel/core';

export default function isCreateElementCall(
p: NodePath,
): p is NodePath<T.CallExpression> {
return (
p.isCallExpression() &&
(p.get('callee.property') as any).node &&
(p.get('callee.property') as any).node.name === 'createElement'
);
}
Loading