@teleporthq/teleport-plugin-react-styled-jsx 0.43.0-alpha.0 → 0.43.3

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 CHANGED
@@ -1,27 +1,53 @@
1
1
  import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
2
- import { ASTUtils, StyleBuilders, ASTBuilders } from '@teleporthq/teleport-plugin-common'
2
+ import {
3
+ ASTUtils,
4
+ StyleBuilders,
5
+ ASTBuilders,
6
+ ParsedASTNode,
7
+ createBinaryExpression,
8
+ } from '@teleporthq/teleport-plugin-common'
3
9
  import {
4
10
  ComponentPluginFactory,
5
11
  ComponentPlugin,
6
12
  UIDLStyleValue,
7
13
  PluginStyledJSX,
14
+ UIDLElement,
15
+ UIDLElementNode,
8
16
  } from '@teleporthq/teleport-types'
9
17
  import { generateStyledJSXTag } from './utils'
10
18
  import * as types from '@babel/types'
11
19
 
12
20
  interface StyledJSXConfig {
13
21
  componentChunkName: string
14
- forceScoping: boolean
15
22
  }
16
23
 
24
+ const transformStyle = (style: Record<string, UIDLStyleValue>, propsPrefix: string) =>
25
+ UIDLUtils.transformDynamicStyles(style, (styleValue) => {
26
+ switch (styleValue.content.referenceType) {
27
+ case 'token':
28
+ return `var(${StringUtils.generateCSSVariableName(styleValue.content.id)})`
29
+ case 'prop':
30
+ return `\$\{${propsPrefix}.${styleValue.content.id}\}`
31
+ default:
32
+ throw new PluginStyledJSX(
33
+ `Error running transformDynamicStyles in reactStyledJSXChunkPlugin.\n
34
+ Unsupported styleValue.content.referenceType value ${styleValue.content.referenceType}`
35
+ )
36
+ }
37
+ })
38
+
17
39
  export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig> = (config) => {
18
- const { componentChunkName = 'jsx-component', forceScoping = false } = config || {}
40
+ const { componentChunkName = 'jsx-component' } = config || {}
19
41
 
20
42
  const reactStyledJSXPlugin: ComponentPlugin = async (structure) => {
21
43
  const { uidl, chunks, options } = structure
22
44
  const { projectStyleSet } = options
23
- const { node, styleSetDefinitions: componentStyleSheet = {}, propDefinitions = {} } = uidl
24
-
45
+ const {
46
+ node,
47
+ styleSetDefinitions: componentStyleSheet = {},
48
+ propDefinitions = {},
49
+ stateDefinitions = {},
50
+ } = uidl
25
51
  const componentChunk = chunks.find((chunk) => chunk.name === componentChunkName)
26
52
  if (!componentChunk) {
27
53
  return structure
@@ -35,24 +61,11 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
35
61
  > = {}
36
62
  const classMap: string[] = []
37
63
 
38
- const transformStyle = (style: Record<string, UIDLStyleValue>) =>
39
- UIDLUtils.transformDynamicStyles(style, (styleValue) => {
40
- switch (styleValue.content.referenceType) {
41
- case 'token':
42
- return `var(${StringUtils.generateCSSVariableName(styleValue.content.id)})`
43
- case 'prop':
44
- return `\$\{${propsPrefix}.${styleValue.content.id}\}`
45
- default:
46
- throw new PluginStyledJSX(
47
- `Error running transformDynamicStyles in reactStyledJSXChunkPlugin.\n
48
- Unsupported styleValue.content.referenceType value ${styleValue.content.referenceType}`
49
- )
50
- }
51
- })
52
-
53
- UIDLUtils.traverseElements(node, (element) => {
64
+ const generateStylesForElementNode = (element: UIDLElement) => {
54
65
  const classNamesToAppend: Set<string> = new Set()
55
- const dynamicVariantsToAppend: Set<types.Identifier | types.MemberExpression> = new Set()
66
+ const dynamicVariantsToAppend: Set<
67
+ types.Identifier | types.MemberExpression | types.ConditionalExpression
68
+ > = new Set()
56
69
  const {
57
70
  style = {},
58
71
  key,
@@ -60,29 +73,101 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
60
73
  attrs = {},
61
74
  dependency,
62
75
  elementType,
76
+ dynamicStyleBindings,
63
77
  } = element
64
- const elementClassName = StringUtils.camelCaseToDashCase(key)
65
- const className = getClassName(forceScoping, uidl.name, elementClassName)
78
+ const hasDynamicBindings =
79
+ dynamicStyleBindings && Object.keys(dynamicStyleBindings).length > 0
80
+
81
+ if (key === undefined) {
82
+ throw new Error(`Key is missing for element \n ${JSON.stringify(element, null, 2)}`)
83
+ }
84
+
85
+ const className = StringUtils.camelCaseToDashCase(key)
86
+ const root = jsxNodesLookup[key]
87
+ if (!root) {
88
+ throw new PluginStyledJSX(
89
+ `Element \n ${JSON.stringify(
90
+ element,
91
+ null,
92
+ 2
93
+ )} \n with key ${key} is missing from the template chunk of component ${uidl.name}`
94
+ )
95
+ }
66
96
 
67
- if (forceScoping && dependency?.type === 'local') {
97
+ if (dependency?.type === 'local') {
68
98
  StyleBuilders.setPropValueForCompStyle({
69
- key,
70
- jsxNodesLookup,
99
+ root,
71
100
  attrs,
72
- getClassName: (str: string) =>
73
- getClassName(forceScoping, StringUtils.camelCaseToDashCase(elementType), str),
101
+ getClassName: (str: string) => StringUtils.camelCaseToDashCase(elementType + str),
74
102
  })
75
103
  }
76
104
 
77
- if (Object.keys(style).length === 0 && Object.keys(referencedStyles).length === 0) {
105
+ if (
106
+ Object.keys(style).length === 0 &&
107
+ Object.keys(referencedStyles).length === 0 &&
108
+ !hasDynamicBindings
109
+ ) {
78
110
  return
79
111
  }
80
112
 
81
- const root = jsxNodesLookup[key]
113
+ // Collect all inline styles from various sources
114
+ const allInlineStyles: Record<string, unknown> = {}
115
+
116
+ if (hasDynamicBindings) {
117
+ for (const [cssProperty, binding] of Object.entries(dynamicStyleBindings)) {
118
+ const camelCaseProperty = cssProperty.replace(/-([a-z])/g, (_, letter: string) =>
119
+ letter.toUpperCase()
120
+ )
121
+ const staticValue = style[cssProperty]
122
+ const staticTemplate =
123
+ staticValue && staticValue.type === 'static' && typeof staticValue.content === 'string'
124
+ ? staticValue.content
125
+ : null
126
+ allInlineStyles[camelCaseProperty] = StyleBuilders.createDynamicBindingExpression(
127
+ binding,
128
+ undefined,
129
+ cssProperty,
130
+ staticTemplate
131
+ )
132
+ }
133
+ }
134
+
135
+ // Separate styles containing template expressions ({{ expr }}) from static CSS.
136
+ // Templates with non-state variables (e.g. {{ enemy.x }}) must be inline styles
137
+ // since those variables are scoped to Repeater render callbacks.
138
+ // Templates with state variables (e.g. {{ state.cameraX }}) are also made inline
139
+ // because they need to be interpolated as JS expressions, not raw CSS text.
140
+ const hasTemplate = (str: string) => /\{\{/.test(str)
141
+ const cssStyles: Record<string, UIDLStyleValue> = {}
142
+
143
+ for (const [prop, value] of Object.entries(style)) {
144
+ // Skip properties that have dynamic bindings — they're already handled as inline styles
145
+ if (hasDynamicBindings && dynamicStyleBindings[prop]) {
146
+ continue
147
+ }
148
+ if (
149
+ value.type === 'static' &&
150
+ typeof value.content === 'string' &&
151
+ hasTemplate(value.content)
152
+ ) {
153
+ const camelCaseProperty = prop.replace(/-([a-z])/g, (_, letter: string) =>
154
+ letter.toUpperCase()
155
+ )
156
+ allInlineStyles[camelCaseProperty] = new ParsedASTNode(
157
+ ASTUtils.parseStringWithTemplateExpressions(String(value.content))
158
+ )
159
+ } else {
160
+ cssStyles[prop] = value
161
+ }
162
+ }
163
+
164
+ if (Object.keys(allInlineStyles).length > 0) {
165
+ ASTUtils.addAttributeToJSXTag(root as types.JSXElement, 'style', allInlineStyles)
166
+ }
82
167
 
83
168
  // Generating the string templates for the dynamic styles
84
- if (Object.keys(style).length > 0) {
85
- const styleRules = transformStyle(style)
169
+ if (Object.keys(cssStyles).length > 0) {
170
+ const styleRules = transformStyle(cssStyles, propsPrefix)
86
171
  classMap.push(StyleBuilders.createCSSClass(className, styleRules))
87
172
  classNamesToAppend.add(className)
88
173
  }
@@ -96,9 +181,8 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
96
181
  if (!mediaStylesMap[String(maxWidth)]) {
97
182
  mediaStylesMap[String(maxWidth)] = []
98
183
  }
99
-
100
184
  mediaStylesMap[String(maxWidth)].push({
101
- [className]: transformStyle(styleRef.content.styles),
185
+ [className]: transformStyle(styleRef.content.styles, propsPrefix),
102
186
  })
103
187
  }
104
188
 
@@ -107,7 +191,7 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
107
191
  StyleBuilders.createCSSClassWithSelector(
108
192
  className,
109
193
  `&:${condition.content}`,
110
- transformStyle(styleRef.content.styles)
194
+ transformStyle(styleRef.content.styles, propsPrefix)
111
195
  )
112
196
  )
113
197
  }
@@ -133,15 +217,13 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
133
217
  )
134
218
  const defaultPropValue =
135
219
  propDefinitions[styleRef.content.content.content.id]?.defaultValue
220
+
136
221
  if (!defaultPropValue) {
137
222
  return
138
223
  }
139
224
 
140
- propDefinitions[styleRef.content.content.content.id].defaultValue = getClassName(
141
- forceScoping,
142
- uidl.name,
143
- String(defaultPropValue)
144
- )
225
+ propDefinitions[styleRef.content.content.content.id].defaultValue =
226
+ StringUtils.camelCaseToDashCase(String(defaultPropValue))
145
227
  }
146
228
 
147
229
  if (
@@ -149,7 +231,7 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
149
231
  styleRef.content.content.content.referenceType === 'comp'
150
232
  ) {
151
233
  classNamesToAppend.add(
152
- getClassName(forceScoping, uidl.name, styleRef.content.content.content.id)
234
+ StringUtils.camelCaseToDashCase(styleRef.content.content.content.id)
153
235
  )
154
236
  }
155
237
 
@@ -160,10 +242,73 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
160
242
  const { content } = styleRef
161
243
  const referedStyle = projectStyleSet.styleSetDefinitions[content.referenceId]
162
244
  if (!referedStyle) {
163
- throw new PluginStyledJSX(`Project style - ${content.referenceId} is missing`)
245
+ return
246
+ }
247
+
248
+ // Validate that the referenceId is a valid CSS class name.
249
+ // Skip entries that contain dots, quotes, braces, pipes, or other
250
+ // characters that indicate a JavaScript expression fragment was
251
+ // incorrectly used as a class name (e.g. "item.type", "'coin'", "{{", "||")
252
+ const refId = content.referenceId as string
253
+ if (/[.'"{}|()!@#$%^&*+=<>?/\\]/.test(refId) || /^\d/.test(refId)) {
254
+ return
164
255
  }
165
256
 
166
- classNamesToAppend.add(content.referenceId)
257
+ if (styleRef.content.condition) {
258
+ const referenceContent = styleRef.content.condition.reference.content
259
+ const referenceType = referenceContent.referenceType
260
+ const nameToAppend =
261
+ referenceType === 'local' && referenceContent.refPath?.length
262
+ ? referenceContent.refPath.join('.')
263
+ : referenceContent.id
264
+
265
+ const { conditions } = styleRef.content.condition.expression
266
+
267
+ const operator = conditions[0].operation as '===' | '!==' | '<' | '<=' | '>' | '>='
268
+ const right = conditions[0].operand
269
+
270
+ let binaryExpressionType = ''
271
+ switch (referenceType) {
272
+ case 'prop': {
273
+ binaryExpressionType = propDefinitions[nameToAppend].type
274
+ break
275
+ }
276
+ case 'state': {
277
+ binaryExpressionType = stateDefinitions[nameToAppend].type
278
+ break
279
+ }
280
+ case 'local': {
281
+ binaryExpressionType = typeof right as string
282
+ break
283
+ }
284
+ default: {
285
+ throw new PluginStyledJSX(
286
+ `Un-supported reference type ${referenceType} for style reference ${JSON.stringify(
287
+ styleRef.content,
288
+ null,
289
+ 2
290
+ )}`
291
+ )
292
+ }
293
+ }
294
+ const binaryExpression = createBinaryExpression(
295
+ { operation: operator, operand: right },
296
+ {
297
+ key: (referenceType === 'prop' ? 'props?.' : '') + nameToAppend,
298
+ type: binaryExpressionType,
299
+ }
300
+ )
301
+
302
+ const conditionalExpression = types.conditionalExpression(
303
+ binaryExpression,
304
+ types.stringLiteral(content.referenceId),
305
+ types.stringLiteral('')
306
+ )
307
+
308
+ dynamicVariantsToAppend.add(conditionalExpression)
309
+ } else {
310
+ classNamesToAppend.add(content.referenceId)
311
+ }
167
312
  return
168
313
  }
169
314
 
@@ -175,13 +320,43 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
175
320
  }
176
321
  })
177
322
 
323
+ // Handle {{ xxx }} template expressions in class names by converting to dynamic values
324
+ let joinedClasses = Array.from(classNamesToAppend).join(' ')
325
+ const dynamicVals: Array<
326
+ types.Identifier | types.MemberExpression | types.ConditionalExpression
327
+ > = Array.from(dynamicVariantsToAppend)
328
+
329
+ const templateClassMatches = joinedClasses.match(/\{\{\s*(.+?)\s*\}\}/g)
330
+ if (templateClassMatches) {
331
+ for (const match of templateClassMatches) {
332
+ const expr = match.replace(/^\{\{\s*/, '').replace(/\s*\}\}$/, '')
333
+ dynamicVals.push(
334
+ ASTUtils.parseJSExpressionAsAST(expr) as types.Identifier | types.MemberExpression
335
+ )
336
+ }
337
+ joinedClasses = joinedClasses
338
+ .replace(/\{\{\s*(.+?)\s*\}\}/g, '')
339
+ .replace(/\s+/g, ' ')
340
+ .trim()
341
+ }
342
+
178
343
  ASTUtils.addClassStringOnJSXTag(
179
344
  root as types.JSXElement,
180
- Array.from(classNamesToAppend).join(' '),
345
+ joinedClasses,
181
346
  'className',
182
- Array.from(dynamicVariantsToAppend)
347
+ dynamicVals
183
348
  )
184
- })
349
+ }
350
+
351
+ UIDLUtils.traverseElements(node, generateStylesForElementNode)
352
+ for (const prop of Object.values(propDefinitions)) {
353
+ if (prop.type === 'element' && prop.defaultValue) {
354
+ UIDLUtils.traverseElements(
355
+ prop.defaultValue as UIDLElementNode,
356
+ generateStylesForElementNode
357
+ )
358
+ }
359
+ }
185
360
 
186
361
  /* Generating component scoped styles */
187
362
  if (Object.keys(componentStyleSheet).length > 0) {
@@ -189,7 +364,7 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
189
364
  componentStyleSheet,
190
365
  classMap,
191
366
  mediaStylesMap,
192
- (styleName: string) => getClassName(forceScoping, uidl.name, styleName)
367
+ (styleName: string) => StringUtils.camelCaseToDashCase(uidl.name + styleName)
193
368
  )
194
369
  }
195
370
 
@@ -201,7 +376,25 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
201
376
  return structure
202
377
  }
203
378
 
204
- const styleJSXAST = generateStyledJSXTag(classMap.join('\n'))
379
+ // Convert {{ state.xxx }} template expressions to ${xxx} styled-jsx interpolations
380
+ // Handles both well-formed {{ state.xxx }} and malformed/unclosed {{ state.xxx patterns
381
+ const convertStateTemplates = (css: string): string => {
382
+ return css.replace(
383
+ /\{\{\s*(.*?state\..+?)(\s*\}\}|(?=["`;])|\s*$)/gm,
384
+ (_match, expr, closing) => {
385
+ let cleanExpr = expr.trim()
386
+ // Replace all state.xxx references with just xxx
387
+ cleanExpr = cleanExpr.replace(/state\.(\w+)/g, '$1')
388
+ // Remove trailing semicolons or whitespace
389
+ cleanExpr = cleanExpr.replace(/[;\s]+$/, '')
390
+ const isClosed = closing && closing.includes('}}')
391
+ return '${' + cleanExpr + '}' + (isClosed ? '' : 'px)')
392
+ }
393
+ )
394
+ }
395
+ const cssString = convertStateTemplates(classMap.join('\n'))
396
+
397
+ const styleJSXAST = generateStyledJSXTag(cssString)
205
398
  // We have the ability to insert the tag into the existig JSX structure, or do something else with it.
206
399
  // Here we take the JSX <style> tag and we insert it as the last child of the JSX structure
207
400
  // inside the React Component
@@ -215,7 +408,7 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
215
408
  // We need to replace the root node with a fragment <>
216
409
  // The fragment will be the parent of both the old root JSXNode and the style tag
217
410
  const componentAST = componentChunk.content as types.VariableDeclaration
218
- const arrowFnExpr = componentAST.declarations[0].init as types.ArrowFunctionExpression
411
+ const arrowFnExpr = componentAST.declarations?.[0]?.init as types.ArrowFunctionExpression
219
412
  const bodyStatement = arrowFnExpr.body as types.BlockStatement
220
413
  const returnStatement = bodyStatement.body.find(
221
414
  (statement) => statement.type === 'ReturnStatement'
@@ -234,9 +427,3 @@ export const createReactStyledJSXPlugin: ComponentPluginFactory<StyledJSXConfig>
234
427
  }
235
428
 
236
429
  export default createReactStyledJSXPlugin()
237
-
238
- const getClassName = (scoping: boolean, uidlName: string, nodeStyleName: string) => {
239
- return scoping
240
- ? StringUtils.camelCaseToDashCase(`${uidlName}-${nodeStyleName}`)
241
- : StringUtils.camelCaseToDashCase(nodeStyleName)
242
- }