@teleporthq/teleport-plugin-css 0.41.0 → 0.42.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,5 +1,11 @@
1
1
  import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
2
- import { StyleUtils, StyleBuilders, HASTUtils, ASTUtils } from '@teleporthq/teleport-plugin-common'
2
+ import {
3
+ StyleUtils,
4
+ StyleBuilders,
5
+ HASTUtils,
6
+ ASTUtils,
7
+ createBinaryExpression,
8
+ } from '@teleporthq/teleport-plugin-common'
3
9
  import * as types from '@babel/types'
4
10
  import {
5
11
  ComponentPluginFactory,
@@ -12,8 +18,14 @@ import {
12
18
  UIDLElementNodeReferenceStyles,
13
19
  UIDLStyleMediaQueryScreenSizeCondition,
14
20
  PluginCSS,
21
+ UIDLElement,
22
+ UIDLElementNode,
23
+ UIDLExpressionValue,
24
+ StateDefaultValueTypes,
25
+ PropDefaultValueTypes,
15
26
  } from '@teleporthq/teleport-types'
16
27
  import { createStyleSheetPlugin } from './style-sheet'
28
+ import { createConditionalStatement } from './utils'
17
29
 
18
30
  interface CSSPluginConfig {
19
31
  chunkName: string
@@ -21,7 +33,6 @@ interface CSSPluginConfig {
21
33
  componentDecoratorChunkName: string
22
34
  inlineStyleAttributeKey: string // style vs :style vs ...
23
35
  classAttributeName: string // class vs className
24
- forceScoping: boolean // class names get the component name prefix
25
36
  templateStyle: 'html' | 'jsx'
26
37
  declareDependency: 'import' | 'decorator' | 'none'
27
38
  dynamicVariantPrefix?: string
@@ -37,14 +48,18 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
37
48
  classAttributeName = 'class',
38
49
  templateStyle = 'html',
39
50
  declareDependency = 'none',
40
- forceScoping = false,
41
51
  dynamicVariantPrefix,
42
52
  staticPropReferences = false,
43
53
  } = config || {}
44
54
 
45
55
  const cssPlugin: ComponentPlugin = async (structure) => {
46
56
  const { uidl, chunks, dependencies, options } = structure
47
- const { node, styleSetDefinitions: componentStyleSet = {}, propDefinitions = {} } = uidl
57
+ const {
58
+ node,
59
+ styleSetDefinitions: componentStyleSet = {},
60
+ propDefinitions = {},
61
+ stateDefinitions = {},
62
+ } = uidl
48
63
  const { projectStyleSet, designLanguage: { tokens = {} } = {}, isRootComponent } = options || {}
49
64
  const {
50
65
  styleSetDefinitions = {},
@@ -71,7 +86,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
71
86
  (chunk) => chunk.name === componentDecoratorChunkName
72
87
  )
73
88
 
74
- const templateLookup = templateChunk.meta.nodesLookup as Record<
89
+ const jsxNodesLookup = templateChunk.meta.nodesLookup as Record<
75
90
  string,
76
91
  HastNode | types.JSXElement
77
92
  >
@@ -86,27 +101,33 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
86
101
  Array<{ [x: string]: Record<string, string | number> }>
87
102
  > = {}
88
103
 
89
- UIDLUtils.traverseElements(node, (element) => {
104
+ const generateStylesForElementNode = (element: UIDLElement) => {
90
105
  const classNamesToAppend: Set<string> = new Set()
91
- const dynamicVariantsToAppend: Set<string> = new Set()
106
+ const dynamicVariantsToAppend: Set<string | types.ConditionalExpression> = new Set()
92
107
  const {
93
108
  style = {},
94
109
  key,
95
110
  referencedStyles = {},
96
- dependency,
97
111
  attrs = {},
98
112
  elementType,
113
+ dependency,
99
114
  } = element
100
- const elementClassName = StringUtils.camelCaseToDashCase(key)
101
115
 
102
- if (forceScoping && dependency?.type === 'local') {
116
+ const root = jsxNodesLookup[key]
117
+ if (!root) {
118
+ return
119
+ }
120
+
121
+ // Refer to line 323 all component scoped styles are appended with component name by default
122
+ if (dependency?.type === 'local') {
103
123
  StyleBuilders.setPropValueForCompStyle({
104
124
  attrs,
105
- key,
106
- jsxNodesLookup: templateLookup,
125
+ root,
107
126
  templateStyle,
127
+ // elementType is used here in-order to target the component name that the class is actually defined.
128
+ // Here we are appendigng to the node where the component is being called.
108
129
  getClassName: (styleName: string) =>
109
- getClassName(forceScoping, StringUtils.camelCaseToDashCase(elementType), styleName),
130
+ StringUtils.camelCaseToDashCase(elementType + styleName),
110
131
  })
111
132
  }
112
133
 
@@ -118,12 +139,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
118
139
  return
119
140
  }
120
141
 
121
- const root = templateLookup[key]
122
- if (!root) {
123
- throw new PluginCSS(`Node - ${key} is missing from the template chunk`)
124
- }
125
-
126
- const className = getClassName(forceScoping, uidl.name, elementClassName)
142
+ const className = StringUtils.camelCaseToDashCase(key)
127
143
 
128
144
  const { staticStyles, dynamicStyles, tokenStyles } =
129
145
  UIDLUtils.splitDynamicAndStaticStyles(style)
@@ -206,24 +222,21 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
206
222
  propDefinitions[styleRef.content.content.content.id]?.defaultValue
207
223
 
208
224
  if (defaultPropValue) {
209
- /* Changing the default value of the prop.
210
- When forceScoping is enabled the classnames change. So, we need to change the default prop too. */
211
- propDefinitions[styleRef.content.content.content.id].defaultValue = getClassName(
212
- forceScoping,
213
- uidl.name,
214
- String(defaultPropValue)
215
- )
225
+ propDefinitions[styleRef.content.content.content.id].defaultValue =
226
+ StringUtils.camelCaseToDashCase(String(defaultPropValue))
216
227
  }
217
228
 
229
+ // staticPropReferences flag is only used for just html-code generation.
230
+ // This is used to append the class name to the node where the component is being called.
231
+ // Instead of how frameworks handle them using props at runtime. This makes the props
232
+ // to behave statically during the generation time instead by appending them directly.
218
233
  if (staticPropReferences) {
219
- if (!defaultPropValue) {
234
+ if (defaultPropValue === undefined || typeof defaultPropValue !== 'string') {
220
235
  return
221
236
  }
222
- if (staticPropReferences) {
223
- classNamesToAppend.add(
224
- getClassName(forceScoping, uidl.name, String(defaultPropValue))
225
- )
226
- }
237
+ classNamesToAppend.add(
238
+ StringUtils.camelCaseToDashCase(uidl.name + defaultPropValue)
239
+ )
227
240
  } else {
228
241
  dynamicVariantsToAppend.add(styleRef.content.content.content.id)
229
242
  }
@@ -239,7 +252,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
239
252
  )
240
253
  }
241
254
  classNamesToAppend.add(
242
- getClassName(forceScoping, uidl.name, String(styleRef.content.content.content.id))
255
+ StringUtils.camelCaseToDashCase(uidl.name + styleRef.content.content.content.id)
243
256
  )
244
257
  }
245
258
 
@@ -255,7 +268,175 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
255
268
  )
256
269
  }
257
270
 
258
- classNamesToAppend.add(content.referenceId)
271
+ if (styleRef.content.condition) {
272
+ if (templateStyle === 'html') {
273
+ const {
274
+ value: staticValue,
275
+ reference,
276
+ expression: { conditions, matchingCriteria },
277
+ } = styleRef.content.condition
278
+
279
+ const {
280
+ content: { referenceType, id, refPath = [] },
281
+ } = reference
282
+
283
+ switch (referenceType) {
284
+ case 'prop': {
285
+ const usedProp = propDefinitions[id]
286
+ if (usedProp === undefined || usedProp.defaultValue === undefined) {
287
+ throw new PluginCSS(`Prop with ${id} is missing in the propDefinitions.`)
288
+ }
289
+
290
+ let defaultValue = usedProp.defaultValue
291
+ let operandDefaultValue: StateDefaultValueTypes | PropDefaultValueTypes | string
292
+ for (const path of refPath) {
293
+ defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
294
+ }
295
+
296
+ // If defaultValue is undefined or null after path traversal, use original default
297
+ defaultValue = defaultValue ?? usedProp.defaultValue
298
+
299
+ // our conditions can be of type 'expr' or they can be a dynamic reference
300
+ const rightSideCondition = conditions[0].operand
301
+ const rightSideConditionType = typeof rightSideCondition
302
+ if (rightSideConditionType === 'object') {
303
+ const type = (
304
+ rightSideCondition as UIDLDynamicReference | UIDLExpressionValue
305
+ ).type
306
+ if (type === 'dynamic') {
307
+ const dynamicRef = rightSideCondition as UIDLDynamicReference
308
+ if (dynamicRef.content.referenceType === 'prop') {
309
+ const prop = propDefinitions[dynamicRef.content.id]
310
+ if (prop && prop.defaultValue) {
311
+ operandDefaultValue = prop.defaultValue
312
+ }
313
+ }
314
+ if (dynamicRef.content.referenceType === 'state') {
315
+ const state = stateDefinitions[dynamicRef.content.id]
316
+ if (state && state.defaultValue) {
317
+ defaultValue = state.defaultValue
318
+ }
319
+ }
320
+ }
321
+ if (type === 'expr') {
322
+ operandDefaultValue = (rightSideCondition as UIDLExpressionValue).content
323
+ }
324
+ } else {
325
+ operandDefaultValue = rightSideCondition
326
+ }
327
+
328
+ // Since we know the operand and the default value from the prop.
329
+ // We can try building the condition and check if the condition is true or false.
330
+ // @todo: You can only use a 'value' in UIDL or 'conditions' but not both.
331
+ // UIDL validations need to be improved on this aspect.
332
+ const resolvedConditions = [
333
+ {
334
+ operation: conditions[0].operation,
335
+ operand: operandDefaultValue as string,
336
+ },
337
+ ]
338
+
339
+ const dynamicConditions = createConditionalStatement(
340
+ staticValue !== undefined
341
+ ? [{ operand: staticValue, operation: '===' }]
342
+ : resolvedConditions,
343
+ defaultValue
344
+ )
345
+ const matchCondition =
346
+ matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
347
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
348
+
349
+ // tslint:disable-next-line function-constructor
350
+ const isConditionPassing = new Function(`return ${conditionString}`)()
351
+ if (isConditionPassing) {
352
+ classNamesToAppend.add(styleRef.content.referenceId)
353
+ return
354
+ }
355
+ return
356
+ }
357
+ case 'state': {
358
+ const usedState = stateDefinitions[id]
359
+ if (usedState === undefined || usedState.defaultValue === undefined) {
360
+ throw new PluginCSS(`State with ${id} is missing in the stateDefinitions.`)
361
+ }
362
+ let defaultValue = usedState.defaultValue
363
+ for (const path of refPath) {
364
+ defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
365
+ }
366
+ // If defaultValue is undefined or null after path traversal, use original default
367
+ defaultValue = defaultValue ?? usedState.defaultValue
368
+ // Since we know the operand and the default value from the state.
369
+ // We can try building the condition and check if the condition is true or false.
370
+
371
+ const dynamicConditions = createConditionalStatement(
372
+ staticValue !== undefined
373
+ ? [{ operand: staticValue, operation: '===' }]
374
+ : conditions,
375
+ defaultValue
376
+ )
377
+ const matchCondition =
378
+ matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
379
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
380
+ // tslint:disable-next-line function-constructor
381
+ const isConditionPassing = new Function(`return ${conditionString}`)()
382
+ if (isConditionPassing) {
383
+ classNamesToAppend.add(styleRef.content.referenceId)
384
+ return
385
+ }
386
+ return
387
+ }
388
+
389
+ default: {
390
+ classNamesToAppend.add(styleRef.content.referenceId)
391
+ return
392
+ }
393
+ }
394
+ } else {
395
+ const nameToAppend = styleRef.content.condition.reference.content.id
396
+
397
+ const { conditions } = styleRef.content.condition.expression
398
+
399
+ const operator = conditions[0].operation as '===' | '!==' | '<' | '<=' | '>' | '>='
400
+ const right = conditions[0].operand as string | number | boolean
401
+ const referenceType = styleRef.content.condition.reference.content.referenceType
402
+
403
+ let binaryExpressionType = ''
404
+ switch (referenceType) {
405
+ case 'prop': {
406
+ binaryExpressionType = propDefinitions[nameToAppend].type
407
+ break
408
+ }
409
+ case 'state': {
410
+ binaryExpressionType = stateDefinitions[nameToAppend].type
411
+ break
412
+ }
413
+ default: {
414
+ throw new PluginCSS(
415
+ `Un-supported reference type ${referenceType} for ${nameToAppend}`
416
+ )
417
+ }
418
+ }
419
+
420
+ const binaryExpression = createBinaryExpression(
421
+ { operation: operator, operand: right },
422
+ {
423
+ key: (referenceType === 'prop' ? 'props?.' : '') + nameToAppend,
424
+ type: binaryExpressionType,
425
+ }
426
+ )
427
+
428
+ const conditionalExpression = types.conditionalExpression(
429
+ binaryExpression,
430
+ types.stringLiteral(content.referenceId),
431
+ types.stringLiteral('')
432
+ )
433
+
434
+ dynamicVariantsToAppend.add(conditionalExpression)
435
+ return
436
+ }
437
+ } else {
438
+ classNamesToAppend.add(content.referenceId)
439
+ }
259
440
  return
260
441
  }
261
442
 
@@ -296,18 +477,35 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
296
477
  ? types.identifier(dynamicVariantPrefix)
297
478
  : types.identifier(propsPrefix)
298
479
 
299
- return types.memberExpression(dynamicAttrValueIdentifier, types.identifier(variant))
480
+ if (typeof variant === 'string') {
481
+ return types.memberExpression(dynamicAttrValueIdentifier, types.identifier(variant))
482
+ } else {
483
+ // variant is a ConditionalExpression, so return it directly
484
+ return variant
485
+ }
300
486
  })
301
487
  )
302
488
  }
303
- })
489
+ }
490
+
491
+ UIDLUtils.traverseElements(node, generateStylesForElementNode)
492
+ for (const prop of Object.values(propDefinitions)) {
493
+ if (prop.type === 'element' && prop.defaultValue) {
494
+ UIDLUtils.traverseElements(
495
+ prop.defaultValue as UIDLElementNode,
496
+ generateStylesForElementNode
497
+ )
498
+ }
499
+ }
304
500
 
305
501
  if (Object.keys(componentStyleSet).length > 0) {
306
502
  StyleBuilders.generateStylesFromStyleSetDefinitions(
307
503
  componentStyleSet,
308
504
  cssMap,
309
505
  mediaStylesMap,
310
- (styleName: string) => getClassName(forceScoping, uidl.name, styleName)
506
+ (styleName: string) => {
507
+ return StringUtils.camelCaseToDashCase(uidl.name + styleName)
508
+ }
311
509
  )
312
510
  }
313
511
 
@@ -348,7 +546,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
348
546
  type: ChunkType.STRING,
349
547
  name: chunkName,
350
548
  fileType: FileType.CSS,
351
- content: cssMap.join('\n'),
549
+ content: cssMap.join('\n \n'),
352
550
  linkAfter: [],
353
551
  })
354
552
  }
@@ -370,9 +568,3 @@ const createDynamicInlineStyle = (styles: UIDLStyleDefinitions) => {
370
568
  })
371
569
  .join(', ')
372
570
  }
373
-
374
- const getClassName = (scoping: boolean, uidlName: string, nodeStyleName: string) => {
375
- return scoping
376
- ? StringUtils.camelCaseToDashCase(`${uidlName}-${nodeStyleName}`)
377
- : StringUtils.camelCaseToDashCase(nodeStyleName)
378
- }
@@ -5,22 +5,42 @@ import {
5
5
  ChunkType,
6
6
  FileType,
7
7
  } from '@teleporthq/teleport-types'
8
+ import { join, relative } from 'path'
8
9
 
9
10
  interface StyleSheetPlugin {
10
11
  fileName?: string
12
+ /*
13
+ Frameworks with dev servers don't need relative path mapping.
14
+ */
15
+ relativeFontPath?: boolean
11
16
  }
12
17
 
13
18
  export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> = (config) => {
14
- const { fileName } = config || { fileName: 'style' }
19
+ const { fileName = 'style', relativeFontPath = false } = config || {
20
+ fileName: 'style',
21
+ relativeFontPath: false,
22
+ }
23
+
15
24
  const styleSheetPlugin: ComponentPlugin = async (structure) => {
16
- const { uidl, chunks } = structure
25
+ const {
26
+ uidl,
27
+ chunks,
28
+ options: { assets },
29
+ } = structure
17
30
  const { styleSetDefinitions = {}, designLanguage: { tokens = {} } = {} } = uidl
31
+ const {
32
+ localFonts = [],
33
+ fontsFolder,
34
+ identifier: assetIdentifier,
35
+ } = assets || { localFonts: [] }
18
36
 
19
- if (
20
- (!styleSetDefinitions && !tokens) ||
21
- (Object.keys(styleSetDefinitions).length === 0 && Object.keys(tokens).length === 0)
22
- ) {
23
- return
37
+ const canSkipThePlugin =
38
+ localFonts?.length === 0 &&
39
+ Object.keys(styleSetDefinitions).length === 0 &&
40
+ Object.keys(tokens).length === 0
41
+
42
+ if (canSkipThePlugin) {
43
+ return structure
24
44
  }
25
45
 
26
46
  const cssMap: string[] = []
@@ -29,6 +49,35 @@ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> =
29
49
  Array<{ [x: string]: Record<string, string | number> }>
30
50
  > = {}
31
51
 
52
+ uidl.outputOptions = uidl.outputOptions || {}
53
+ uidl.outputOptions.styleFileName = fileName
54
+
55
+ if (localFonts.length > 0) {
56
+ localFonts.forEach((font) => {
57
+ const properties = StyleUtils.getContentOfStyleObject(font.properties) as Record<
58
+ string,
59
+ string | number
60
+ >
61
+ const format = properties?.format ? `format('${properties.format}')` : ''
62
+ /* tslint:disable:no-string-literal */
63
+ delete properties['format']
64
+
65
+ const fontPath = relativeFontPath
66
+ ? join(
67
+ relative(join(...(uidl.outputOptions?.folderPath || [])), './'),
68
+ join(fontsFolder || '', font.path)
69
+ )
70
+ : join('/', assetIdentifier || '', 'fonts', font.path)
71
+
72
+ cssMap.push(
73
+ StyleBuilders.createFontDecleration({
74
+ ...properties,
75
+ src: `url("${fontPath}") ${format}`,
76
+ })
77
+ )
78
+ })
79
+ }
80
+
32
81
  if (Object.keys(tokens).length > 0) {
33
82
  cssMap.push(
34
83
  StyleBuilders.createCSSClassWithSelector(
@@ -44,7 +93,7 @@ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> =
44
93
  styleSetDefinitions,
45
94
  cssMap,
46
95
  mediaStylesMap,
47
- (styleName: string) => styleName
96
+ (styleName) => styleName
48
97
  )
49
98
  }
50
99
 
@@ -54,14 +103,11 @@ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> =
54
103
  return structure
55
104
  }
56
105
 
57
- uidl.outputOptions = uidl.outputOptions || {}
58
- uidl.outputOptions.styleFileName = fileName
59
-
60
106
  chunks.push({
61
107
  name: fileName,
62
108
  type: ChunkType.STRING,
63
109
  fileType: FileType.CSS,
64
- content: cssMap.join('\n'),
110
+ content: cssMap.join('\n \n'),
65
111
  linkAfter: [],
66
112
  })
67
113
 
package/src/utils.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { ASTUtils } from '@teleporthq/teleport-plugin-common'
2
+ import { PluginCSS, UIDLConditionalNode, UIDLPropDefinition } from '@teleporthq/teleport-types'
3
+
4
+ export const createConditionalStatement = (
5
+ conditions: UIDLConditionalNode['content']['condition']['conditions'],
6
+ leftOperand: UIDLPropDefinition['defaultValue']
7
+ ) => {
8
+ return conditions.map((condition) => {
9
+ const { operation, operand } = condition
10
+
11
+ if (operand === undefined) {
12
+ return `${ASTUtils.convertToUnaryOperator(operation)}${getValueType(operand)}`
13
+ }
14
+
15
+ return `${getValueType(leftOperand)} ${ASTUtils.convertToBinaryOperator(
16
+ operation
17
+ )} ${getValueType(operand)}`
18
+ })
19
+ }
20
+
21
+ const getValueType = (value: UIDLPropDefinition['defaultValue']) => {
22
+ const valueType = typeof value
23
+ switch (valueType) {
24
+ case 'string':
25
+ return `"${value}"`
26
+ case 'number':
27
+ return value
28
+ case 'boolean':
29
+ return value
30
+ default:
31
+ throw new PluginCSS(
32
+ `Conditional node received an operand of type ${valueType} \n
33
+ Received ${JSON.stringify(value)}`
34
+ )
35
+ }
36
+ }