-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CSS Map alternative compilation approach (#1496)
* Add CSS Map * Update packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts Co-authored-by: Jake Lane <[email protected]> * Update packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts Co-authored-by: Jake Lane <[email protected]> * Remove console.log * Update function name * Add CSS Map to Parcel example * Refactor error handling * Add doc to cssMap * Regenerate cssMap Flow types * Update test * Update error messages * Implement alternative compilation method * Update packages/babel-plugin/src/types.ts Co-authored-by: Jake Lane <[email protected]> * Add integration tests, storybooks, and vr tests for CSS Map * Add changeset --------- Co-authored-by: Jake Lane <[email protected]>
- Loading branch information
Showing
24 changed files
with
663 additions
and
2 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
'@compiled/babel-plugin': minor | ||
'@compiled/webpack-app': minor | ||
'@compiled/parcel-app': minor | ||
'@compiled/react': minor | ||
--- | ||
|
||
Implement the `cssMap` API to enable library users to dynamically choose a varied set of CSS rules. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
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,12 @@ | ||
import { css, cssMap } from '@compiled/react'; | ||
|
||
const styles = cssMap({ | ||
danger: { | ||
color: 'red', | ||
}, | ||
success: { | ||
color: 'green', | ||
}, | ||
}); | ||
|
||
export default ({ variant, children }) => <div css={css(styles[variant])}>{children}</div>; |
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
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,12 @@ | ||
import { css, cssMap } from '@compiled/react'; | ||
|
||
const styles = cssMap({ | ||
danger: { | ||
color: 'red', | ||
}, | ||
success: { | ||
color: 'green', | ||
}, | ||
}); | ||
|
||
export default ({ variant, children }) => <div css={css(styles[variant])}>{children}</div>; |
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.