@teleporthq/teleport-plugin-react-jss 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,302 @@
1
+ /*
2
+ teleport-plugin-react-jss
3
+
4
+ Plugin is responsible for generating styles from
5
+ - Styles defined on individual nodes.
6
+ - Styles defined in the project's global stylesheet.
7
+ - Styles present in the component style sheeet.
8
+ */
9
+
10
+ import * as types from '@babel/types'
11
+ import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
12
+ import { ASTUtils, ASTBuilders, ParsedASTNode } from '@teleporthq/teleport-plugin-common'
13
+ import {
14
+ ComponentPluginFactory,
15
+ ComponentPlugin,
16
+ ChunkType,
17
+ FileType,
18
+ PluginReactJSS,
19
+ } from '@teleporthq/teleport-types'
20
+ import {
21
+ generateStylesFromStyleObj,
22
+ createStylesHookDecleration,
23
+ generateStylesFromStyleSetDefinitions,
24
+ convertMediaAndStylesToObject,
25
+ } from './utils'
26
+ import { createStyleSheetPlugin } from './style-sheet'
27
+
28
+ interface JSSConfig {
29
+ styleChunkName?: string
30
+ importChunkName?: string
31
+ componentChunkName: string
32
+ jssDeclarationName?: string
33
+ classAttributeName?: string
34
+ styleObjectImportName?: string
35
+ }
36
+ const EXPORT_IDENTIFIER = 'createUseStyles'
37
+
38
+ export const createReactJSSPlugin: ComponentPluginFactory<JSSConfig> = (config) => {
39
+ const {
40
+ componentChunkName = 'jsx-component',
41
+ importChunkName = 'import-local',
42
+ styleChunkName = 'jss-style-definition',
43
+ jssDeclarationName = 'useStyles',
44
+ classAttributeName = 'className',
45
+ styleObjectImportName = 'classes',
46
+ } = config || {}
47
+
48
+ const reactJSSPlugin: ComponentPlugin = async (structure) => {
49
+ const { uidl, chunks, dependencies, options } = structure
50
+ const { projectStyleSet } = options
51
+ const { node, styleSetDefinitions: componentStyleSheet = {} } = uidl
52
+
53
+ const componentChunk = chunks.find((chunkItem) => chunkItem.name === componentChunkName)
54
+ if (!componentChunk) {
55
+ return structure
56
+ }
57
+
58
+ const jssStyleMap: Record<string, unknown> = {}
59
+ const mediaStyles: Record<string, Record<string, unknown>> = {}
60
+ const tokensUsed: string[] = []
61
+
62
+ const propsPrefix = componentChunk.meta.dynamicRefPrefix.prop as string
63
+ const jsxNodesLookup = componentChunk.meta.nodesLookup || {}
64
+ let isProjectReferenced: boolean = false
65
+ const propsUsed: string[] = []
66
+
67
+ UIDLUtils.traverseElements(node, (element) => {
68
+ const { style, key, referencedStyles } = element
69
+ if (!style && !referencedStyles) {
70
+ return
71
+ }
72
+
73
+ const jsxTag = jsxNodesLookup[key] as types.JSXElement
74
+ if (!jsxTag) {
75
+ return
76
+ }
77
+
78
+ const className = StringUtils.camelCaseToDashCase(key)
79
+ const classNamesToAppend: Set<types.MemberExpression | types.Identifier> = new Set()
80
+ const nodeStyleIdentifier = types.memberExpression(
81
+ types.identifier(styleObjectImportName),
82
+ types.identifier(`'${className}'`),
83
+ true
84
+ )
85
+
86
+ if (Object.keys(style || {}).length > 0) {
87
+ jssStyleMap[className] = generateStylesFromStyleObj(style, tokensUsed, propsUsed)
88
+ classNamesToAppend.add(nodeStyleIdentifier)
89
+ }
90
+
91
+ if (referencedStyles && Object.keys(referencedStyles || {}).length > 0) {
92
+ Object.values(referencedStyles).forEach((styleRef) => {
93
+ switch (styleRef.content.mapType) {
94
+ case 'inlined': {
95
+ const { conditions } = styleRef.content
96
+ const [condition] = conditions
97
+
98
+ if (Object.keys(styleRef.content.styles || {}).length === 0) {
99
+ return
100
+ }
101
+
102
+ if (condition.conditionType === 'screen-size') {
103
+ mediaStyles[condition.maxWidth] = {
104
+ ...mediaStyles[condition.maxWidth],
105
+ [className]: generateStylesFromStyleObj(
106
+ styleRef.content.styles,
107
+ tokensUsed,
108
+ propsUsed
109
+ ),
110
+ }
111
+ }
112
+
113
+ if (condition.conditionType === 'element-state') {
114
+ jssStyleMap[className] = {
115
+ ...(jssStyleMap[className] as Record<string, string>),
116
+ [`&:${condition.content}`]: generateStylesFromStyleObj(
117
+ styleRef.content.styles,
118
+ tokensUsed,
119
+ propsUsed
120
+ ),
121
+ }
122
+ }
123
+
124
+ classNamesToAppend.add(nodeStyleIdentifier)
125
+ return
126
+ }
127
+
128
+ case 'component-referenced': {
129
+ const classContent = styleRef.content.content
130
+ if (classContent.type === 'static') {
131
+ classNamesToAppend.add(types.identifier(`'${classContent.content}'`))
132
+ return
133
+ }
134
+
135
+ if (
136
+ classContent.type === 'dynamic' &&
137
+ classContent.content.referenceType === 'prop'
138
+ ) {
139
+ classNamesToAppend.add(
140
+ types.memberExpression(
141
+ types.identifier(styleObjectImportName),
142
+ types.memberExpression(
143
+ types.identifier(propsPrefix),
144
+ types.identifier(classContent.content.id)
145
+ ),
146
+ true
147
+ )
148
+ )
149
+ }
150
+
151
+ if (
152
+ classContent.type === 'dynamic' &&
153
+ classContent.content.referenceType === 'comp'
154
+ ) {
155
+ classNamesToAppend.add(
156
+ types.memberExpression(
157
+ types.identifier(styleObjectImportName),
158
+ types.identifier(`'${classContent.content.id}'`),
159
+ true
160
+ )
161
+ )
162
+ }
163
+
164
+ return
165
+ }
166
+
167
+ case 'project-referenced': {
168
+ if (!projectStyleSet) {
169
+ throw new PluginReactJSS(
170
+ `Project Style Sheet is missing, but the node is referring to it ${JSON.stringify(
171
+ element,
172
+ null,
173
+ 2
174
+ )}`
175
+ )
176
+ }
177
+
178
+ const { content } = styleRef
179
+ const referedStyle = projectStyleSet.styleSetDefinitions[content.referenceId]
180
+ if (!referedStyle) {
181
+ throw new PluginReactJSS(
182
+ `Style used from global stylesheet is missing - ${content.referenceId}`
183
+ )
184
+ }
185
+
186
+ classNamesToAppend.add(
187
+ types.memberExpression(
188
+ types.identifier('projectStyles'),
189
+ types.identifier(`'${content.referenceId}'`),
190
+ true
191
+ )
192
+ )
193
+ isProjectReferenced = true
194
+ return
195
+ }
196
+ default: {
197
+ throw new PluginReactJSS(`
198
+ We support only inlined and project-referenced styles as of now, received ${JSON.stringify(
199
+ styleRef.content,
200
+ null,
201
+ 2
202
+ )}
203
+ `)
204
+ }
205
+ }
206
+ })
207
+ }
208
+
209
+ ASTUtils.addMultipleDynamicAttributesToJSXTag(
210
+ jsxTag,
211
+ classAttributeName,
212
+ Array.from(classNamesToAppend)
213
+ )
214
+ })
215
+
216
+ if (Object.keys(componentStyleSheet).length > 0) {
217
+ generateStylesFromStyleSetDefinitions({
218
+ styleSetDefinitions: componentStyleSheet,
219
+ styleSet: jssStyleMap,
220
+ mediaStyles,
221
+ tokensUsed,
222
+ })
223
+ }
224
+
225
+ const { content: astContent } = componentChunk
226
+ const parser = new ParsedASTNode(astContent)
227
+
228
+ const astNode = (parser.ast as types.VariableDeclaration)
229
+ .declarations[0] as types.VariableDeclarator
230
+ if (astNode.type === 'VariableDeclarator') {
231
+ const isPropsInjected = (astNode.init as types.ArrowFunctionExpression).params?.some(
232
+ (prop: types.Identifier) => prop.name === propsPrefix
233
+ )
234
+ if (!isPropsInjected && propsUsed.length > 0) {
235
+ ;(astNode.init as types.ArrowFunctionExpression).params.push(types.identifier(propsPrefix))
236
+ }
237
+ }
238
+
239
+ if (tokensUsed.length > 0) {
240
+ dependencies.TOKENS = {
241
+ type: 'local',
242
+ path: `${projectStyleSet.path}${projectStyleSet.fileName}`,
243
+ meta: {
244
+ namedImport: true,
245
+ },
246
+ }
247
+ }
248
+
249
+ if (isProjectReferenced) {
250
+ dependencies.useProjectStyles = {
251
+ type: 'local',
252
+ path: `${projectStyleSet.path}${projectStyleSet.fileName}`,
253
+ meta: {
254
+ namedImport: true,
255
+ },
256
+ }
257
+ ;((astNode.init as types.ArrowFunctionExpression).body as types.BlockStatement).body.unshift(
258
+ createStylesHookDecleration('projectStyles', 'useProjectStyles')
259
+ )
260
+ }
261
+
262
+ if (!Object.keys(jssStyleMap).length && !Object.keys(mediaStyles).length) {
263
+ return structure
264
+ }
265
+
266
+ ;((astNode.init as types.ArrowFunctionExpression).body as types.BlockStatement).body.unshift(
267
+ propsUsed.length > 0
268
+ ? createStylesHookDecleration(styleObjectImportName, 'useStyles', propsPrefix)
269
+ : createStylesHookDecleration(styleObjectImportName, 'useStyles')
270
+ )
271
+
272
+ dependencies[EXPORT_IDENTIFIER] = {
273
+ type: 'package',
274
+ path: 'react-jss',
275
+ version: '10.4.0',
276
+ meta: {
277
+ namedImport: true,
278
+ },
279
+ }
280
+
281
+ chunks.push({
282
+ type: ChunkType.AST,
283
+ fileType: FileType.JS,
284
+ name: styleChunkName,
285
+ linkAfter: [importChunkName],
286
+ content: ASTBuilders.createConstAssignment(
287
+ jssDeclarationName,
288
+ types.callExpression(types.identifier(EXPORT_IDENTIFIER), [
289
+ convertMediaAndStylesToObject(jssStyleMap, mediaStyles),
290
+ ])
291
+ ),
292
+ })
293
+
294
+ return structure
295
+ }
296
+
297
+ return reactJSSPlugin
298
+ }
299
+
300
+ export { createStyleSheetPlugin }
301
+
302
+ export default createReactJSSPlugin()
@@ -0,0 +1,97 @@
1
+ import * as types from '@babel/types'
2
+ import { ASTUtils } from '@teleporthq/teleport-plugin-common'
3
+ import {
4
+ ComponentPlugin,
5
+ ComponentPluginFactory,
6
+ ChunkType,
7
+ FileType,
8
+ } from '@teleporthq/teleport-types'
9
+ import { StringUtils } from '@teleporthq/teleport-shared'
10
+ import { generateStylesFromStyleSetDefinitions, convertMediaAndStylesToObject } from './utils'
11
+
12
+ interface StyleSheetPlugin {
13
+ fileName?: string
14
+ }
15
+
16
+ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> = (config) => {
17
+ const { fileName } = config || { fileName: 'style' }
18
+ const styleSheetPlugin: ComponentPlugin = async (structure) => {
19
+ const { uidl, chunks, dependencies } = structure
20
+ const { styleSetDefinitions = {}, designLanguage: { tokens = {} } = {} } = uidl
21
+
22
+ if (Object.keys(styleSetDefinitions).length === 0 && Object.keys(tokens).length === 0) {
23
+ return structure
24
+ }
25
+
26
+ const tokensMap: Record<string, string | number> = Object.keys(tokens || {}).reduce(
27
+ (acc: Record<string, string | number>, key: string) => {
28
+ const style = tokens[key]
29
+ const name: string = StringUtils.capitalize(StringUtils.dashCaseToCamelCase(key))
30
+ acc[name] = style.content as string
31
+ return acc
32
+ },
33
+ {}
34
+ )
35
+
36
+ const styleSet: Record<string, unknown> = {}
37
+ const mediaStyles: Record<string, Record<string, unknown>> = {}
38
+ if (Object.keys(styleSetDefinitions).length > 0) {
39
+ generateStylesFromStyleSetDefinitions({
40
+ styleSetDefinitions,
41
+ styleSet,
42
+ mediaStyles,
43
+ })
44
+ }
45
+
46
+ uidl.outputOptions = uidl.outputOptions || {}
47
+ uidl.outputOptions.fileName = fileName
48
+
49
+ dependencies.createUseStyles = {
50
+ type: 'package',
51
+ path: 'react-jss',
52
+ version: '10.4.0',
53
+ meta: {
54
+ namedImport: true,
55
+ },
56
+ }
57
+
58
+ if (Object.keys(tokens).length > 0) {
59
+ chunks.push({
60
+ name: 'tokens-chunk',
61
+ type: ChunkType.AST,
62
+ fileType: FileType.JS,
63
+ content: types.exportNamedDeclaration(
64
+ types.variableDeclaration('const', [
65
+ types.variableDeclarator(
66
+ types.identifier('TOKENS'),
67
+ ASTUtils.objectToObjectExpression(tokensMap)
68
+ ),
69
+ ])
70
+ ),
71
+ linkAfter: ['import-local'],
72
+ })
73
+ }
74
+
75
+ if (Object.keys(styleSet).length > 0) {
76
+ chunks.push({
77
+ name: fileName,
78
+ type: ChunkType.AST,
79
+ fileType: FileType.JS,
80
+ content: types.exportNamedDeclaration(
81
+ types.variableDeclaration('const', [
82
+ types.variableDeclarator(
83
+ types.identifier('useProjectStyles'),
84
+ types.callExpression(types.identifier('createUseStyles'), [
85
+ convertMediaAndStylesToObject(styleSet, mediaStyles),
86
+ ])
87
+ ),
88
+ ])
89
+ ),
90
+ linkAfter: ['tokens-chunk'],
91
+ })
92
+ }
93
+
94
+ return structure
95
+ }
96
+ return styleSheetPlugin
97
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,111 @@
1
+ import * as types from '@babel/types'
2
+ import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
3
+ import { ParsedASTNode, ASTBuilders, ASTUtils } from '@teleporthq/teleport-plugin-common'
4
+ import { UIDLStyleSetDefinition, UIDLStyleValue } from '@teleporthq/teleport-types'
5
+
6
+ export const generateStylesFromStyleSetDefinitions = (params: {
7
+ styleSetDefinitions: Record<string, UIDLStyleSetDefinition>
8
+ styleSet: Record<string, unknown>
9
+ mediaStyles: Record<string, Record<string, unknown>>
10
+ tokensUsed?: string[]
11
+ formatClassName?: boolean
12
+ }) => {
13
+ const {
14
+ styleSetDefinitions,
15
+ styleSet,
16
+ tokensUsed,
17
+ formatClassName = false,
18
+ mediaStyles = {},
19
+ } = params
20
+ Object.keys(styleSetDefinitions).forEach((styleId) => {
21
+ const style = styleSetDefinitions[styleId]
22
+ const className = formatClassName ? StringUtils.dashCaseToCamelCase(styleId) : styleId
23
+ const { conditions = [], content } = style
24
+ styleSet[className] = generateStylesFromStyleObj(content, tokensUsed)
25
+
26
+ if (conditions.length > 0) {
27
+ conditions.forEach((styleRef) => {
28
+ if (Object.keys(styleRef.content).length === 0) {
29
+ return
30
+ }
31
+ if (styleRef.type === 'screen-size') {
32
+ mediaStyles[styleRef.meta.maxWidth] = {
33
+ ...mediaStyles[styleRef.meta.maxWidth],
34
+ [className]: generateStylesFromStyleObj(styleRef.content, tokensUsed),
35
+ }
36
+ }
37
+
38
+ if (styleRef.type === 'element-state') {
39
+ styleSet[className] = {
40
+ ...((styleSet[className] as Record<string, unknown>) || {}),
41
+ ...{
42
+ [`&:${styleRef.meta.state}`]: generateStylesFromStyleObj(
43
+ styleRef.content,
44
+ tokensUsed
45
+ ),
46
+ },
47
+ }
48
+ }
49
+ })
50
+ }
51
+ })
52
+ }
53
+
54
+ export const generateStylesFromStyleObj = (
55
+ style: Record<string, UIDLStyleValue>,
56
+ tokensUsed?: string[],
57
+ propsUsed?: string[]
58
+ ) => {
59
+ return UIDLUtils.transformDynamicStyles(style, (styleValue) => {
60
+ switch (styleValue.content.referenceType) {
61
+ case 'prop':
62
+ propsUsed?.push(styleValue.content.id)
63
+ return new ParsedASTNode(
64
+ ASTBuilders.createArrowFunctionWithMemberExpression('props', styleValue.content.id)
65
+ )
66
+ case 'token':
67
+ const token = StringUtils.capitalize(StringUtils.dashCaseToCamelCase(styleValue.content.id))
68
+ tokensUsed?.push(token)
69
+ return new ParsedASTNode(
70
+ types.memberExpression(types.identifier('TOKENS'), types.identifier(token))
71
+ )
72
+ default:
73
+ throw new Error(
74
+ `Error running transformDynamicStyles in reactJSSComponentStyleChunksPlugin. Unsupported styleValue.content.referenceType value ${styleValue.content.referenceType}`
75
+ )
76
+ }
77
+ })
78
+ }
79
+
80
+ export const createStylesHookDecleration = (
81
+ assignee: string,
82
+ hookName: string,
83
+ dynamicValueIdentifier?: string
84
+ ) =>
85
+ types.variableDeclaration('const', [
86
+ types.variableDeclarator(
87
+ types.identifier(assignee),
88
+ types.callExpression(
89
+ types.identifier(hookName),
90
+ dynamicValueIdentifier ? [types.identifier(dynamicValueIdentifier)] : []
91
+ )
92
+ ),
93
+ ])
94
+
95
+ export const convertMediaAndStylesToObject = (
96
+ styleSet: Record<string, unknown>,
97
+ mediaStyles: Record<string, Record<string, unknown>>
98
+ ): types.ObjectExpression => {
99
+ const styles = ASTUtils.objectToObjectExpression(styleSet)
100
+
101
+ Object.keys(mediaStyles).forEach((mediaKey: string) => {
102
+ styles.properties.push(
103
+ types.objectProperty(
104
+ types.identifier(`'@media(max-width: ${mediaKey}px)'`),
105
+ ASTUtils.objectToObjectExpression(mediaStyles[mediaKey])
106
+ )
107
+ )
108
+ }, [])
109
+
110
+ return styles
111
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist/esm"
5
+ },
6
+ "include": [
7
+ "./src"
8
+ ]
9
+ }