-
Notifications
You must be signed in to change notification settings - Fork 67
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
CSS Map alternative compilation approach #1496
Merged
Changes from 15 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
664672b
Add CSS Map
liamqma 73a7ffb
Update packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts
liamqma f5e8a25
Update packages/babel-plugin/src/css-prop/__tests__/css-map.test.ts
liamqma 0a22e90
Remove console.log
liamqma 184c044
Update function name
liamqma 8a028d5
Add CSS Map to Parcel example
liamqma dc772f8
Refactor error handling
liamqma c0fde8d
Add doc to cssMap
liamqma 8002047
Regenerate cssMap Flow types
liamqma 7c0ac9a
Merge branch 'master' into css-map
liamqma 3f6a017
Update test
liamqma 4748a80
Update error messages
liamqma 3b6f848
Merge branch 'master' into css-map
liamqma 4738f0d
Implement alternative compilation method
liamqma 1d21e82
Update packages/babel-plugin/src/types.ts
liamqma 2641585
Merge branch 'master' into css-map-2
liamqma 80cfb66
Add integration tests, storybooks, and vr tests for CSS Map
liamqma 717b429
Add changeset
liamqma File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the key is not in the css map?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then class name will be undefined and stripped out by
ax
. The idea is to reply on type safety to detect problems, instead of build-time errors.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense, I assume that should help reduce the impact on build time