Skip to content

Commit

Permalink
Implement alternative compilation method
Browse files Browse the repository at this point in the history
  • Loading branch information
liamqma committed Aug 24, 2023
1 parent 3b6f848 commit 4738f0d
Show file tree
Hide file tree
Showing 10 changed files with 411 additions and 435 deletions.
10 changes: 8 additions & 2 deletions packages/babel-plugin/src/babel-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as t from '@babel/types';
import { unique, preserveLeadingComments } from '@compiled/utils';

import { visitClassNamesPath } from './class-names';
import { visitCssMapPath } from './css-map';
import { visitCssPropPath } from './css-prop';
import { visitStyledPath } from './styled';
import type { State } from './types';
Expand Down Expand Up @@ -40,6 +41,7 @@ export default declare<State>((api) => {
inherits: jsxSyntax,
pre() {
this.sheets = {};
this.cssMap = {};
let cache: Cache;

if (this.opts.cache === true) {
Expand Down Expand Up @@ -172,6 +174,11 @@ export default declare<State>((api) => {
path: NodePath<t.TaggedTemplateExpression> | NodePath<t.CallExpression>,
state: State
) {
if (isCompiledCSSMapCallExpression(path.node, state)) {
visitCssMapPath(path, { context: 'root', state, parentPath: path });
return;
}

const hasStyles =
isCompiledCSSTaggedTemplateExpression(path.node, state) ||
isCompiledStyledTaggedTemplateExpression(path.node, state) ||
Expand All @@ -186,8 +193,7 @@ export default declare<State>((api) => {
isCompiledCSSTaggedTemplateExpression(path.node, state) ||
isCompiledKeyframesTaggedTemplateExpression(path.node, state) ||
isCompiledCSSCallExpression(path.node, state) ||
isCompiledKeyframesCallExpression(path.node, state) ||
isCompiledCSSMapCallExpression(path.node, state);
isCompiledKeyframesCallExpression(path.node, state);

if (isCompiledUtil) {
state.pathsToCleanup.push({ path, action: 'replace' });
Expand Down
135 changes: 135 additions & 0 deletions packages/babel-plugin/src/css-map/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import type { TransformOptions } from '../../test-utils';
import { transform as transformCode } from '../../test-utils';
import { ErrorMessages } from '../index';

describe('css map', () => {
const transform = (code: string, opts: TransformOptions = {}) =>
transformCode(code, { pretty: false, ...opts });

const styles = `{
danger: {
color: 'red',
backgroundColor: 'red'
},
success: {
color: 'green',
backgroundColor: 'green'
}
}`;

it('should transform css map', () => {
const actual = transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap(${styles});
`);

expect(actual).toInclude(
'const styles={danger:"_syaz5scu _bfhk5scu",success:"_syazbf54 _bfhkbf54"};'
);
});

it('should error out if variants are not defined at the top-most scope of the module.', () => {
expect(() => {
transform(`
import { cssMap } from '@compiled/react';
const styles = {
map1: cssMap(${styles}),
}
`);
}).toThrow(ErrorMessages.DEFINE_MAP);

expect(() => {
transform(`
import { cssMap } from '@compiled/react';
const styles = () => cssMap(${styles})
`);
}).toThrow(ErrorMessages.DEFINE_MAP);
});

it('should error out if cssMap receives more than one argument', () => {
expect(() => {
transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap(${styles}, ${styles})
`);
}).toThrow(ErrorMessages.NUMBER_OF_ARGUMENT);
});

it('should error out if cssMap does not receive an object', () => {
expect(() => {
transform(`
import { cssMap } from '@compiled/react';
const styles = cssMap('color: red')
`);
}).toThrow(ErrorMessages.ARGUMENT_TYPE);
});

it('should error out if spread element is used', () => {
expect(() => {
transform(`
import { css, cssMap } from '@compiled/react';
const styles = cssMap({
...base
});
`);
}).toThrow(ErrorMessages.NO_SPREAD_ELEMENT);
});

it('should error out if object method is used', () => {
expect(() => {
transform(`
import { css, cssMap } from '@compiled/react';
const styles = cssMap({
danger() {}
});
`);
}).toThrow(ErrorMessages.NO_OBJECT_METHOD);
});

it('should error out if variant object is dynamic', () => {
expect(() => {
transform(`
import { css, cssMap } from '@compiled/react';
const styles = cssMap({
danger: otherStyles
});
`);
}).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT);
});

it('should error out if styles include runtime variables', () => {
expect(() => {
transform(`
import { css, cssMap } from '@compiled/react';
const styles = cssMap({
danger: {
color: canNotBeStaticallyEvulated
}
});
`);
}).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT);
});

it('should error out if styles include conditional CSS', () => {
expect(() => {
transform(`
import { css, cssMap } from '@compiled/react';
const styles = cssMap({
danger: {
color: canNotBeStaticallyEvulated ? 'red' : 'blue'
}
});
`);
}).toThrow(ErrorMessages.STATIC_VARIANT_OBJECT);
});
});
153 changes: 153 additions & 0 deletions packages/babel-plugin/src/css-map/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { NodePath } from '@babel/core';
import * as t from '@babel/types';

import type { Metadata } from '../types';
import { buildCodeFrameError } from '../utils/ast';
import { buildCss } from '../utils/css-builders';
import { transformCssItems } from '../utils/transform-css-items';

// The messages are exported for testing.
export enum ErrorMessages {
NO_TAGGED_TEMPLATE = 'cssMap function cannot be used as a tagged template expression.',
NUMBER_OF_ARGUMENT = 'cssMap function can only receive one argument.',
ARGUMENT_TYPE = 'cssMap function can only receive an object.',
DEFINE_MAP = 'CSS Map must be declared at the top-most scope of the module.',
NO_SPREAD_ELEMENT = 'Spread element is not supported in CSS Map.',
NO_OBJECT_METHOD = 'Object method is not supported in CSS Map.',
STATIC_VARIANT_OBJECT = 'The variant object must be statically defined.',
}

const createErrorMessage = (message: string): string => {
return `
${message}
To correctly implement a CSS Map, follow the syntax below:
\`\`\`
import { css, cssMap } from '@compiled/react';
const borderStyleMap = cssMap({
none: { borderStyle: 'none' },
solid: { borderStyle: 'solid' },
});
const Component = ({ borderStyle }) => <div css={css(borderStyleMap[borderStyle])} />
\`\`\`
`;
};

/**
* Takes `cssMap` function expression and then transforms it to a record of class names and sheets.
*
* For example:
* ```
* const styles = cssMap({
* none: { color: 'red' },
* solid: { color: 'green' },
* });
* ```
* gets transformed to
* ```
* const styles = {
* danger: "_syaz5scu",
* success: "_syazbf54",
* };
* ```
*
* @param path {NodePath} The path to be evaluated.
* @param meta {Metadata} Useful metadata that can be used during the transformation
*/
export const visitCssMapPath = (
path: NodePath<t.CallExpression> | NodePath<t.TaggedTemplateExpression>,
meta: Metadata
): void => {
// We don't support tagged template expressions.
if (t.isTaggedTemplateExpression(path.node)) {
throw buildCodeFrameError(
createErrorMessage(ErrorMessages.DEFINE_MAP),
path.node,
meta.parentPath
);
}

// We need to ensure CSS Map is declared at the top-most scope of the module.
if (!t.isVariableDeclarator(path.parent) || !t.isIdentifier(path.parent.id)) {
throw buildCodeFrameError(
createErrorMessage(ErrorMessages.DEFINE_MAP),
path.node,
meta.parentPath
);
}

// We need to ensure cssMap receives only one argument.
if (path.node.arguments.length !== 1) {
throw buildCodeFrameError(
createErrorMessage(ErrorMessages.NUMBER_OF_ARGUMENT),
path.node,
meta.parentPath
);
}

// We need to ensure the argument is an objectExpression.
if (!t.isObjectExpression(path.node.arguments[0])) {
throw buildCodeFrameError(
createErrorMessage(ErrorMessages.ARGUMENT_TYPE),
path.node,
meta.parentPath
);
}

const totalSheets: string[] = [];
path.replaceWith(
t.objectExpression(
path.node.arguments[0].properties.map((property) => {
if (t.isSpreadElement(property)) {
throw buildCodeFrameError(
createErrorMessage(ErrorMessages.NO_SPREAD_ELEMENT),
property.argument,
meta.parentPath
);
}

if (t.isObjectMethod(property)) {
throw buildCodeFrameError(
createErrorMessage(ErrorMessages.NO_OBJECT_METHOD),
property.key,
meta.parentPath
);
}

if (!t.isObjectExpression(property.value)) {
throw buildCodeFrameError(
createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT),
property.value,
meta.parentPath
);
}

const { css, variables } = buildCss(property.value, meta);

if (variables.length) {
throw buildCodeFrameError(
createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT),
property.value,
meta.parentPath
);
}

const { sheets, classNames } = transformCssItems(css, meta);
totalSheets.push(...sheets);

if (classNames.length !== 1) {
throw buildCodeFrameError(
createErrorMessage(ErrorMessages.STATIC_VARIANT_OBJECT),
property,
meta.parentPath
);
}

return t.objectProperty(property.key, classNames[0]);
})
)
);

// We store sheets in the meta state so that we can use it later to generate Compiled component.
meta.state.cssMap[path.parent.id.name] = totalSheets;
};
Loading

0 comments on commit 4738f0d

Please sign in to comment.