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