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