-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement alternative compilation method
- Loading branch information
Showing
10 changed files
with
411 additions
and
435 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
packages/babel-plugin/src/css-map/__tests__/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
Oops, something went wrong.