@teleporthq/teleport-plugin-css 0.40.0-alpha.0 → 0.40.1

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,11 @@ import {
12
18
  UIDLElementNodeReferenceStyles,
13
19
  UIDLStyleMediaQueryScreenSizeCondition,
14
20
  PluginCSS,
21
+ UIDLElement,
22
+ UIDLElementNode,
15
23
  } from '@teleporthq/teleport-types'
16
24
  import { createStyleSheetPlugin } from './style-sheet'
25
+ import { createConditionalStatement } from './utils'
17
26
 
18
27
  interface CSSPluginConfig {
19
28
  chunkName: string
@@ -21,7 +30,6 @@ interface CSSPluginConfig {
21
30
  componentDecoratorChunkName: string
22
31
  inlineStyleAttributeKey: string // style vs :style vs ...
23
32
  classAttributeName: string // class vs className
24
- forceScoping: boolean // class names get the component name prefix
25
33
  templateStyle: 'html' | 'jsx'
26
34
  declareDependency: 'import' | 'decorator' | 'none'
27
35
  dynamicVariantPrefix?: string
@@ -37,14 +45,18 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
37
45
  classAttributeName = 'class',
38
46
  templateStyle = 'html',
39
47
  declareDependency = 'none',
40
- forceScoping = false,
41
48
  dynamicVariantPrefix,
42
49
  staticPropReferences = false,
43
50
  } = config || {}
44
51
 
45
52
  const cssPlugin: ComponentPlugin = async (structure) => {
46
53
  const { uidl, chunks, dependencies, options } = structure
47
- const { node, styleSetDefinitions: componentStyleSet = {}, propDefinitions = {} } = uidl
54
+ const {
55
+ node,
56
+ styleSetDefinitions: componentStyleSet = {},
57
+ propDefinitions = {},
58
+ stateDefinitions = {},
59
+ } = uidl
48
60
  const { projectStyleSet, designLanguage: { tokens = {} } = {}, isRootComponent } = options || {}
49
61
  const {
50
62
  styleSetDefinitions = {},
@@ -71,7 +83,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
71
83
  (chunk) => chunk.name === componentDecoratorChunkName
72
84
  )
73
85
 
74
- const templateLookup = templateChunk.meta.nodesLookup as Record<
86
+ const jsxNodesLookup = templateChunk.meta.nodesLookup as Record<
75
87
  string,
76
88
  HastNode | types.JSXElement
77
89
  >
@@ -86,27 +98,33 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
86
98
  Array<{ [x: string]: Record<string, string | number> }>
87
99
  > = {}
88
100
 
89
- UIDLUtils.traverseElements(node, (element) => {
101
+ const generateStylesForElementNode = (element: UIDLElement) => {
90
102
  const classNamesToAppend: Set<string> = new Set()
91
- const dynamicVariantsToAppend: Set<string> = new Set()
103
+ const dynamicVariantsToAppend: Set<string | types.ConditionalExpression> = new Set()
92
104
  const {
93
105
  style = {},
94
106
  key,
95
107
  referencedStyles = {},
96
- dependency,
97
108
  attrs = {},
98
109
  elementType,
110
+ dependency,
99
111
  } = element
100
- const elementClassName = StringUtils.camelCaseToDashCase(key)
101
112
 
102
- if (forceScoping && dependency?.type === 'local') {
113
+ const root = jsxNodesLookup[key]
114
+ if (!root) {
115
+ return
116
+ }
117
+
118
+ // Refer to line 323 all component scoped styles are appended with component name by default
119
+ if (dependency?.type === 'local') {
103
120
  StyleBuilders.setPropValueForCompStyle({
104
121
  attrs,
105
- key,
106
- jsxNodesLookup: templateLookup,
122
+ root,
107
123
  templateStyle,
124
+ // elementType is used here in-order to target the component name that the class is actually defined.
125
+ // Here we are appendigng to the node where the component is being called.
108
126
  getClassName: (styleName: string) =>
109
- getClassName(forceScoping, StringUtils.camelCaseToDashCase(elementType), styleName),
127
+ StringUtils.camelCaseToDashCase(elementType + styleName),
110
128
  })
111
129
  }
112
130
 
@@ -118,12 +136,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
118
136
  return
119
137
  }
120
138
 
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)
139
+ const className = StringUtils.camelCaseToDashCase(key)
127
140
 
128
141
  const { staticStyles, dynamicStyles, tokenStyles } =
129
142
  UIDLUtils.splitDynamicAndStaticStyles(style)
@@ -206,24 +219,21 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
206
219
  propDefinitions[styleRef.content.content.content.id]?.defaultValue
207
220
 
208
221
  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
- )
222
+ propDefinitions[styleRef.content.content.content.id].defaultValue =
223
+ StringUtils.camelCaseToDashCase(String(defaultPropValue))
216
224
  }
217
225
 
226
+ // staticPropReferences flag is only used for just html-code generation.
227
+ // This is used to append the class name to the node where the component is being called.
228
+ // Instead of how frameworks handle them using props at runtime. This makes the props
229
+ // to behave statically during the generation time instead by appending them directly.
218
230
  if (staticPropReferences) {
219
- if (!defaultPropValue) {
231
+ if (defaultPropValue === undefined || typeof defaultPropValue !== 'string') {
220
232
  return
221
233
  }
222
- if (staticPropReferences) {
223
- classNamesToAppend.add(
224
- getClassName(forceScoping, uidl.name, String(defaultPropValue))
225
- )
226
- }
234
+ classNamesToAppend.add(
235
+ StringUtils.camelCaseToDashCase(uidl.name + defaultPropValue)
236
+ )
227
237
  } else {
228
238
  dynamicVariantsToAppend.add(styleRef.content.content.content.id)
229
239
  }
@@ -239,7 +249,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
239
249
  )
240
250
  }
241
251
  classNamesToAppend.add(
242
- getClassName(forceScoping, uidl.name, String(styleRef.content.content.content.id))
252
+ StringUtils.camelCaseToDashCase(uidl.name + styleRef.content.content.content.id)
243
253
  )
244
254
  }
245
255
 
@@ -255,7 +265,138 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
255
265
  )
256
266
  }
257
267
 
258
- classNamesToAppend.add(content.referenceId)
268
+ if (styleRef.content.condition) {
269
+ if (templateStyle === 'html') {
270
+ const {
271
+ value: staticValue,
272
+ reference,
273
+ expression: { conditions, matchingCriteria },
274
+ } = styleRef.content.condition
275
+
276
+ const {
277
+ content: { referenceType, id, refPath = [] },
278
+ } = reference
279
+
280
+ switch (referenceType) {
281
+ case 'prop': {
282
+ const usedProp = propDefinitions[id]
283
+ if (usedProp === undefined || usedProp.defaultValue === undefined) {
284
+ throw new PluginCSS(`Prop with ${id} is missing in the propDefinitions.`)
285
+ }
286
+
287
+ let defaultValue = usedProp.defaultValue
288
+ for (const path of refPath) {
289
+ defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
290
+ }
291
+
292
+ // If defaultValue is undefined or null after path traversal, use original default
293
+ defaultValue = defaultValue ?? usedProp.defaultValue
294
+
295
+ // Since we know the operand and the default value from the prop.
296
+ // We can try building the condition and check if the condition is true or false.
297
+ // @todo: You can only use a 'value' in UIDL or 'conditions' but not both.
298
+ // UIDL validations need to be improved on this aspect.
299
+ const dynamicConditions = createConditionalStatement(
300
+ staticValue !== undefined
301
+ ? [{ operand: staticValue, operation: '===' }]
302
+ : conditions,
303
+ defaultValue
304
+ )
305
+ const matchCondition =
306
+ matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
307
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
308
+
309
+ // tslint:disable-next-line function-constructor
310
+ const isConditionPassing = new Function(`return ${conditionString}`)()
311
+ if (isConditionPassing) {
312
+ classNamesToAppend.add(styleRef.content.referenceId)
313
+ return
314
+ }
315
+ return
316
+ }
317
+ case 'state': {
318
+ const usedState = stateDefinitions[id]
319
+ if (usedState === undefined || usedState.defaultValue === undefined) {
320
+ throw new PluginCSS(`State with ${id} is missing in the stateDefinitions.`)
321
+ }
322
+ let defaultValue = usedState.defaultValue
323
+ for (const path of refPath) {
324
+ defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
325
+ }
326
+ // If defaultValue is undefined or null after path traversal, use original default
327
+ defaultValue = defaultValue ?? usedState.defaultValue
328
+ // Since we know the operand and the default value from the state.
329
+ // We can try building the condition and check if the condition is true or false.
330
+
331
+ const dynamicConditions = createConditionalStatement(
332
+ staticValue !== undefined
333
+ ? [{ operand: staticValue, operation: '===' }]
334
+ : conditions,
335
+ defaultValue
336
+ )
337
+ const matchCondition =
338
+ matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
339
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
340
+ // tslint:disable-next-line function-constructor
341
+ const isConditionPassing = new Function(`return ${conditionString}`)()
342
+ if (isConditionPassing) {
343
+ classNamesToAppend.add(styleRef.content.referenceId)
344
+ return
345
+ }
346
+ return
347
+ }
348
+
349
+ default: {
350
+ classNamesToAppend.add(styleRef.content.referenceId)
351
+ return
352
+ }
353
+ }
354
+ } else {
355
+ const nameToAppend = styleRef.content.condition.reference.content.id
356
+
357
+ const { conditions } = styleRef.content.condition.expression
358
+
359
+ const operator = conditions[0].operation as '===' | '!==' | '<' | '<=' | '>' | '>='
360
+ const right = conditions[0].operand as string | number | boolean
361
+ const referenceType = styleRef.content.condition.reference.content.referenceType
362
+
363
+ let binaryExpressionType = ''
364
+ switch (referenceType) {
365
+ case 'prop': {
366
+ binaryExpressionType = propDefinitions[nameToAppend].type
367
+ break
368
+ }
369
+ case 'state': {
370
+ binaryExpressionType = stateDefinitions[nameToAppend].type
371
+ break
372
+ }
373
+ default: {
374
+ throw new PluginCSS(
375
+ `Un-supported reference type ${referenceType} for ${nameToAppend}`
376
+ )
377
+ }
378
+ }
379
+
380
+ const binaryExpression = createBinaryExpression(
381
+ { operation: operator, operand: right },
382
+ {
383
+ key: (referenceType === 'prop' ? 'props?.' : '') + nameToAppend,
384
+ type: binaryExpressionType,
385
+ }
386
+ )
387
+
388
+ const conditionalExpression = types.conditionalExpression(
389
+ binaryExpression,
390
+ types.stringLiteral(content.referenceId),
391
+ types.stringLiteral('')
392
+ )
393
+
394
+ dynamicVariantsToAppend.add(conditionalExpression)
395
+ return
396
+ }
397
+ } else {
398
+ classNamesToAppend.add(content.referenceId)
399
+ }
259
400
  return
260
401
  }
261
402
 
@@ -296,18 +437,35 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
296
437
  ? types.identifier(dynamicVariantPrefix)
297
438
  : types.identifier(propsPrefix)
298
439
 
299
- return types.memberExpression(dynamicAttrValueIdentifier, types.identifier(variant))
440
+ if (typeof variant === 'string') {
441
+ return types.memberExpression(dynamicAttrValueIdentifier, types.identifier(variant))
442
+ } else {
443
+ // variant is a ConditionalExpression, so return it directly
444
+ return variant
445
+ }
300
446
  })
301
447
  )
302
448
  }
303
- })
449
+ }
450
+
451
+ UIDLUtils.traverseElements(node, generateStylesForElementNode)
452
+ for (const prop of Object.values(propDefinitions)) {
453
+ if (prop.type === 'element' && prop.defaultValue) {
454
+ UIDLUtils.traverseElements(
455
+ prop.defaultValue as UIDLElementNode,
456
+ generateStylesForElementNode
457
+ )
458
+ }
459
+ }
304
460
 
305
461
  if (Object.keys(componentStyleSet).length > 0) {
306
462
  StyleBuilders.generateStylesFromStyleSetDefinitions(
307
463
  componentStyleSet,
308
464
  cssMap,
309
465
  mediaStylesMap,
310
- (styleName: string) => getClassName(forceScoping, uidl.name, styleName)
466
+ (styleName: string) => {
467
+ return StringUtils.camelCaseToDashCase(uidl.name + styleName)
468
+ }
311
469
  )
312
470
  }
313
471
 
@@ -348,7 +506,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
348
506
  type: ChunkType.STRING,
349
507
  name: chunkName,
350
508
  fileType: FileType.CSS,
351
- content: cssMap.join('\n'),
509
+ content: cssMap.join('\n \n'),
352
510
  linkAfter: [],
353
511
  })
354
512
  }
@@ -370,9 +528,3 @@ const createDynamicInlineStyle = (styles: UIDLStyleDefinitions) => {
370
528
  })
371
529
  .join(', ')
372
530
  }
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
+ }