@teleporthq/teleport-plugin-css 0.43.0-alpha.0 → 0.43.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,13 @@
1
+ import { join, relative } from 'path'
1
2
  import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
2
- import { StyleUtils, StyleBuilders, HASTUtils, ASTUtils } from '@teleporthq/teleport-plugin-common'
3
+ import {
4
+ StyleUtils,
5
+ StyleBuilders,
6
+ HASTUtils,
7
+ HASTBuilders,
8
+ ASTUtils,
9
+ createBinaryExpression,
10
+ } from '@teleporthq/teleport-plugin-common'
3
11
  import * as types from '@babel/types'
4
12
  import {
5
13
  ComponentPluginFactory,
@@ -12,8 +20,17 @@ import {
12
20
  UIDLElementNodeReferenceStyles,
13
21
  UIDLStyleMediaQueryScreenSizeCondition,
14
22
  PluginCSS,
23
+ UIDLElement,
24
+ UIDLElementNode,
25
+ UIDLExpressionValue,
26
+ StateDefaultValueTypes,
27
+ PropDefaultValueTypes,
28
+ UIDLStyleInlineAsset,
29
+ UIDLFontAsset,
30
+ UIDLGlobalStateDefinition,
15
31
  } from '@teleporthq/teleport-types'
16
32
  import { createStyleSheetPlugin } from './style-sheet'
33
+ import { createConditionalStatement } from './utils'
17
34
 
18
35
  interface CSSPluginConfig {
19
36
  chunkName: string
@@ -21,11 +38,30 @@ interface CSSPluginConfig {
21
38
  componentDecoratorChunkName: string
22
39
  inlineStyleAttributeKey: string // style vs :style vs ...
23
40
  classAttributeName: string // class vs className
24
- forceScoping: boolean // class names get the component name prefix
25
41
  templateStyle: 'html' | 'jsx'
26
42
  declareDependency: 'import' | 'decorator' | 'none'
27
43
  dynamicVariantPrefix?: string
28
44
  staticPropReferences?: boolean
45
+ standaloneHtmlComponents?: boolean
46
+ }
47
+
48
+ const prefixUrlPathsInCss = (css: string, folderPath: string[]): string => {
49
+ const relativePrefix = relative(join(...folderPath), './')
50
+ if (!relativePrefix) {
51
+ return css
52
+ }
53
+
54
+ return css.replace(/url\(["']?(.*?)["']?\)/g, (match, url) => {
55
+ if (
56
+ url.startsWith('http') ||
57
+ url.startsWith('data:') ||
58
+ url.startsWith('../') ||
59
+ url.startsWith('#')
60
+ ) {
61
+ return match
62
+ }
63
+ return `url("${join(relativePrefix, url)}")`
64
+ })
29
65
  }
30
66
 
31
67
  const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
@@ -37,15 +73,25 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
37
73
  classAttributeName = 'class',
38
74
  templateStyle = 'html',
39
75
  declareDependency = 'none',
40
- forceScoping = false,
41
76
  dynamicVariantPrefix,
42
77
  staticPropReferences = false,
78
+ standaloneHtmlComponents = false,
43
79
  } = config || {}
44
80
 
45
81
  const cssPlugin: ComponentPlugin = async (structure) => {
46
82
  const { uidl, chunks, dependencies, options } = structure
47
- const { node, styleSetDefinitions: componentStyleSet = {}, propDefinitions = {} } = uidl
48
- const { projectStyleSet, designLanguage: { tokens = {} } = {}, isRootComponent } = options || {}
83
+ const {
84
+ node,
85
+ styleSetDefinitions: componentStyleSet = {},
86
+ propDefinitions = {},
87
+ stateDefinitions = {},
88
+ } = uidl
89
+ const {
90
+ projectStyleSet,
91
+ designLanguage: { tokens = {} } = {},
92
+ isRootComponent,
93
+ globalStateDefinitions = {},
94
+ } = options || {}
49
95
  const {
50
96
  styleSetDefinitions = {},
51
97
  fileName: projectStyleSheetName,
@@ -71,7 +117,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
71
117
  (chunk) => chunk.name === componentDecoratorChunkName
72
118
  )
73
119
 
74
- const templateLookup = templateChunk.meta.nodesLookup as Record<
120
+ const jsxNodesLookup = templateChunk.meta.nodesLookup as Record<
75
121
  string,
76
122
  HastNode | types.JSXElement
77
123
  >
@@ -85,45 +131,51 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
85
131
  string,
86
132
  Array<{ [x: string]: Record<string, string | number> }>
87
133
  > = {}
134
+ const usedProjectStyleIds: Set<string> = new Set()
88
135
 
89
- UIDLUtils.traverseElements(node, (element) => {
136
+ const generateStylesForElementNode = (element: UIDLElement) => {
90
137
  const classNamesToAppend: Set<string> = new Set()
91
- const dynamicVariantsToAppend: Set<string> = new Set()
138
+ const dynamicVariantsToAppend: Set<string | types.ConditionalExpression> = new Set()
92
139
  const {
93
140
  style = {},
94
141
  key,
95
142
  referencedStyles = {},
96
- dependency,
97
143
  attrs = {},
98
144
  elementType,
145
+ dependency,
99
146
  } = element
100
- const elementClassName = StringUtils.camelCaseToDashCase(key)
147
+ const { dynamicStyleBindings } = element
148
+ const hasDynamicBindings =
149
+ dynamicStyleBindings && Object.keys(dynamicStyleBindings).length > 0
101
150
 
102
- if (forceScoping && dependency?.type === 'local') {
151
+ const root = jsxNodesLookup[key]
152
+ if (!root) {
153
+ return
154
+ }
155
+
156
+ // Refer to line 323 all component scoped styles are appended with component name by default
157
+ if (dependency?.type === 'local') {
103
158
  StyleBuilders.setPropValueForCompStyle({
104
159
  attrs,
105
- key,
106
- jsxNodesLookup: templateLookup,
160
+ root,
107
161
  templateStyle,
162
+ // elementType is used here in-order to target the component name that the class is actually defined.
163
+ // Here we are appendigng to the node where the component is being called.
108
164
  getClassName: (styleName: string) =>
109
- getClassName(forceScoping, StringUtils.camelCaseToDashCase(elementType), styleName),
165
+ StringUtils.camelCaseToDashCase(elementType + styleName),
110
166
  })
111
167
  }
112
168
 
113
169
  if (
114
170
  Object.keys(style).length === 0 &&
115
171
  Object.keys(referencedStyles).length === 0 &&
116
- Object.keys(componentStyleSet).length === 0
172
+ Object.keys(componentStyleSet).length === 0 &&
173
+ !hasDynamicBindings
117
174
  ) {
118
175
  return
119
176
  }
120
177
 
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)
178
+ const className = StringUtils.camelCaseToDashCase(key)
127
179
 
128
180
  const { staticStyles, dynamicStyles, tokenStyles } =
129
181
  UIDLUtils.splitDynamicAndStaticStyles(style)
@@ -138,28 +190,58 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
138
190
  classNamesToAppend.add(className)
139
191
  }
140
192
 
193
+ let jsxInlineStyles: Record<string, unknown> | null = null
194
+
141
195
  if (Object.keys(dynamicStyles).length > 0) {
196
+ for (const styleValue of Object.values(dynamicStyles)) {
197
+ if (
198
+ styleValue.type === 'dynamic' &&
199
+ styleValue.content.referenceType === 'global' &&
200
+ templateChunk.meta.globalReferences
201
+ ) {
202
+ ;(templateChunk.meta.globalReferences as string[]).push(styleValue.content.id)
203
+ }
204
+ }
205
+
142
206
  /* If dynamic styles are on nested-styles they are unfortunately lost,
143
207
  since inline style does not support that */
144
208
  if (templateStyle === 'html') {
145
209
  const inlineStyles = createDynamicInlineStyle(dynamicStyles)
146
- HASTUtils.addAttributeToNode(
147
- root as HastNode,
148
- inlineStyleAttributeKey,
149
- `{${inlineStyles}}`
150
- )
210
+ const bindingParts = hasDynamicBindings
211
+ ? createDynamicBindingInlineStyle(dynamicStyleBindings)
212
+ : ''
213
+ const combined = bindingParts ? `${inlineStyles}, ${bindingParts}` : inlineStyles
214
+ HASTUtils.addAttributeToNode(root as HastNode, inlineStyleAttributeKey, `{${combined}}`)
151
215
  } else {
152
- const inlineStyles = UIDLUtils.transformDynamicStyles(dynamicStyles, (styleValue) =>
216
+ jsxInlineStyles = UIDLUtils.transformDynamicStyles(dynamicStyles, (styleValue) =>
153
217
  StyleBuilders.createDynamicStyleExpression(styleValue, propsPrefix)
154
218
  )
155
- ASTUtils.addAttributeToJSXTag(
156
- root as types.JSXElement,
157
- inlineStyleAttributeKey,
158
- inlineStyles
219
+ }
220
+ } else if (hasDynamicBindings && templateStyle === 'html') {
221
+ const bindingParts = createDynamicBindingInlineStyle(dynamicStyleBindings)
222
+ HASTUtils.addAttributeToNode(root as HastNode, inlineStyleAttributeKey, `{${bindingParts}}`)
223
+ }
224
+
225
+ if (hasDynamicBindings && templateStyle !== 'html') {
226
+ if (!jsxInlineStyles) {
227
+ jsxInlineStyles = {}
228
+ }
229
+ for (const [cssProperty, binding] of Object.entries(dynamicStyleBindings)) {
230
+ const camelCaseProperty = cssProperty.replace(/-([a-z])/g, (_, letter: string) =>
231
+ letter.toUpperCase()
159
232
  )
233
+ jsxInlineStyles[camelCaseProperty] = StyleBuilders.createDynamicBindingExpression(binding)
160
234
  }
161
235
  }
162
236
 
237
+ if (jsxInlineStyles) {
238
+ ASTUtils.addAttributeToJSXTag(
239
+ root as types.JSXElement,
240
+ inlineStyleAttributeKey,
241
+ jsxInlineStyles
242
+ )
243
+ }
244
+
163
245
  Object.values(referencedStyles).forEach((styleRef: UIDLElementNodeReferenceStyles) => {
164
246
  switch (styleRef.content.mapType) {
165
247
  case 'inlined': {
@@ -206,24 +288,21 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
206
288
  propDefinitions[styleRef.content.content.content.id]?.defaultValue
207
289
 
208
290
  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
- )
291
+ propDefinitions[styleRef.content.content.content.id].defaultValue =
292
+ StringUtils.camelCaseToDashCase(String(defaultPropValue))
216
293
  }
217
294
 
295
+ // staticPropReferences flag is only used for just html-code generation.
296
+ // This is used to append the class name to the node where the component is being called.
297
+ // Instead of how frameworks handle them using props at runtime. This makes the props
298
+ // to behave statically during the generation time instead by appending them directly.
218
299
  if (staticPropReferences) {
219
- if (!defaultPropValue) {
300
+ if (defaultPropValue === undefined || typeof defaultPropValue !== 'string') {
220
301
  return
221
302
  }
222
- if (staticPropReferences) {
223
- classNamesToAppend.add(
224
- getClassName(forceScoping, uidl.name, String(defaultPropValue))
225
- )
226
- }
303
+ classNamesToAppend.add(
304
+ StringUtils.camelCaseToDashCase(uidl.name + defaultPropValue)
305
+ )
227
306
  } else {
228
307
  dynamicVariantsToAppend.add(styleRef.content.content.content.id)
229
308
  }
@@ -239,7 +318,7 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
239
318
  )
240
319
  }
241
320
  classNamesToAppend.add(
242
- getClassName(forceScoping, uidl.name, String(styleRef.content.content.content.id))
321
+ StringUtils.camelCaseToDashCase(uidl.name + styleRef.content.content.content.id)
243
322
  )
244
323
  }
245
324
 
@@ -250,12 +329,246 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
250
329
  const { content } = styleRef
251
330
  const referedStyle = styleSetDefinitions[content.referenceId]
252
331
  if (!referedStyle) {
253
- throw new PluginCSS(
254
- `Style used from global stylesheet is missing - ${content.referenceId}`
255
- )
332
+ return
256
333
  }
257
334
 
258
- classNamesToAppend.add(content.referenceId)
335
+ usedProjectStyleIds.add(content.referenceId)
336
+
337
+ if (styleRef.content.condition) {
338
+ if (templateStyle === 'html') {
339
+ const {
340
+ value: staticValue,
341
+ reference,
342
+ expression: { conditions, matchingCriteria },
343
+ } = styleRef.content.condition
344
+
345
+ const {
346
+ content: { referenceType, id, refPath = [] },
347
+ } = reference
348
+
349
+ switch (referenceType) {
350
+ case 'prop': {
351
+ const usedProp = propDefinitions[id]
352
+ if (usedProp === undefined || usedProp.defaultValue === undefined) {
353
+ throw new PluginCSS(`Prop with ${id} is missing in the propDefinitions.`)
354
+ }
355
+
356
+ let defaultValue = usedProp.defaultValue
357
+ let operandDefaultValue: StateDefaultValueTypes | PropDefaultValueTypes | string
358
+ for (const path of refPath) {
359
+ defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
360
+ }
361
+
362
+ // If defaultValue is undefined or null after path traversal, use original default
363
+ defaultValue = defaultValue ?? usedProp.defaultValue
364
+
365
+ // our conditions can be of type 'expr' or they can be a dynamic reference
366
+ const rightSideCondition = conditions[0].operand
367
+ const rightSideConditionType = typeof rightSideCondition
368
+ if (rightSideConditionType === 'object') {
369
+ const type = (
370
+ rightSideCondition as UIDLDynamicReference | UIDLExpressionValue
371
+ ).type
372
+ if (type === 'dynamic') {
373
+ const dynamicRef = rightSideCondition as UIDLDynamicReference
374
+ if (dynamicRef.content.referenceType === 'prop') {
375
+ const prop = propDefinitions[dynamicRef.content.id]
376
+ if (prop && prop.defaultValue) {
377
+ operandDefaultValue = prop.defaultValue
378
+ }
379
+ }
380
+ if (dynamicRef.content.referenceType === 'state') {
381
+ const state = stateDefinitions[dynamicRef.content.id]
382
+ if (state && state.defaultValue) {
383
+ defaultValue = state.defaultValue
384
+ }
385
+ }
386
+ }
387
+ if (type === 'expr') {
388
+ operandDefaultValue = (rightSideCondition as UIDLExpressionValue).content
389
+ }
390
+ } else {
391
+ operandDefaultValue = rightSideCondition
392
+ }
393
+
394
+ // Since we know the operand and the default value from the prop.
395
+ // We can try building the condition and check if the condition is true or false.
396
+ // @todo: You can only use a 'value' in UIDL or 'conditions' but not both.
397
+ // UIDL validations need to be improved on this aspect.
398
+ const resolvedConditions = [
399
+ {
400
+ operation: conditions[0].operation,
401
+ operand: operandDefaultValue as string,
402
+ },
403
+ ]
404
+
405
+ const dynamicConditions = createConditionalStatement(
406
+ staticValue !== undefined
407
+ ? [{ operand: staticValue, operation: '===' }]
408
+ : resolvedConditions,
409
+ defaultValue
410
+ )
411
+ const matchCondition =
412
+ matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
413
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
414
+
415
+ // tslint:disable-next-line function-constructor
416
+ const isConditionPassing = new Function(`return ${conditionString}`)()
417
+ if (isConditionPassing) {
418
+ classNamesToAppend.add(styleRef.content.referenceId)
419
+ return
420
+ }
421
+ return
422
+ }
423
+ case 'state': {
424
+ const usedState = stateDefinitions[id]
425
+ if (usedState === undefined || usedState.defaultValue === undefined) {
426
+ throw new PluginCSS(`State with ${id} is missing in the stateDefinitions.`)
427
+ }
428
+ let defaultValue = usedState.defaultValue
429
+ for (const path of refPath) {
430
+ defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
431
+ }
432
+ if (
433
+ typeof defaultValue === 'object' &&
434
+ defaultValue !== null &&
435
+ 'type' in (defaultValue as Record<string, unknown>) &&
436
+ 'content' in (defaultValue as Record<string, unknown>)
437
+ ) {
438
+ const entry = defaultValue as { type: string; content: unknown }
439
+ if (entry.type === 'static') {
440
+ defaultValue = entry.content as typeof defaultValue
441
+ }
442
+ }
443
+ defaultValue = defaultValue ?? usedState.defaultValue
444
+
445
+ const dynamicConditions = createConditionalStatement(
446
+ staticValue !== undefined
447
+ ? [{ operand: staticValue, operation: '===' }]
448
+ : conditions,
449
+ defaultValue
450
+ )
451
+ const matchCondition =
452
+ matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
453
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
454
+ // tslint:disable-next-line function-constructor
455
+ const isConditionPassing = new Function(`return ${conditionString}`)()
456
+ if (isConditionPassing) {
457
+ classNamesToAppend.add(styleRef.content.referenceId)
458
+ return
459
+ }
460
+ return
461
+ }
462
+
463
+ case 'globalState' as any: {
464
+ const gsDef = findGlobalStateDefinitionById(id, globalStateDefinitions)
465
+ if (!gsDef) {
466
+ classNamesToAppend.add(styleRef.content.referenceId)
467
+ return
468
+ }
469
+ let defaultValue: StateDefaultValueTypes | PropDefaultValueTypes =
470
+ gsDef.defaultValue as StateDefaultValueTypes
471
+ for (const path of refPath) {
472
+ defaultValue = (defaultValue as Record<string, unknown>)?.[
473
+ path
474
+ ] as typeof defaultValue
475
+ }
476
+ defaultValue = defaultValue ?? (gsDef.defaultValue as StateDefaultValueTypes)
477
+ const dynamicConditions = createConditionalStatement(
478
+ staticValue !== undefined
479
+ ? [{ operand: staticValue, operation: '===' }]
480
+ : conditions,
481
+ defaultValue
482
+ )
483
+ const matchCondition =
484
+ matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
485
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
486
+ // tslint:disable-next-line function-constructor
487
+ const isConditionPassing = new Function(`return ${conditionString}`)()
488
+ if (isConditionPassing) {
489
+ classNamesToAppend.add(styleRef.content.referenceId)
490
+ return
491
+ }
492
+ return
493
+ }
494
+
495
+ default: {
496
+ classNamesToAppend.add(styleRef.content.referenceId)
497
+ return
498
+ }
499
+ }
500
+ } else {
501
+ const referenceContent = styleRef.content.condition.reference.content
502
+ const referenceType = referenceContent.referenceType
503
+ const nameToAppend =
504
+ referenceType === 'local' && referenceContent.refPath?.length
505
+ ? referenceContent.refPath.join('.')
506
+ : referenceContent.id
507
+
508
+ const { conditions } = styleRef.content.condition.expression
509
+
510
+ const operator = conditions[0].operation as '===' | '!==' | '<' | '<=' | '>' | '>='
511
+ const right = conditions[0].operand as string | number | boolean
512
+
513
+ let binaryExpressionType = ''
514
+ switch (referenceType) {
515
+ case 'prop': {
516
+ binaryExpressionType = propDefinitions[nameToAppend].type
517
+ break
518
+ }
519
+ case 'state': {
520
+ binaryExpressionType = stateDefinitions[nameToAppend].type
521
+ break
522
+ }
523
+ case 'local': {
524
+ binaryExpressionType = typeof right as string
525
+ break
526
+ }
527
+ default: {
528
+ if ((referenceType as string) === 'globalState') {
529
+ const gsDef = findGlobalStateDefinitionById(
530
+ nameToAppend,
531
+ globalStateDefinitions
532
+ )
533
+ if (gsDef) {
534
+ binaryExpressionType = gsDef.type
535
+ }
536
+ break
537
+ }
538
+ throw new PluginCSS(
539
+ `Un-supported reference type ${referenceType} for ${nameToAppend}`
540
+ )
541
+ }
542
+ }
543
+
544
+ let binaryKey = nameToAppend
545
+ if (referenceType === 'prop') {
546
+ binaryKey = 'props?.' + nameToAppend
547
+ } else if ((referenceType as string) === 'globalState') {
548
+ const gsDef = findGlobalStateDefinitionById(nameToAppend, globalStateDefinitions)
549
+ binaryKey = gsDef ? gsDef.name : nameToAppend
550
+ }
551
+
552
+ const binaryExpression = createBinaryExpression(
553
+ { operation: operator, operand: right },
554
+ {
555
+ key: binaryKey,
556
+ type: binaryExpressionType,
557
+ }
558
+ )
559
+
560
+ const conditionalExpression = types.conditionalExpression(
561
+ binaryExpression,
562
+ types.stringLiteral(content.referenceId),
563
+ types.stringLiteral('')
564
+ )
565
+
566
+ dynamicVariantsToAppend.add(conditionalExpression)
567
+ return
568
+ }
569
+ } else {
570
+ classNamesToAppend.add(content.referenceId)
571
+ }
259
572
  return
260
573
  }
261
574
 
@@ -296,18 +609,35 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
296
609
  ? types.identifier(dynamicVariantPrefix)
297
610
  : types.identifier(propsPrefix)
298
611
 
299
- return types.memberExpression(dynamicAttrValueIdentifier, types.identifier(variant))
612
+ if (typeof variant === 'string') {
613
+ return types.memberExpression(dynamicAttrValueIdentifier, types.identifier(variant))
614
+ } else {
615
+ // variant is a ConditionalExpression, so return it directly
616
+ return variant
617
+ }
300
618
  })
301
619
  )
302
620
  }
303
- })
621
+ }
622
+
623
+ UIDLUtils.traverseElements(node, generateStylesForElementNode)
624
+ for (const prop of Object.values(propDefinitions)) {
625
+ if (prop.type === 'element' && prop.defaultValue) {
626
+ UIDLUtils.traverseElements(
627
+ prop.defaultValue as UIDLElementNode,
628
+ generateStylesForElementNode
629
+ )
630
+ }
631
+ }
304
632
 
305
633
  if (Object.keys(componentStyleSet).length > 0) {
306
634
  StyleBuilders.generateStylesFromStyleSetDefinitions(
307
635
  componentStyleSet,
308
636
  cssMap,
309
637
  mediaStylesMap,
310
- (styleName: string) => getClassName(forceScoping, uidl.name, styleName)
638
+ (styleName: string) => {
639
+ return StringUtils.camelCaseToDashCase(uidl.name + styleName)
640
+ }
311
641
  )
312
642
  }
313
643
 
@@ -315,7 +645,210 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
315
645
  cssMap.push(...StyleBuilders.generateMediaStyle(mediaStylesMap))
316
646
  }
317
647
 
318
- if (cssMap.length > 0) {
648
+ // Handle inline style tag for HTML templates (self-contained fragments)
649
+ if (standaloneHtmlComponents && templateStyle === 'html' && templateChunk) {
650
+ const inlineCssMap: string[] = []
651
+ const hasTokens = Object.keys(tokens).length > 0
652
+ const hasUsedProjectStyles = usedProjectStyleIds.size > 0
653
+ const globalAssets = options.globalAssets || []
654
+
655
+ // Get the scoped root class from the template chunk meta
656
+ const scopedRootClass = templateChunk.meta?.scopedRootClass as string | undefined
657
+
658
+ // Utility function to scope CSS selectors
659
+ const scopeCssSelectors = (css: string, rootSelector: string): string => {
660
+ // Replace :root with scoped root class
661
+ let scopedCss = css.replace(/:root\s*\{/g, `${rootSelector} {`)
662
+
663
+ // Scope regular selectors (but not inside @media or @keyframes)
664
+ // This regex matches CSS rules outside of @ blocks
665
+ scopedCss = scopedCss.replace(/(?:^|\})\s*([^@{}]+?)\s*\{/gm, (match, selectors) => {
666
+ // Skip if it's just closing brace or empty
667
+ if (!selectors || selectors.trim() === '') {
668
+ return match
669
+ }
670
+
671
+ // Skip if selector is already the root class
672
+ if (selectors.trim() === rootSelector) {
673
+ return match
674
+ }
675
+
676
+ // Handle multiple selectors (comma-separated)
677
+ const scopedSelectors = selectors
678
+ .split(',')
679
+ .map((sel: string) => {
680
+ const trimmedSel = sel.trim()
681
+ // Skip empty selectors
682
+ if (!trimmedSel) {
683
+ return ''
684
+ }
685
+ // Skip if already scoped
686
+ if (trimmedSel.startsWith(rootSelector)) {
687
+ return trimmedSel
688
+ }
689
+ // Replace html/body with root selector
690
+ if (trimmedSel === 'html' || trimmedSel === 'body') {
691
+ return rootSelector
692
+ }
693
+ // For universal selector, scope it
694
+ if (trimmedSel === '*') {
695
+ return `${rootSelector} *`
696
+ }
697
+ // Prefix other selectors with root
698
+ return `${rootSelector} ${trimmedSel}`
699
+ })
700
+ .filter(Boolean)
701
+ .join(', ')
702
+
703
+ return match.replace(selectors, scopedSelectors)
704
+ })
705
+
706
+ return scopedCss
707
+ }
708
+
709
+ // 0. Add reset and default styles from global assets (scoped)
710
+ const inlineStyleAssets = globalAssets.filter(
711
+ (asset): asset is UIDLStyleInlineAsset => asset.type === 'style' && 'content' in asset
712
+ )
713
+ for (const styleAsset of inlineStyleAssets) {
714
+ if (styleAsset.content) {
715
+ const css = scopedRootClass
716
+ ? scopeCssSelectors(styleAsset.content, `.${scopedRootClass}`)
717
+ : styleAsset.content
718
+ inlineCssMap.push(css)
719
+ }
720
+ }
721
+
722
+ // 1. Add CSS variables (tokens) - scoped to root class instead of :root
723
+ if (hasTokens) {
724
+ const tokenSelector = scopedRootClass ? `.${scopedRootClass}` : ':root'
725
+ inlineCssMap.push(
726
+ StyleBuilders.createCSSClassWithSelector(
727
+ '@global',
728
+ tokenSelector,
729
+ StyleUtils.getTokensContentFromTokensObject(tokens)
730
+ )
731
+ )
732
+ }
733
+
734
+ // 2. Add project-referenced styles that this component uses (scoped)
735
+ if (hasUsedProjectStyles) {
736
+ const usedProjectStyles = Array.from(usedProjectStyleIds)
737
+ // Include exact matches and compound selectors that reference the same base class
738
+ const filteredDefinitions = Object.fromEntries(
739
+ Object.entries(styleSetDefinitions).filter(([key, definition]) => {
740
+ // Include if exact match
741
+ if (usedProjectStyles.includes(key)) {
742
+ return true
743
+ }
744
+ // Include compound selectors where className matches a used project style
745
+ // e.g., "navigation-mobile-overlay.navigation-mobile-overlay-active" has className "navigation-mobile-overlay"
746
+ const className = (definition as { className?: string }).className
747
+ if (className && usedProjectStyles.includes(className)) {
748
+ return true
749
+ }
750
+ // Include compound selectors where the base class (before the dot or space) is a used style
751
+ // e.g., "dashboard-sidebar.collapsed" should be included if "dashboard-sidebar" is used
752
+ // e.g., "dashboard-sidebar.collapsed .sidebar-brand-text" should also be included
753
+ const dotIdx = key.indexOf('.')
754
+ const spaceIdx = key.indexOf(' ')
755
+ if (dotIdx > 0) {
756
+ const baseClass = key.substring(0, dotIdx)
757
+ if (usedProjectStyles.includes(baseClass)) {
758
+ return true
759
+ }
760
+ }
761
+ if (spaceIdx > 0 && dotIdx < 0) {
762
+ const baseClass = key.substring(0, spaceIdx)
763
+ if (usedProjectStyles.includes(baseClass)) {
764
+ return true
765
+ }
766
+ }
767
+ return false
768
+ })
769
+ )
770
+ const projectStylesCssMap: string[] = []
771
+ const projectMediaStylesMap: Record<
772
+ string,
773
+ Array<{ [x: string]: Record<string, string | number> }>
774
+ > = {}
775
+ StyleBuilders.generateStylesFromStyleSetDefinitions(
776
+ filteredDefinitions,
777
+ projectStylesCssMap,
778
+ projectMediaStylesMap,
779
+ (styleName) => styleName
780
+ )
781
+ // Scope the project styles if scoped root class is available
782
+ if (scopedRootClass) {
783
+ const scopedProjectStyles = projectStylesCssMap.map((css) =>
784
+ scopeCssSelectors(css, `.${scopedRootClass}`)
785
+ )
786
+ inlineCssMap.push(...scopedProjectStyles)
787
+ } else {
788
+ inlineCssMap.push(...projectStylesCssMap)
789
+ }
790
+ if (Object.keys(projectMediaStylesMap).length > 0) {
791
+ const mediaStyles = StyleBuilders.generateMediaStyle(projectMediaStylesMap)
792
+ if (scopedRootClass) {
793
+ const scopedMediaStyles = mediaStyles.map((css) =>
794
+ scopeCssSelectors(css, `.${scopedRootClass}`)
795
+ )
796
+ inlineCssMap.push(...scopedMediaStyles)
797
+ } else {
798
+ inlineCssMap.push(...mediaStyles)
799
+ }
800
+ }
801
+ }
802
+
803
+ // 3. Add component-specific styles (scoped)
804
+ if (scopedRootClass) {
805
+ // Scope the component CSS
806
+ const scopedCssMap = cssMap.map((css) => scopeCssSelectors(css, `.${scopedRootClass}`))
807
+ inlineCssMap.push(...scopedCssMap)
808
+ } else {
809
+ inlineCssMap.push(...cssMap)
810
+ }
811
+
812
+ // 4. Create font link tags from global assets
813
+ const fontAssets = globalAssets.filter(
814
+ (asset): asset is UIDLFontAsset => asset.type === 'font' && 'path' in asset
815
+ )
816
+ const fontLinkNodes: HastNode[] = []
817
+ for (const fontAsset of fontAssets) {
818
+ const linkNode = HASTBuilders.createHTMLNode('link')
819
+ HASTUtils.addAttributeToNode(linkNode, 'rel', 'stylesheet')
820
+ HASTUtils.addAttributeToNode(linkNode, 'href', fontAsset.path)
821
+ if (fontAsset.attrs) {
822
+ for (const [attrKey, attrValue] of Object.entries(fontAsset.attrs)) {
823
+ const value =
824
+ typeof attrValue === 'object' && 'content' in attrValue
825
+ ? String(attrValue.content)
826
+ : String(attrValue)
827
+ HASTUtils.addAttributeToNode(linkNode, attrKey, value)
828
+ }
829
+ }
830
+ fontLinkNodes.push(linkNode)
831
+ }
832
+
833
+ // 5. Create <style> node and prepend to HTML (fonts first, then styles)
834
+ if (inlineCssMap.length > 0 || fontLinkNodes.length > 0) {
835
+ const nodesToPrepend: HastNode[] = []
836
+
837
+ // Add font links first
838
+ nodesToPrepend.push(...fontLinkNodes)
839
+
840
+ // Add style tag with all CSS
841
+ if (inlineCssMap.length > 0) {
842
+ const styleNode = HASTBuilders.createHTMLNode('style')
843
+ const cssContent = HASTBuilders.createTextNode(inlineCssMap.join('\n\n'))
844
+ styleNode.children.push(cssContent)
845
+ nodesToPrepend.push(styleNode)
846
+ }
847
+
848
+ ;(templateChunk.content as HastNode).children.unshift(...nodesToPrepend)
849
+ }
850
+ } else if (cssMap.length > 0) {
851
+ // Original behavior: create separate CSS file
319
852
  /**
320
853
  * Setup an import statement for the styles
321
854
  * The name of the file is either in the meta of the component generator
@@ -344,11 +877,17 @@ const createCSSPlugin: ComponentPluginFactory<CSSPluginConfig> = (config) => {
344
877
  }
345
878
  }
346
879
 
880
+ const folderPath = uidl.outputOptions?.folderPath
881
+ const cssContent =
882
+ folderPath?.length > 0
883
+ ? cssMap.map((css) => prefixUrlPathsInCss(css, folderPath)).join('\n \n')
884
+ : cssMap.join('\n \n')
885
+
347
886
  chunks.push({
348
887
  type: ChunkType.STRING,
349
888
  name: chunkName,
350
889
  fileType: FileType.CSS,
351
- content: cssMap.join('\n'),
890
+ content: cssContent,
352
891
  linkAfter: [],
353
892
  })
354
893
  }
@@ -363,6 +902,18 @@ export { createStyleSheetPlugin, createCSSPlugin }
363
902
 
364
903
  export default createCSSPlugin()
365
904
 
905
+ const findGlobalStateDefinitionById = (
906
+ id: string,
907
+ definitions: Record<string, UIDLGlobalStateDefinition>
908
+ ): UIDLGlobalStateDefinition | undefined => {
909
+ for (const def of Object.values(definitions)) {
910
+ if (def.id === id) {
911
+ return def
912
+ }
913
+ }
914
+ return undefined
915
+ }
916
+
366
917
  const createDynamicInlineStyle = (styles: UIDLStyleDefinitions) => {
367
918
  return Object.keys(styles)
368
919
  .map((styleKey) => {
@@ -371,8 +922,25 @@ const createDynamicInlineStyle = (styles: UIDLStyleDefinitions) => {
371
922
  .join(', ')
372
923
  }
373
924
 
374
- const getClassName = (scoping: boolean, uidlName: string, nodeStyleName: string) => {
375
- return scoping
376
- ? StringUtils.camelCaseToDashCase(`${uidlName}-${nodeStyleName}`)
377
- : StringUtils.camelCaseToDashCase(nodeStyleName)
925
+ const createDynamicBindingInlineStyle = (
926
+ bindings: Record<
927
+ string,
928
+ {
929
+ referenceType: string
930
+ stateKey: string
931
+ defaultValue: string
932
+ contextName?: string
933
+ stateDefinitionId?: string
934
+ }
935
+ >
936
+ ): string => {
937
+ return Object.entries(bindings)
938
+ .map(([prop, binding]) => {
939
+ const value =
940
+ binding.referenceType === 'ctx' && binding.contextName
941
+ ? `${binding.contextName}.${binding.stateKey}`
942
+ : binding.stateKey
943
+ return `${prop}: ${value} != null ? ${value} : '${binding.defaultValue || ''}'`
944
+ })
945
+ .join(', ')
378
946
  }