@teleporthq/teleport-plugin-html-base-component 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.
@@ -21,17 +21,55 @@ import {
21
21
  ComponentStructure,
22
22
  UIDLComponentOutputOptions,
23
23
  UIDLElement,
24
+ ElementsLookup,
25
+ UIDLConditionalNode,
26
+ PropDefaultValueTypes,
27
+ UIDLCMSListRepeaterNode,
24
28
  } from '@teleporthq/teleport-types'
25
29
  import { join, relative } from 'path'
26
- import { HASTBuilders, HASTUtils } from '@teleporthq/teleport-plugin-common'
27
- import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
30
+ import { HASTBuilders, HASTUtils, ASTUtils } from '@teleporthq/teleport-plugin-common'
31
+ import { GenericUtils, StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
28
32
  import { staticNode } from '@teleporthq/teleport-uidl-builders'
29
33
  import { createCSSPlugin } from '@teleporthq/teleport-plugin-css'
34
+ import { generateUniqueKeys, createNodesLookup } from '@teleporthq/teleport-uidl-resolver'
30
35
  import { DEFAULT_COMPONENT_CHUNK_NAME } from './constants'
31
36
 
37
+ const isValidURL = (url: string) => {
38
+ try {
39
+ /* tslint:disable:no-unused-expression */
40
+ new URL(url)
41
+ return true
42
+ } catch (error) {
43
+ return false
44
+ }
45
+ }
46
+
47
+ const addNodeToLookup = (
48
+ key: string,
49
+ tag: HastNode | HastText | Array<HastNode | HastText>,
50
+ nodesLoookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>
51
+ ) => {
52
+ // In html code-generation we combine the nodes of the component that is being consumed with the current component.
53
+ // As html can't load the component at runtime like react or any other frameworks. So, we merge the component as a standalone
54
+ // component in the current component.
55
+ const currentLookup = nodesLoookup[key]
56
+ if (currentLookup) {
57
+ if (Array.isArray(currentLookup)) {
58
+ Array.isArray(tag) ? currentLookup.push(...tag) : currentLookup.push(tag)
59
+ } else {
60
+ nodesLoookup[key] = Array.isArray(tag) ? [currentLookup, ...tag] : [currentLookup, tag]
61
+ }
62
+
63
+ return
64
+ }
65
+
66
+ nodesLoookup[key] = tag
67
+ }
68
+
32
69
  type NodeToHTML<NodeType, ReturnType> = (
33
70
  node: NodeType,
34
- templatesLookUp: Record<string, unknown>,
71
+ componentName: string,
72
+ nodesLookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>,
35
73
  propDefinitions: Record<string, UIDLPropDefinition>,
36
74
  stateDefinitions: Record<string, UIDLStateDefinition>,
37
75
  subComponentOptions: {
@@ -43,16 +81,28 @@ type NodeToHTML<NodeType, ReturnType> = (
43
81
  dependencies: Record<string, UIDLDependency>
44
82
  options: GeneratorOptions
45
83
  outputOptions: UIDLComponentOutputOptions
84
+ },
85
+ /**
86
+ * This param is just to be able to handle CMS array mappers/Repeater nodes. A bit hacky, better support should be implemented
87
+ */
88
+ resolvedExpressions?: {
89
+ expressions: Record<string, UIDLPropDefinition>
90
+ currentIndex: number
46
91
  }
47
92
  ) => ReturnType
48
93
 
49
- export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastText>> = async (
94
+ export const generateHtmlSyntax: NodeToHTML<
95
+ UIDLNode,
96
+ Promise<HastNode | HastText | Array<HastNode | HastText>>
97
+ > = async (
50
98
  node,
51
- templatesLookUp,
99
+ compName,
100
+ nodesLookup,
52
101
  propDefinitions,
53
102
  stateDefinitions,
54
103
  subComponentOptions,
55
- structure
104
+ structure,
105
+ resolvedExpressions
56
106
  ) => {
57
107
  switch (node.type) {
58
108
  case 'inject':
@@ -66,25 +116,180 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
66
116
  return HASTBuilders.createHTMLNode(node.type)
67
117
 
68
118
  case 'element':
69
- return generatElementNode(
119
+ const elementNode = await generateElementNode(
70
120
  node,
71
- templatesLookUp,
121
+ compName,
122
+ nodesLookup,
72
123
  propDefinitions,
73
124
  stateDefinitions,
74
125
  subComponentOptions,
75
- structure
126
+ structure,
127
+ resolvedExpressions
76
128
  )
129
+ return elementNode
77
130
 
78
131
  case 'dynamic':
79
- return generateDynamicNode(
132
+ const dynamicNode = await generateDynamicNode(
80
133
  node,
81
- templatesLookUp,
134
+ compName,
135
+ nodesLookup,
136
+ propDefinitions,
137
+ stateDefinitions,
138
+ subComponentOptions,
139
+ structure,
140
+ resolvedExpressions
141
+ )
142
+ return dynamicNode
143
+
144
+ case 'conditional':
145
+ const conditionalNodeComment = HASTBuilders.createTextNode('')
146
+ const {
147
+ value: staticValue,
148
+ reference,
149
+ condition: { conditions, matchingCriteria },
150
+ } = node.content
151
+
152
+ if (reference.type !== 'dynamic') {
153
+ return conditionalNodeComment
154
+ }
155
+
156
+ const {
157
+ content: { referenceType, id, refPath = [] },
158
+ } = reference
159
+
160
+ switch (referenceType) {
161
+ case 'prop': {
162
+ const usedProp = propDefinitions[id]
163
+ if (usedProp === undefined || usedProp.defaultValue === undefined) {
164
+ return conditionalNodeComment
165
+ }
166
+ let defaultValue = usedProp.defaultValue
167
+ for (const path of refPath) {
168
+ defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
169
+ }
170
+
171
+ // If defaultValue is undefined or null after path traversal, use original default
172
+ defaultValue = defaultValue ?? usedProp.defaultValue
173
+
174
+ // Since we know the operand and the default value from the prop.
175
+ // We can try building the condition and check if the condition is true or false.
176
+ // @todo: You can only use a 'value' in UIDL or 'conditions' but not both.
177
+ // UIDL validations need to be improved on this aspect.
178
+ const dynamicConditions = createConditionalStatement(
179
+ staticValue !== undefined ? [{ operand: staticValue, operation: '===' }] : conditions,
180
+ defaultValue
181
+ )
182
+ const matchCondition = matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
183
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
184
+
185
+ try {
186
+ // tslint:disable-next-line function-constructor
187
+ const isConditionPassing = new Function(`return ${conditionString}`)()
188
+ if (isConditionPassing) {
189
+ return generateHtmlSyntax(
190
+ node.content.node,
191
+ compName,
192
+ nodesLookup,
193
+ propDefinitions,
194
+ stateDefinitions,
195
+ subComponentOptions,
196
+ structure,
197
+ resolvedExpressions
198
+ )
199
+ }
200
+ } catch (error) {
201
+ return conditionalNodeComment
202
+ }
203
+
204
+ return conditionalNodeComment
205
+ }
206
+
207
+ case 'state':
208
+ default:
209
+ return conditionalNodeComment
210
+ }
211
+
212
+ case 'expr':
213
+ const content = node.content.split('?.')
214
+
215
+ if (resolvedExpressions && resolvedExpressions.expressions?.[content[0] || '']) {
216
+ const uidlDynamicRef: UIDLDynamicReference = {
217
+ type: 'dynamic',
218
+ content: {
219
+ referenceType: 'prop',
220
+ refPath: [resolvedExpressions.currentIndex.toString(), ...content.slice(1)],
221
+ id: content[0],
222
+ },
223
+ }
224
+ const generatedNode = await generateDynamicNode(
225
+ uidlDynamicRef,
226
+ compName,
227
+ nodesLookup,
228
+ resolvedExpressions.expressions,
229
+ stateDefinitions,
230
+ subComponentOptions,
231
+ structure
232
+ )
233
+ return generatedNode
234
+ }
235
+
236
+ // Fallback: support simple prop/state expressions outside of repeater context
237
+ if (content[0] && (propDefinitions?.[content[0]] || stateDefinitions?.[content[0]])) {
238
+ const isProp = Boolean(propDefinitions?.[content[0]])
239
+ const uidlDynamicRef: UIDLDynamicReference = {
240
+ type: 'dynamic',
241
+ content: {
242
+ referenceType: isProp ? 'prop' : 'state',
243
+ refPath: content.slice(1),
244
+ id: content[0],
245
+ },
246
+ }
247
+ const generatedNode = await generateDynamicNode(
248
+ uidlDynamicRef,
249
+ compName,
250
+ nodesLookup,
251
+ isProp ? propDefinitions : stateDefinitions,
252
+ stateDefinitions,
253
+ subComponentOptions,
254
+ structure
255
+ )
256
+ return generatedNode
257
+ }
258
+
259
+ return HASTBuilders.createComment('Expressions are not supported in HTML')
260
+ case 'cms-list-repeater':
261
+ return generateRepeaterNode(
262
+ node,
263
+ compName,
264
+ nodesLookup,
82
265
  propDefinitions,
83
266
  stateDefinitions,
84
267
  subComponentOptions,
85
268
  structure
86
269
  )
87
270
 
271
+ case 'cms-item':
272
+ case 'cms-list':
273
+ case 'data-source-item':
274
+ case 'data-source-list':
275
+ // For HTML generation, render the success node content
276
+ // Since HTML doesn't support dynamic data fetching, we just render the static structure
277
+ const successNode = (node.content as any).nodes?.success
278
+ if (successNode) {
279
+ return generateHtmlSyntax(
280
+ successNode,
281
+ compName,
282
+ nodesLookup,
283
+ propDefinitions,
284
+ stateDefinitions,
285
+ subComponentOptions,
286
+ structure,
287
+ resolvedExpressions
288
+ )
289
+ }
290
+ // If no success node, return empty div
291
+ return HASTBuilders.createHTMLNode('div')
292
+
88
293
  default:
89
294
  throw new HTMLComponentGeneratorError(
90
295
  `generateHtmlSyntax encountered a node of unsupported type: ${JSON.stringify(
@@ -96,14 +301,211 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
96
301
  }
97
302
  }
98
303
 
99
- const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastText>> = async (
304
+ const createConditionalStatement = (
305
+ conditions: UIDLConditionalNode['content']['condition']['conditions'],
306
+ leftOperand: UIDLPropDefinition['defaultValue']
307
+ ) => {
308
+ return conditions.map((condition) => {
309
+ const { operation, operand } = condition
310
+
311
+ if (operand === undefined) {
312
+ return `${ASTUtils.convertToUnaryOperator(operation)}${getValueType(operand)}`
313
+ }
314
+
315
+ return `${getValueType(leftOperand)} ${ASTUtils.convertToBinaryOperator(
316
+ operation
317
+ )} ${getValueType(operand)}`
318
+ })
319
+ }
320
+
321
+ const getValueType = (value: UIDLPropDefinition['defaultValue']) => {
322
+ const valueType = typeof value
323
+ switch (valueType) {
324
+ case 'string':
325
+ return `"${value}"`
326
+ case 'number':
327
+ return value
328
+ case 'boolean':
329
+ return value
330
+ case 'object':
331
+ // Handle dynamic references (local, prop, state)
332
+ if (value && typeof value === 'object' && 'type' in value) {
333
+ const dynamicValue = value as unknown as UIDLDynamicReference
334
+ if (dynamicValue.type === 'dynamic' && dynamicValue.content) {
335
+ const { referenceType, id, refPath } = dynamicValue.content
336
+ if (referenceType === 'local' && id) {
337
+ // Local reference from repeater context
338
+ if (refPath && refPath.length > 0) {
339
+ return `${id}.${refPath.join('.')}`
340
+ }
341
+ return id
342
+ } else if (referenceType === 'prop' || referenceType === 'state') {
343
+ // Prop or state reference
344
+ const key = refPath && refPath.length > 0 ? refPath.join('.') : id
345
+ return key
346
+ }
347
+ }
348
+ }
349
+ throw new HTMLComponentGeneratorError(
350
+ `Conditional node received an operand of type ${valueType} \n
351
+ Received ${JSON.stringify(value)}`
352
+ )
353
+ default:
354
+ throw new HTMLComponentGeneratorError(
355
+ `Conditional node received an operand of type ${valueType} \n
356
+ Received ${JSON.stringify(value)}`
357
+ )
358
+ }
359
+ }
360
+
361
+ const generateRepeaterNode: NodeToHTML<
362
+ UIDLCMSListRepeaterNode,
363
+ Promise<HastNode | HastText>
364
+ > = async (
100
365
  node,
101
- templatesLookUp,
366
+ compName,
367
+ nodesLookup,
102
368
  propDefinitions,
103
369
  stateDefinitions,
104
370
  subComponentOptions,
105
371
  structure
106
372
  ) => {
373
+ const { nodes } = node.content
374
+
375
+ const contextId = node.content.renderPropIdentifier
376
+ const sourceValue = node.content.source
377
+ let propDef =
378
+ sourceValue && typeof sourceValue === 'string'
379
+ ? propDefinitions[
380
+ Object.keys(propDefinitions).find((propKey) => sourceValue.includes(propKey)) || ''
381
+ ]
382
+ : undefined
383
+
384
+ if (!propDef || !Array.isArray(propDef.defaultValue)) {
385
+ // If no prop is found we might have a static source value
386
+ try {
387
+ const parsedSource = JSON.parse(sourceValue)
388
+ propDef = {
389
+ defaultValue: parsedSource,
390
+ id: contextId,
391
+ type: 'array',
392
+ }
393
+ } catch {
394
+ // Silent fail
395
+ }
396
+ }
397
+
398
+ // We do the check again to keep typescript happy, otherwise this could be in catch
399
+ if (!propDef || !Array.isArray(propDef.defaultValue)) {
400
+ return HASTBuilders.createComment(
401
+ 'CMS Array Mapper/Repeater not supported in HTML without a prop source'
402
+ )
403
+ }
404
+ propDefinitions[contextId] = propDef
405
+
406
+ const elementNode = HASTBuilders.createHTMLNode('div')
407
+ node.content.nodes.list.content.style = { display: { type: 'static', content: 'contents' } }
408
+ if (node.content.nodes.empty) {
409
+ node.content.nodes.empty.content.style = { display: { type: 'static', content: 'contents' } }
410
+ }
411
+ if (node.content.nodes.loading) {
412
+ node.content.nodes.loading.content.style = { display: { type: 'static', content: 'contents' } }
413
+ }
414
+ // Empty case
415
+ if (propDef.defaultValue.length === 0) {
416
+ const emptyChildren = nodes.empty?.content.children
417
+ if (emptyChildren) {
418
+ for (const child of emptyChildren) {
419
+ const childTag = await generateHtmlSyntax(
420
+ child,
421
+ compName,
422
+ nodesLookup,
423
+ propDefinitions,
424
+ stateDefinitions,
425
+ subComponentOptions,
426
+ structure
427
+ )
428
+
429
+ if (typeof childTag === 'string') {
430
+ HASTUtils.addTextNode(elementNode, childTag)
431
+ } else {
432
+ HASTUtils.addChildNode(elementNode, childTag as HastNode)
433
+ }
434
+ }
435
+ }
436
+
437
+ if (nodes.empty) {
438
+ addNodeToLookup(`${node.content.nodes.empty.content.key}`, elementNode, nodesLookup)
439
+ }
440
+ return elementNode
441
+ }
442
+
443
+ const listChildren = nodes.list.content.children
444
+ if (listChildren) {
445
+ for (let index = 0; index < propDef.defaultValue.length; index++) {
446
+ for (const child of listChildren) {
447
+ const childTag = await generateHtmlSyntax(
448
+ child,
449
+ compName,
450
+ nodesLookup,
451
+ propDefinitions,
452
+ stateDefinitions,
453
+ subComponentOptions,
454
+ structure,
455
+ { currentIndex: index, expressions: { [contextId]: propDef } }
456
+ )
457
+
458
+ if (typeof childTag === 'string') {
459
+ HASTUtils.addTextNode(elementNode, childTag)
460
+ } else {
461
+ HASTUtils.addChildNode(elementNode, childTag as HastNode)
462
+ }
463
+ }
464
+ }
465
+ }
466
+
467
+ addNodeToLookup(`${node.content.nodes.list.content.key}`, elementNode, nodesLookup)
468
+ return elementNode
469
+ }
470
+
471
+ const generateElementNode: NodeToHTML<
472
+ UIDLElementNode,
473
+ Promise<HastNode | HastText | Array<HastNode | HastText>>
474
+ > = async (
475
+ node,
476
+ compName,
477
+ nodesLookup,
478
+ propDefinitions,
479
+ stateDefinitions,
480
+ subComponentOptions,
481
+ structure,
482
+ resolvedExpressions
483
+ ) => {
484
+ // Check if this is a wrapped data-source node
485
+ if (
486
+ node.content &&
487
+ typeof node.content === 'object' &&
488
+ 'type' in node.content &&
489
+ (node.content.type === 'data-source-item' || node.content.type === 'data-source-list')
490
+ ) {
491
+ // Handle as data-source node - just render the success content
492
+ const successNode = (node.content as any).nodes?.success
493
+ if (successNode) {
494
+ return generateHtmlSyntax(
495
+ successNode,
496
+ compName,
497
+ nodesLookup,
498
+ propDefinitions,
499
+ stateDefinitions,
500
+ subComponentOptions,
501
+ structure,
502
+ resolvedExpressions
503
+ )
504
+ }
505
+ // If no success node, return empty div
506
+ return HASTBuilders.createHTMLNode('div')
507
+ }
508
+
107
509
  const {
108
510
  elementType,
109
511
  children,
@@ -111,43 +513,45 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
111
513
  style = {},
112
514
  referencedStyles = {},
113
515
  dependency,
114
- key,
115
516
  } = node.content
116
-
117
- const elementNode = HASTBuilders.createHTMLNode(elementType)
118
- templatesLookUp[key] = elementNode
119
-
120
517
  const { dependencies } = structure
121
- if (dependency && (dependency as UIDLDependency)?.type !== 'local') {
122
- dependencies[dependency.path] = dependency
123
- }
124
-
125
518
  if (dependency && (dependency as UIDLDependency)?.type === 'local') {
126
519
  const compTag = await generateComponentContent(
127
520
  node,
521
+ nodesLookup,
128
522
  propDefinitions,
129
523
  stateDefinitions,
130
524
  subComponentOptions,
131
- structure
525
+ structure,
526
+ resolvedExpressions
132
527
  )
528
+
529
+ if ('tagName' in compTag) {
530
+ compTag.children.unshift(HASTBuilders.createComment(`${node.content.semanticType} component`))
531
+ }
532
+
133
533
  return compTag
134
534
  }
135
535
 
536
+ if (dependency && (dependency as UIDLDependency)?.type !== 'local') {
537
+ dependencies[dependency.path] = dependency
538
+ }
539
+
540
+ const elementNode = HASTBuilders.createHTMLNode(elementType)
541
+
136
542
  if (children) {
137
543
  for (const child of children) {
138
- const childTag = await generateHtmlSynatx(
544
+ const childTag = await generateHtmlSyntax(
139
545
  child,
140
- templatesLookUp,
546
+ compName,
547
+ nodesLookup,
141
548
  propDefinitions,
142
549
  stateDefinitions,
143
550
  subComponentOptions,
144
- structure
551
+ structure,
552
+ resolvedExpressions
145
553
  )
146
554
 
147
- if (!childTag) {
148
- return
149
- }
150
-
151
555
  if (typeof childTag === 'string') {
152
556
  HASTUtils.addTextNode(elementNode, childTag)
153
557
  } else {
@@ -161,7 +565,6 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
161
565
  const refStyle = referencedStyles[styleRef]
162
566
  if (refStyle.content.mapType === 'inlined') {
163
567
  handleStyles(node, refStyle.content.styles, propDefinitions, stateDefinitions)
164
- return
165
568
  }
166
569
  })
167
570
  }
@@ -170,23 +573,39 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
170
573
  handleStyles(node, style, propDefinitions, stateDefinitions)
171
574
  }
172
575
 
173
- if (Object.keys(attrs).length > 0) {
174
- handleAttributes(
175
- elementType,
176
- elementNode,
177
- attrs,
178
- propDefinitions,
179
- stateDefinitions,
180
- structure.options.projectRouteDefinition,
181
- structure.outputOptions
182
- )
183
- }
576
+ handleAttributes(
577
+ elementType,
578
+ elementNode,
579
+ attrs,
580
+ propDefinitions,
581
+ stateDefinitions,
582
+ structure.options.projectRouteDefinition,
583
+ structure.outputOptions,
584
+ resolvedExpressions?.currentIndex
585
+ )
184
586
 
587
+ addNodeToLookup(node.content.key, elementNode, nodesLookup)
185
588
  return elementNode
186
589
  }
187
590
 
591
+ const createLookupTable = (
592
+ component: ComponentUIDL,
593
+ nodesLookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>
594
+ ): ElementsLookup => {
595
+ const lookup: ElementsLookup = {}
596
+ for (const node of Object.keys(nodesLookup)) {
597
+ lookup[node] = {
598
+ count: 1,
599
+ nextKey: '1',
600
+ }
601
+ }
602
+ createNodesLookup(component, lookup)
603
+ return lookup
604
+ }
605
+
188
606
  const generateComponentContent = async (
189
607
  node: UIDLElementNode,
608
+ nodesLookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>,
190
609
  propDefinitions: Record<string, UIDLPropDefinition>,
191
610
  stateDefinitions: Record<string, UIDLStateDefinition>,
192
611
  subComponentOptions: {
@@ -198,23 +617,26 @@ const generateComponentContent = async (
198
617
  dependencies: Record<string, UIDLDependency>
199
618
  options: GeneratorOptions
200
619
  outputOptions: UIDLComponentOutputOptions
620
+ },
621
+ resolvedExpressions?: {
622
+ expressions: Record<string, UIDLPropDefinition>
623
+ currentIndex: number
201
624
  }
202
625
  ) => {
203
626
  const { externals, plugins } = subComponentOptions
204
- const { elementType, attrs = {}, key, children = [] } = node.content
627
+ const { elementType, attrs = {}, children = [] } = node.content
205
628
  const { dependencies, chunks = [], options } = structure
206
- const comp = UIDLUtils.cloneObject(externals[elementType] || {}) as ComponentUIDL
207
- const lookUpTemplates: Record<string, unknown> = {}
208
- let compHasSlots: boolean = false
209
-
210
- if (!comp || !comp?.node) {
211
- throw new HTMLComponentGeneratorError(`${elementType} is not found from the externals. \n
212
- Received ${JSON.stringify(Object.keys(externals), null, 2)}`)
629
+ // "Component" will not exist when generating a component because the resolver checks for illegal class names
630
+ const componentName = elementType === 'Component' ? 'AppComponent' : elementType
631
+ const component = externals[componentName]
632
+ if (component === undefined) {
633
+ throw new HTMLComponentGeneratorError(`${componentName} is missing from externals object`)
213
634
  }
214
635
 
636
+ const componentClone = UIDLUtils.cloneObject<ComponentUIDL>(component)
637
+
215
638
  if (children.length) {
216
- compHasSlots = true
217
- UIDLUtils.traverseNodes(comp.node, (childNode, parentNode) => {
639
+ UIDLUtils.traverseNodes(componentClone.node, (childNode, parentNode) => {
218
640
  if (childNode.type === 'slot' && parentNode.type === 'element') {
219
641
  const nonSlotNodes = parentNode.content?.children?.filter((n) => n.type !== 'slot')
220
642
  parentNode.content.children = [
@@ -224,6 +646,7 @@ const generateComponentContent = async (
224
646
  content: {
225
647
  key: 'custom-slot',
226
648
  elementType: 'slot',
649
+ name: componentClone.name + 'slot',
227
650
  style: {
228
651
  display: {
229
652
  type: 'static',
@@ -243,31 +666,47 @@ const generateComponentContent = async (
243
666
  node.content.children = []
244
667
  }
245
668
 
246
- const combinedProps = { ...propDefinitions, ...(comp?.propDefinitions || {}) }
669
+ // In UIDL, we define only the link between a component and a page.
670
+ // We define this link using the UIDLLocalDependency approach.
671
+ // So, during the page resolution step, where we ideally generate the unique keys for the components.
672
+ // We can't generate the unique keys for the components because we don't have the full UIDL of the component.
673
+ // When we are using components in a page, the `addExternalComponents` step of the
674
+ // html-component-generator will add the full UIDL of the component to the externals object after resolving them.
675
+ // But when a component is used multiple number of times, we are basically using the same nodes again and again.
676
+ // Which indivates duplication. So, we create a lookup table of all the nodes present with us in the page
677
+ // And then pass it to the component to avoid any coilissions.
678
+ const lookupTableForCurrentPage = createLookupTable(componentClone, nodesLookup)
679
+ generateUniqueKeys(componentClone, lookupTableForCurrentPage)
247
680
 
248
- const propsForInstance = Object.keys(combinedProps).reduce(
249
- (acc: Record<string, UIDLPropDefinition>, propKey) => {
250
- if (attrs[propKey]) {
251
- acc[propKey] = {
252
- ...combinedProps[propKey],
253
- defaultValue: attrs[propKey]?.content || combinedProps[propKey]?.defaultValue,
254
- }
255
- } else {
256
- acc[propKey] = combinedProps[propKey]
257
- }
258
-
259
- return acc
260
- },
261
- {}
262
- )
263
-
264
- const combinedStates = { ...stateDefinitions, ...(comp?.stateDefinitions || {}) }
681
+ // We are combining props of the current component
682
+ // with props of the component that we need to generate.
683
+ // Refer to line 309, for element props. We either pick from the attr of the current instance of component
684
+ // or from the propDefinitions of the component that we are generating.
685
+ // We NEED to keep passing the props of the current component to the child component and so on,
686
+ // so that all props are forwarded.
687
+ const combinedProps: Record<string, UIDLPropDefinition> = {
688
+ ...Object.keys(propDefinitions).reduce<Record<string, UIDLPropDefinition>>(
689
+ (acc: Record<string, UIDLPropDefinition>, propKey) => {
690
+ acc[propKey] = propDefinitions[propKey]
691
+ return acc
692
+ },
693
+ {}
694
+ ),
695
+ ...(componentClone?.propDefinitions || {}),
696
+ }
697
+ const combinedStates = { ...stateDefinitions, ...(componentClone?.stateDefinitions || {}) }
265
698
  const statesForInstance = Object.keys(combinedStates).reduce(
266
699
  (acc: Record<string, UIDLStateDefinition>, propKey) => {
267
- if (attrs[propKey]) {
700
+ const attr = attrs[propKey]
701
+
702
+ if (attr?.type === 'object') {
703
+ throw new Error(`Object attributes are not supported in html exports`)
704
+ }
705
+
706
+ if (attr) {
268
707
  acc[propKey] = {
269
708
  ...combinedStates[propKey],
270
- defaultValue: attrs[propKey]?.content || combinedStates[propKey]?.defaultValue,
709
+ defaultValue: attr?.content ?? combinedStates[propKey]?.defaultValue,
271
710
  }
272
711
  } else {
273
712
  acc[propKey] = combinedStates[propKey]
@@ -278,42 +717,140 @@ const generateComponentContent = async (
278
717
  {}
279
718
  )
280
719
 
281
- const elementNode = HASTBuilders.createHTMLNode(StringUtils.camelCaseToDashCase(elementType))
282
- lookUpTemplates[key] = elementNode
283
-
284
- const compTag = (await generateHtmlSynatx(
285
- {
286
- ...comp.node,
287
- content: {
288
- ...comp.node.content,
289
- style: {
290
- ...(comp.node.content?.style || {}),
291
- display: {
292
- type: 'static',
293
- content: 'contents',
294
- },
720
+ const propsForInstance: Record<string, UIDLPropDefinition> = {}
721
+ // this is where we check if the component we are conusming is actually passing any props to the instance.
722
+ // We check if we are passing any props and pick the value from the atrrs, if not we pick the value from the propDefinitions of
723
+ // the component instance that we are using here.
724
+ for (const propKey of Object.keys(combinedProps)) {
725
+ const attribute = attrs[propKey]
726
+
727
+ if (attribute?.type === 'element') {
728
+ propsForInstance[propKey] = {
729
+ ...combinedProps[propKey],
730
+ defaultValue: attrs[propKey],
731
+ }
732
+ }
733
+
734
+ if (attribute?.type === 'dynamic') {
735
+ // When we are using a component instance in a component and the attribute
736
+ // that is passed to the component is of dynamic reference.
737
+ // If means, the component is redirecting the prop that is received to the prop of the component that it is consuming.
738
+ // In this case, we need to pass the value of the prop that is received to the prop of the component that it is consuming.
739
+ // And similary we do the same for the states.
740
+ switch (attribute.content.referenceType) {
741
+ case 'prop':
742
+ propsForInstance[propKey] = combinedProps[attribute.content.id]
743
+ break
744
+ case 'state':
745
+ propsForInstance[propKey] = combinedStates[attribute.content.id]
746
+ break
747
+ case 'expr':
748
+ // Ignore expr type attributes in html comp instances for the time being.
749
+ break
750
+ default:
751
+ throw new Error(
752
+ `ReferenceType ${attribute.content.referenceType} is not supported in HTML Export.`
753
+ )
754
+ }
755
+ }
756
+
757
+ if (attribute?.type === 'object') {
758
+ propsForInstance[propKey] = {
759
+ ...combinedProps[propKey],
760
+ defaultValue: (attribute?.content as object) || combinedProps[propKey]?.defaultValue,
761
+ }
762
+ }
763
+
764
+ if (attribute?.type === 'expr') {
765
+ const [ctxId, ...refPath] = attribute.content.split('?.')
766
+ const propKeyFromAttr = Object.keys(combinedProps).find((key) => key === ctxId)
767
+
768
+ const resolvedValue = combinedProps[propKeyFromAttr]
769
+ const hasRefPath = refPath.length > 0
770
+ // Build the path using repeater index when available
771
+ const fullRefPath =
772
+ typeof resolvedExpressions?.currentIndex === 'number' && hasRefPath
773
+ ? [resolvedExpressions.currentIndex.toString(), ...refPath]
774
+ : refPath
775
+
776
+ if (Array.isArray(resolvedValue)) {
777
+ // If the resolved value itself is an array definition, pass it through
778
+ propsForInstance[propKey] = resolvedValue
779
+ } else {
780
+ const defaultVal = resolvedValue?.defaultValue
781
+ const extracted =
782
+ hasRefPath && defaultVal !== undefined
783
+ ? extractDefaultValueFromRefPath(defaultVal, fullRefPath)
784
+ : defaultVal ?? null
785
+ propsForInstance[propKey] = {
786
+ ...resolvedValue,
787
+ defaultValue: extracted,
788
+ }
789
+ }
790
+ }
791
+
792
+ if (
793
+ attribute?.type !== 'dynamic' &&
794
+ attribute?.type !== 'element' &&
795
+ attribute?.type !== 'object' &&
796
+ attribute?.type !== 'expr'
797
+ ) {
798
+ propsForInstance[propKey] = {
799
+ ...combinedProps[propKey],
800
+ defaultValue: attribute?.content ?? combinedProps[propKey]?.defaultValue,
801
+ }
802
+ }
803
+
804
+ if (attribute === undefined) {
805
+ const propFromCurrentComponent = combinedProps[propKey]
806
+ propsForInstance[propKey] = propFromCurrentComponent
807
+ }
808
+ }
809
+
810
+ let componentWrapper = StringUtils.camelCaseToDashCase(`${componentName}-wrapper`)
811
+ const isExistingNode = nodesLookup[componentWrapper]
812
+ if (isExistingNode !== undefined) {
813
+ componentWrapper = `${componentWrapper}-${StringUtils.generateRandomString()}`
814
+ }
815
+
816
+ const componentInstanceToGenerate: UIDLElementNode = {
817
+ type: 'element',
818
+ content: {
819
+ elementType: componentWrapper,
820
+ key: componentWrapper,
821
+ children: [componentClone.node],
822
+ style: {
823
+ display: {
824
+ type: 'static',
825
+ content: 'contents',
295
826
  },
296
827
  },
297
828
  },
298
- lookUpTemplates,
829
+ }
830
+
831
+ const compTag = await generateHtmlSyntax(
832
+ componentInstanceToGenerate,
833
+ component.name,
834
+ nodesLookup,
299
835
  propsForInstance,
300
836
  statesForInstance,
301
837
  subComponentOptions,
302
- structure
303
- )) as unknown as HastNode
838
+ structure,
839
+ resolvedExpressions
840
+ )
304
841
 
305
842
  const cssPlugin = createCSSPlugin({
306
843
  templateStyle: 'html',
307
844
  templateChunkName: DEFAULT_COMPONENT_CHUNK_NAME,
308
845
  declareDependency: 'import',
309
- forceScoping: true,
310
- chunkName: comp.name,
846
+ chunkName: componentClone.name,
311
847
  staticPropReferences: true,
312
848
  })
313
849
 
314
850
  const initialStructure: ComponentStructure = {
315
851
  uidl: {
316
- ...comp,
852
+ ...componentClone,
853
+ node: componentInstanceToGenerate,
317
854
  propDefinitions: propsForInstance,
318
855
  stateDefinitions: statesForInstance,
319
856
  },
@@ -325,7 +862,7 @@ const generateComponentContent = async (
325
862
  linkAfter: [],
326
863
  content: compTag,
327
864
  meta: {
328
- nodesLookup: lookUpTemplates,
865
+ nodesLookup,
329
866
  },
330
867
  },
331
868
  ],
@@ -341,41 +878,105 @@ const generateComponentContent = async (
341
878
  Promise.resolve(initialStructure)
342
879
  )
343
880
 
344
- if (compHasSlots) {
345
- result.chunks.forEach((chunk) => {
346
- if (chunk.fileType === FileType.CSS) {
347
- chunks.push(chunk)
348
- }
349
- })
350
- } else {
351
- const chunk = chunks.find((item) => item.name === comp.name)
352
- if (!chunk) {
353
- const styleChunk = result.chunks.find(
354
- (item: ChunkDefinition) => item.fileType === FileType.CSS
355
- )
356
- if (!styleChunk) {
357
- return
358
- }
359
- chunks.push(styleChunk)
881
+ result.chunks.forEach((chunk) => {
882
+ if (chunk.fileType === FileType.CSS) {
883
+ chunks.push(chunk)
360
884
  }
361
- }
885
+ })
362
886
 
887
+ addNodeToLookup(node.content.key, compTag, nodesLookup)
363
888
  return compTag
364
889
  }
365
890
 
366
- const generateDynamicNode: NodeToHTML<UIDLDynamicReference, HastNode> = (
891
+ const generateDynamicNode: NodeToHTML<
892
+ UIDLDynamicReference,
893
+ Promise<HastNode | HastText | Array<HastNode | HastText>>
894
+ > = async (
367
895
  node,
368
- _,
896
+ compName,
897
+ nodesLookup,
369
898
  propDefinitions,
370
- stateDefinitions
371
- ) => {
372
- const spanTag = HASTBuilders.createHTMLNode('span')
373
- const usedReferenceValue =
374
- node.content.referenceType === 'prop'
375
- ? getValueFromReference(node.content.id, propDefinitions)
376
- : getValueFromReference(node.content.id, stateDefinitions)
899
+ stateDefinitions,
900
+ subComponentOptions,
901
+ structure,
902
+ resolvedExpressions?
903
+ ): Promise<HastNode | HastText | Array<HastNode | HastText>> => {
904
+ if (node.content.referenceType === 'locale') {
905
+ const localeTag = HASTBuilders.createHTMLNode('span')
906
+ const commentNode = HASTBuilders.createComment(`Content for locale ${node.content.id}`)
907
+ HASTUtils.addChildNode(localeTag, commentNode)
908
+ return localeTag
909
+ }
910
+
911
+ const usedReferenceValue = getValueFromReference(
912
+ node.content.id,
913
+ node.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
914
+ )
377
915
 
378
- HASTUtils.addTextNode(spanTag, String(usedReferenceValue))
916
+ if (
917
+ (usedReferenceValue.type === 'object' || usedReferenceValue.type === 'array') &&
918
+ usedReferenceValue.defaultValue
919
+ ) {
920
+ // Let's say users are biding the prop to a node using something like this "fields.Title"
921
+ // But the fields in the object is the value where the object is defined either in propDefinitions
922
+ // or on the attrs. So, we just need to parsed the rest of the object path and get the value from the object.
923
+ const extracted = extractDefaultValueFromRefPath(
924
+ usedReferenceValue.defaultValue as Record<string, UIDLPropDefinition>,
925
+ node.content.refPath
926
+ )
927
+ if (extracted === undefined || extracted === null) {
928
+ return HASTBuilders.createTextNode('')
929
+ }
930
+ return HASTBuilders.createTextNode(String(extracted))
931
+ }
932
+
933
+ if (usedReferenceValue.type === 'element') {
934
+ const elementNode = usedReferenceValue.defaultValue as UIDLElementNode
935
+ if (elementNode) {
936
+ // In repeater context, avoid reusing cached nodes; uniquify key per iteration
937
+ if (resolvedExpressions && typeof resolvedExpressions.currentIndex === 'number') {
938
+ const elementClone = UIDLUtils.cloneObject<UIDLElementNode>(elementNode)
939
+ if (elementClone?.content?.key) {
940
+ elementClone.content.key = `${elementClone.content.key}-${resolvedExpressions.currentIndex}`
941
+ }
942
+ const iterElementTag = await generateHtmlSyntax(
943
+ elementClone,
944
+ compName,
945
+ nodesLookup,
946
+ propDefinitions,
947
+ stateDefinitions,
948
+ subComponentOptions,
949
+ structure,
950
+ resolvedExpressions
951
+ )
952
+ return iterElementTag
953
+ }
954
+
955
+ if (elementNode.content.key in nodesLookup) {
956
+ return nodesLookup[elementNode.content.key]
957
+ }
958
+
959
+ const generatedElementTag = await generateHtmlSyntax(
960
+ elementNode,
961
+ compName,
962
+ nodesLookup,
963
+ propDefinitions,
964
+ stateDefinitions,
965
+ subComponentOptions,
966
+ structure,
967
+ resolvedExpressions
968
+ )
969
+ return generatedElementTag
970
+ }
971
+
972
+ const spanTagWrapper = HASTBuilders.createHTMLNode('span')
973
+ const commentNode = HASTBuilders.createComment(`Content for slot ${node.content.id}`)
974
+ HASTUtils.addChildNode(spanTagWrapper, commentNode)
975
+ return spanTagWrapper
976
+ }
977
+
978
+ const spanTag = HASTBuilders.createHTMLNode('span')
979
+ HASTUtils.addTextNode(spanTag, String(usedReferenceValue.defaultValue))
379
980
  return spanTag
380
981
  }
381
982
 
@@ -388,10 +989,14 @@ const handleStyles = (
388
989
  Object.keys(styles).forEach((styleKey) => {
389
990
  let style: string | UIDLStyleValue = styles[styleKey]
390
991
  if (style.type === 'dynamic' && style.content?.referenceType !== 'token') {
391
- if (style.content.referenceType === 'prop') {
392
- style = getValueFromReference(style.content.id, propDefinitions)
393
- } else if (style.content.referenceType === 'state') {
394
- style = getValueFromReference(style.content.id, stateDefinitions)
992
+ const referencedValue = getValueFromReference(
993
+ style.content.id,
994
+ style.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
995
+ )
996
+ if (referencedValue.type === 'string' || referencedValue.type === 'number') {
997
+ style = String(
998
+ extractDefaultValueFromRefPath(referencedValue.defaultValue, style?.content?.refPath)
999
+ )
395
1000
  }
396
1001
  node.content.style[styleKey] = typeof style === 'string' ? staticNode(style) : style
397
1002
  }
@@ -405,102 +1010,148 @@ const handleAttributes = (
405
1010
  propDefinitions: Record<string, UIDLPropDefinition>,
406
1011
  stateDefinitions: Record<string, UIDLStateDefinition>,
407
1012
  routeDefinitions: UIDLRouteDefinitions,
408
- outputOptions: UIDLComponentOutputOptions
1013
+ outputOptions: UIDLComponentOutputOptions,
1014
+ currentIndex?: number
409
1015
  ) => {
410
- Object.keys(attrs).forEach((attrKey) => {
1016
+ for (const attrKey of Object.keys(attrs)) {
411
1017
  const attrValue = attrs[attrKey]
1018
+ const { type, content } = attrValue
412
1019
 
413
- if (
414
- attrKey === 'href' &&
415
- attrValue.type === 'static' &&
416
- typeof attrValue.content === 'string' &&
417
- attrValue.content.startsWith('/')
418
- ) {
419
- let targetLink
1020
+ switch (type) {
1021
+ case 'static': {
1022
+ if (attrKey === 'href' && typeof content === 'string' && content.startsWith('/')) {
1023
+ let targetLink
420
1024
 
421
- const targetRoute = (routeDefinitions?.values || []).find(
422
- (route) => route.pageOptions.navLink === attrValue.content
423
- )
1025
+ const targetRoute = (routeDefinitions?.values || []).find(
1026
+ (route) => route.pageOptions.navLink === content
1027
+ )
1028
+
1029
+ if (targetRoute) {
1030
+ targetLink = targetRoute.pageOptions.navLink
1031
+ }
1032
+
1033
+ if (!targetRoute && content === '/home') {
1034
+ targetLink = '/'
1035
+ }
1036
+
1037
+ if (!targetLink && !targetRoute) {
1038
+ targetLink = content
1039
+ }
1040
+
1041
+ const currentPageRoute = join(...(outputOptions?.folderPath || []), './')
1042
+ const localPrefix = relative(
1043
+ `/${currentPageRoute}`,
1044
+ `/${targetLink === '/' ? 'index' : targetLink}`
1045
+ )
1046
+
1047
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, `${localPrefix}.html`)
1048
+ break
1049
+ }
1050
+
1051
+ if (typeof content === 'boolean') {
1052
+ htmlNode.properties[attrKey] = content === true ? 'true' : 'false'
1053
+ } else if (typeof content === 'string' || typeof attrValue.content === 'number') {
1054
+ let value = StringUtils.encode(String(attrValue.content))
1055
+
1056
+ /*
1057
+ elementType of image is always mapped to img.
1058
+ For reference, check `html-mapping` file.
1059
+ */
1060
+ if (elementType === 'img' && attrKey === 'src' && !isValidURL(value)) {
1061
+ /*
1062
+ By default we just prefix all the asset paths with just the
1063
+ assetPrefix that is configured in the project. But for `html` generators
1064
+ we need to prefix that with the current file location.
1065
+
1066
+ Because, all the other frameworks have a build setup. which serves all the
1067
+ assets from the `public` folder. But in the case of `html` here is how it works
1068
+
1069
+ We load a file from `index.html` the request for the image goes from
1070
+ '...url.../public/...image...'
1071
+ If it's a nested url, then the request goes from
1072
+ '...url/nested/public/...image..'
424
1073
 
425
- if (targetRoute) {
426
- targetLink = targetRoute.pageOptions.navLink
1074
+ But the nested folder is available only on the root. With this
1075
+ The url changes prefixes to
1076
+
1077
+ ../public/playground_assets/..image.. etc depending on the dept the file is in.
1078
+ */
1079
+ value = join(relative(join(...outputOptions.folderPath), './'), value)
1080
+ }
1081
+
1082
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
1083
+ }
1084
+
1085
+ break
427
1086
  }
428
1087
 
429
- if (!targetRoute && attrValue.content === '/home') {
430
- targetLink = '/'
1088
+ case 'dynamic': {
1089
+ const value = getValueFromReference(
1090
+ content.id,
1091
+ content.referenceType === 'prop' ? propDefinitions : stateDefinitions
1092
+ )
1093
+
1094
+ const extracted = extractDefaultValueFromRefPath(value.defaultValue, content.refPath)
1095
+ const extractedValue = String(extracted)
1096
+
1097
+ if (
1098
+ (elementType === 'img' || elementType === 'video') &&
1099
+ attrKey === 'src' &&
1100
+ !extractedValue.startsWith('http')
1101
+ ) {
1102
+ const path = join(relative(join(...outputOptions.folderPath), './'), extractedValue)
1103
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, path)
1104
+ break
1105
+ }
1106
+
1107
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, extractedValue)
1108
+ break
431
1109
  }
432
1110
 
433
- if (!targetLink && !targetRoute) {
434
- targetLink = attrValue.content
1111
+ case 'raw': {
1112
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, content)
1113
+ break
435
1114
  }
436
1115
 
437
- const currentPageRoute = join(...(outputOptions?.folderPath || []), './')
438
- const localPrefix = relative(
439
- `/${currentPageRoute}`,
440
- `/${targetLink === '/' ? 'index' : targetLink}`
441
- )
1116
+ case 'expr': {
1117
+ const fullPath = content.split('?.')
1118
+ const prop = propDefinitions[fullPath?.[0] || '']
442
1119
 
443
- HASTUtils.addAttributeToNode(htmlNode, attrKey, `${localPrefix}.html`)
444
- return
445
- }
1120
+ if (!prop) {
1121
+ break
1122
+ }
446
1123
 
447
- if (attrValue.type === 'dynamic') {
448
- const value =
449
- attrValue.content.referenceType === 'prop'
450
- ? getValueFromReference(attrValue.content.id, propDefinitions)
451
- : getValueFromReference(attrValue.content.id, stateDefinitions)
452
- HASTUtils.addAttributeToNode(htmlNode, attrKey, String(value))
453
- return
454
- }
1124
+ const path =
1125
+ typeof currentIndex === 'number'
1126
+ ? [currentIndex.toString(), ...fullPath.slice(1)]
1127
+ : fullPath.slice(1)
1128
+ const value = extractDefaultValueFromRefPath(prop.defaultValue, path)
1129
+ if (!value) {
1130
+ break
1131
+ }
1132
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, String(value))
1133
+ break
1134
+ }
455
1135
 
456
- if (attrValue.type === 'raw') {
457
- HASTUtils.addAttributeToNode(htmlNode, attrKey, attrValue.content)
458
- return
459
- }
1136
+ case 'element':
1137
+ case 'import':
1138
+ case 'object':
1139
+ break
460
1140
 
461
- if (typeof attrValue.content === 'boolean') {
462
- HASTUtils.addBooleanAttributeToNode(htmlNode, attrKey)
463
- return
464
- } else if (typeof attrValue.content === 'string' || typeof attrValue.content === 'number') {
465
- let value = StringUtils.encode(String(attrValue.content))
466
-
467
- /*
468
- elementType of image is always mapped to img.
469
- For reference, check `html-mapping` file.
470
- */
471
- if (elementType === 'img' && attrKey === 'src') {
472
- /*
473
- By default we just prefix all the asset paths with just the
474
- assetPrefix that is configured in the project. But for `html` generators
475
- we need to prefix that with the current file location.
476
-
477
- Because, all the other frameworks have a build setup. which serves all the
478
- assets from the `public` folder. But in the case of `html` here is how it works
479
-
480
- We load a file from `index.html` the request for the image goes from
481
- '...url.../public/...image...'
482
- If it's a nested url, then the request goes from
483
- '...url/nested/public/...image..'
484
-
485
- But the nested folder is available only on the root. With this
486
- The url changes prefixes to
487
-
488
- ../public/playground_assets/..image.. etc depending on the dept the file is in.
489
- */
490
- value = join(relative(join(...outputOptions.folderPath), './'), value)
1141
+ default: {
1142
+ throw new HTMLComponentGeneratorError(
1143
+ `Received ${JSON.stringify(attrValue, null, 2)} \n in handleAttributes for html`
1144
+ )
491
1145
  }
492
-
493
- HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
494
- return
495
1146
  }
496
- })
1147
+ }
497
1148
  }
498
1149
 
499
1150
  const getValueFromReference = (
500
1151
  key: string,
501
1152
  definitions: Record<string, UIDLPropDefinition>
502
- ): string => {
503
- const usedReferenceValue = definitions[key.includes('.') ? key.split('.')[0] : key]
1153
+ ): UIDLPropDefinition | undefined => {
1154
+ const usedReferenceValue = definitions[key.includes('?.') ? key.split('?.')[0] : key]
504
1155
 
505
1156
  if (!usedReferenceValue) {
506
1157
  throw new HTMLComponentGeneratorError(
@@ -508,9 +1159,13 @@ const getValueFromReference = (
508
1159
  )
509
1160
  }
510
1161
 
511
- if (!usedReferenceValue.hasOwnProperty('defaultValue')) {
1162
+ if (
1163
+ ['string', 'number', 'object', 'element', 'array', 'boolean'].includes(
1164
+ usedReferenceValue?.type
1165
+ ) === false
1166
+ ) {
512
1167
  throw new HTMLComponentGeneratorError(
513
- `Default value is missing from dynamic reference - ${JSON.stringify(
1168
+ `Attribute is using dynamic value, but received of type ${JSON.stringify(
514
1169
  usedReferenceValue,
515
1170
  null,
516
1171
  2
@@ -518,9 +1173,12 @@ const getValueFromReference = (
518
1173
  )
519
1174
  }
520
1175
 
521
- if (!['string', 'number', 'object'].includes(usedReferenceValue?.type)) {
1176
+ if (
1177
+ usedReferenceValue.type !== 'element' &&
1178
+ usedReferenceValue.hasOwnProperty('defaultValue') === false
1179
+ ) {
522
1180
  throw new HTMLComponentGeneratorError(
523
- `Attribute is using dynamic value, but received of type ${JSON.stringify(
1181
+ `Default value is missing from dynamic reference - ${JSON.stringify(
524
1182
  usedReferenceValue,
525
1183
  null,
526
1184
  2
@@ -528,5 +1186,33 @@ const getValueFromReference = (
528
1186
  )
529
1187
  }
530
1188
 
531
- return String(usedReferenceValue.defaultValue)
1189
+ return usedReferenceValue
1190
+ }
1191
+
1192
+ const extractDefaultValueFromRefPath = (
1193
+ propDefaultValue: PropDefaultValueTypes,
1194
+ refPath?: string[]
1195
+ ): PropDefaultValueTypes => {
1196
+ if (!refPath || refPath.length === 0) {
1197
+ return propDefaultValue
1198
+ }
1199
+
1200
+ // Directly handle array indexing for the first segment when applicable
1201
+ if (Array.isArray(propDefaultValue)) {
1202
+ const [first, ...rest] = refPath
1203
+ const idx = Number(first)
1204
+ if (!Number.isNaN(idx) && idx >= 0 && idx < propDefaultValue.length) {
1205
+ const nextVal = propDefaultValue[idx] as PropDefaultValueTypes
1206
+ if (rest.length === 0) {
1207
+ return nextVal
1208
+ }
1209
+ return extractDefaultValueFromRefPath(nextVal, rest)
1210
+ }
1211
+ }
1212
+
1213
+ if (typeof propDefaultValue !== 'object') {
1214
+ return propDefaultValue
1215
+ }
1216
+
1217
+ return GenericUtils.getValueFromPath(refPath.join('.'), propDefaultValue) as PropDefaultValueTypes
532
1218
  }