@teleporthq/teleport-plugin-html-base-component 0.42.0-alpha.0 → 0.42.2

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(
133
+ node,
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(
80
262
  node,
81
- templatesLookUp,
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,149 @@ 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
+ // Transfer data-node attributes from component instance to wrapper
817
+ const wrapperAttrs: Record<string, UIDLAttributeValue> = {}
818
+ Object.keys(attrs).forEach((attrKey) => {
819
+ if (attrKey.startsWith('dataNode') || attrKey.startsWith('data-node')) {
820
+ wrapperAttrs[attrKey] = attrs[attrKey]
821
+ }
822
+ })
823
+
824
+ const componentInstanceToGenerate: UIDLElementNode = {
825
+ type: 'element',
826
+ content: {
827
+ elementType: componentWrapper,
828
+ key: componentWrapper,
829
+ children: [componentClone.node],
830
+ attrs: wrapperAttrs,
831
+ style: {
832
+ display: {
833
+ type: 'static',
834
+ content: 'contents',
295
835
  },
296
836
  },
297
837
  },
298
- lookUpTemplates,
838
+ }
839
+
840
+ const compTag = await generateHtmlSyntax(
841
+ componentInstanceToGenerate,
842
+ component.name,
843
+ nodesLookup,
299
844
  propsForInstance,
300
845
  statesForInstance,
301
846
  subComponentOptions,
302
- structure
303
- )) as unknown as HastNode
847
+ structure,
848
+ resolvedExpressions
849
+ )
304
850
 
305
851
  const cssPlugin = createCSSPlugin({
306
852
  templateStyle: 'html',
307
853
  templateChunkName: DEFAULT_COMPONENT_CHUNK_NAME,
308
854
  declareDependency: 'import',
309
- forceScoping: true,
310
- chunkName: comp.name,
855
+ chunkName: componentClone.name,
311
856
  staticPropReferences: true,
312
857
  })
313
858
 
314
859
  const initialStructure: ComponentStructure = {
315
860
  uidl: {
316
- ...comp,
861
+ ...componentClone,
862
+ node: componentInstanceToGenerate,
317
863
  propDefinitions: propsForInstance,
318
864
  stateDefinitions: statesForInstance,
319
865
  },
@@ -325,7 +871,7 @@ const generateComponentContent = async (
325
871
  linkAfter: [],
326
872
  content: compTag,
327
873
  meta: {
328
- nodesLookup: lookUpTemplates,
874
+ nodesLookup,
329
875
  },
330
876
  },
331
877
  ],
@@ -341,41 +887,105 @@ const generateComponentContent = async (
341
887
  Promise.resolve(initialStructure)
342
888
  )
343
889
 
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)
890
+ result.chunks.forEach((chunk) => {
891
+ if (chunk.fileType === FileType.CSS) {
892
+ chunks.push(chunk)
360
893
  }
361
- }
894
+ })
362
895
 
896
+ addNodeToLookup(node.content.key, compTag, nodesLookup)
363
897
  return compTag
364
898
  }
365
899
 
366
- const generateDynamicNode: NodeToHTML<UIDLDynamicReference, HastNode> = (
900
+ const generateDynamicNode: NodeToHTML<
901
+ UIDLDynamicReference,
902
+ Promise<HastNode | HastText | Array<HastNode | HastText>>
903
+ > = async (
367
904
  node,
368
- _,
905
+ compName,
906
+ nodesLookup,
369
907
  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)
908
+ stateDefinitions,
909
+ subComponentOptions,
910
+ structure,
911
+ resolvedExpressions?
912
+ ): Promise<HastNode | HastText | Array<HastNode | HastText>> => {
913
+ if (node.content.referenceType === 'locale') {
914
+ const localeTag = HASTBuilders.createHTMLNode('span')
915
+ const commentNode = HASTBuilders.createComment(`Content for locale ${node.content.id}`)
916
+ HASTUtils.addChildNode(localeTag, commentNode)
917
+ return localeTag
918
+ }
919
+
920
+ const usedReferenceValue = getValueFromReference(
921
+ node.content.id,
922
+ node.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
923
+ )
924
+
925
+ if (
926
+ (usedReferenceValue.type === 'object' || usedReferenceValue.type === 'array') &&
927
+ usedReferenceValue.defaultValue
928
+ ) {
929
+ // Let's say users are biding the prop to a node using something like this "fields.Title"
930
+ // But the fields in the object is the value where the object is defined either in propDefinitions
931
+ // or on the attrs. So, we just need to parsed the rest of the object path and get the value from the object.
932
+ const extracted = extractDefaultValueFromRefPath(
933
+ usedReferenceValue.defaultValue as Record<string, UIDLPropDefinition>,
934
+ node.content.refPath
935
+ )
936
+ if (extracted === undefined || extracted === null) {
937
+ return HASTBuilders.createTextNode('')
938
+ }
939
+ return HASTBuilders.createTextNode(String(extracted))
940
+ }
377
941
 
378
- HASTUtils.addTextNode(spanTag, String(usedReferenceValue))
942
+ if (usedReferenceValue.type === 'element') {
943
+ const elementNode = usedReferenceValue.defaultValue as UIDLElementNode
944
+ if (elementNode) {
945
+ // In repeater context, avoid reusing cached nodes; uniquify key per iteration
946
+ if (resolvedExpressions && typeof resolvedExpressions.currentIndex === 'number') {
947
+ const elementClone = UIDLUtils.cloneObject<UIDLElementNode>(elementNode)
948
+ if (elementClone?.content?.key) {
949
+ elementClone.content.key = `${elementClone.content.key}-${resolvedExpressions.currentIndex}`
950
+ }
951
+ const iterElementTag = await generateHtmlSyntax(
952
+ elementClone,
953
+ compName,
954
+ nodesLookup,
955
+ propDefinitions,
956
+ stateDefinitions,
957
+ subComponentOptions,
958
+ structure,
959
+ resolvedExpressions
960
+ )
961
+ return iterElementTag
962
+ }
963
+
964
+ if (elementNode.content.key in nodesLookup) {
965
+ return nodesLookup[elementNode.content.key]
966
+ }
967
+
968
+ const generatedElementTag = await generateHtmlSyntax(
969
+ elementNode,
970
+ compName,
971
+ nodesLookup,
972
+ propDefinitions,
973
+ stateDefinitions,
974
+ subComponentOptions,
975
+ structure,
976
+ resolvedExpressions
977
+ )
978
+ return generatedElementTag
979
+ }
980
+
981
+ const spanTagWrapper = HASTBuilders.createHTMLNode('span')
982
+ const commentNode = HASTBuilders.createComment(`Content for slot ${node.content.id}`)
983
+ HASTUtils.addChildNode(spanTagWrapper, commentNode)
984
+ return spanTagWrapper
985
+ }
986
+
987
+ const spanTag = HASTBuilders.createHTMLNode('span')
988
+ HASTUtils.addTextNode(spanTag, String(usedReferenceValue.defaultValue))
379
989
  return spanTag
380
990
  }
381
991
 
@@ -388,10 +998,14 @@ const handleStyles = (
388
998
  Object.keys(styles).forEach((styleKey) => {
389
999
  let style: string | UIDLStyleValue = styles[styleKey]
390
1000
  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)
1001
+ const referencedValue = getValueFromReference(
1002
+ style.content.id,
1003
+ style.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
1004
+ )
1005
+ if (referencedValue.type === 'string' || referencedValue.type === 'number') {
1006
+ style = String(
1007
+ extractDefaultValueFromRefPath(referencedValue.defaultValue, style?.content?.refPath)
1008
+ )
395
1009
  }
396
1010
  node.content.style[styleKey] = typeof style === 'string' ? staticNode(style) : style
397
1011
  }
@@ -405,102 +1019,148 @@ const handleAttributes = (
405
1019
  propDefinitions: Record<string, UIDLPropDefinition>,
406
1020
  stateDefinitions: Record<string, UIDLStateDefinition>,
407
1021
  routeDefinitions: UIDLRouteDefinitions,
408
- outputOptions: UIDLComponentOutputOptions
1022
+ outputOptions: UIDLComponentOutputOptions,
1023
+ currentIndex?: number
409
1024
  ) => {
410
- Object.keys(attrs).forEach((attrKey) => {
1025
+ for (const attrKey of Object.keys(attrs)) {
411
1026
  const attrValue = attrs[attrKey]
1027
+ const { type, content } = attrValue
412
1028
 
413
- if (
414
- attrKey === 'href' &&
415
- attrValue.type === 'static' &&
416
- typeof attrValue.content === 'string' &&
417
- attrValue.content.startsWith('/')
418
- ) {
419
- let targetLink
1029
+ switch (type) {
1030
+ case 'static': {
1031
+ if (attrKey === 'href' && typeof content === 'string' && content.startsWith('/')) {
1032
+ let targetLink
420
1033
 
421
- const targetRoute = (routeDefinitions?.values || []).find(
422
- (route) => route.pageOptions.navLink === attrValue.content
423
- )
1034
+ const targetRoute = (routeDefinitions?.values || []).find(
1035
+ (route) => route.pageOptions.navLink === content
1036
+ )
1037
+
1038
+ if (targetRoute) {
1039
+ targetLink = targetRoute.pageOptions.navLink
1040
+ }
1041
+
1042
+ if (!targetRoute && content === '/home') {
1043
+ targetLink = '/'
1044
+ }
1045
+
1046
+ if (!targetLink && !targetRoute) {
1047
+ targetLink = content
1048
+ }
1049
+
1050
+ const currentPageRoute = join(...(outputOptions?.folderPath || []), './')
1051
+ const localPrefix = relative(
1052
+ `/${currentPageRoute}`,
1053
+ `/${targetLink === '/' ? 'index' : targetLink}`
1054
+ )
1055
+
1056
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, `${localPrefix}.html`)
1057
+ break
1058
+ }
1059
+
1060
+ if (typeof content === 'boolean') {
1061
+ htmlNode.properties[attrKey] = content === true ? 'true' : 'false'
1062
+ } else if (typeof content === 'string' || typeof attrValue.content === 'number') {
1063
+ let value = StringUtils.encode(String(attrValue.content))
1064
+
1065
+ /*
1066
+ elementType of image is always mapped to img.
1067
+ For reference, check `html-mapping` file.
1068
+ */
1069
+ if (elementType === 'img' && attrKey === 'src' && !isValidURL(value)) {
1070
+ /*
1071
+ By default we just prefix all the asset paths with just the
1072
+ assetPrefix that is configured in the project. But for `html` generators
1073
+ we need to prefix that with the current file location.
1074
+
1075
+ Because, all the other frameworks have a build setup. which serves all the
1076
+ assets from the `public` folder. But in the case of `html` here is how it works
424
1077
 
425
- if (targetRoute) {
426
- targetLink = targetRoute.pageOptions.navLink
1078
+ We load a file from `index.html` the request for the image goes from
1079
+ '...url.../public/...image...'
1080
+ If it's a nested url, then the request goes from
1081
+ '...url/nested/public/...image..'
1082
+
1083
+ But the nested folder is available only on the root. With this
1084
+ The url changes prefixes to
1085
+
1086
+ ../public/playground_assets/..image.. etc depending on the dept the file is in.
1087
+ */
1088
+ value = join(relative(join(...outputOptions.folderPath), './'), value)
1089
+ }
1090
+
1091
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
1092
+ }
1093
+
1094
+ break
427
1095
  }
428
1096
 
429
- if (!targetRoute && attrValue.content === '/home') {
430
- targetLink = '/'
1097
+ case 'dynamic': {
1098
+ const value = getValueFromReference(
1099
+ content.id,
1100
+ content.referenceType === 'prop' ? propDefinitions : stateDefinitions
1101
+ )
1102
+
1103
+ const extracted = extractDefaultValueFromRefPath(value.defaultValue, content.refPath)
1104
+ const extractedValue = String(extracted)
1105
+
1106
+ if (
1107
+ (elementType === 'img' || elementType === 'video') &&
1108
+ attrKey === 'src' &&
1109
+ !extractedValue.startsWith('http')
1110
+ ) {
1111
+ const path = join(relative(join(...outputOptions.folderPath), './'), extractedValue)
1112
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, path)
1113
+ break
1114
+ }
1115
+
1116
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, extractedValue)
1117
+ break
431
1118
  }
432
1119
 
433
- if (!targetLink && !targetRoute) {
434
- targetLink = attrValue.content
1120
+ case 'raw': {
1121
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, content)
1122
+ break
435
1123
  }
436
1124
 
437
- const currentPageRoute = join(...(outputOptions?.folderPath || []), './')
438
- const localPrefix = relative(
439
- `/${currentPageRoute}`,
440
- `/${targetLink === '/' ? 'index' : targetLink}`
441
- )
1125
+ case 'expr': {
1126
+ const fullPath = content.split('?.')
1127
+ const prop = propDefinitions[fullPath?.[0] || '']
442
1128
 
443
- HASTUtils.addAttributeToNode(htmlNode, attrKey, `${localPrefix}.html`)
444
- return
445
- }
1129
+ if (!prop) {
1130
+ break
1131
+ }
446
1132
 
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
- }
1133
+ const path =
1134
+ typeof currentIndex === 'number'
1135
+ ? [currentIndex.toString(), ...fullPath.slice(1)]
1136
+ : fullPath.slice(1)
1137
+ const value = extractDefaultValueFromRefPath(prop.defaultValue, path)
1138
+ if (!value) {
1139
+ break
1140
+ }
1141
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, String(value))
1142
+ break
1143
+ }
455
1144
 
456
- if (attrValue.type === 'raw') {
457
- HASTUtils.addAttributeToNode(htmlNode, attrKey, attrValue.content)
458
- return
459
- }
1145
+ case 'element':
1146
+ case 'import':
1147
+ case 'object':
1148
+ break
460
1149
 
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)
1150
+ default: {
1151
+ throw new HTMLComponentGeneratorError(
1152
+ `Received ${JSON.stringify(attrValue, null, 2)} \n in handleAttributes for html`
1153
+ )
491
1154
  }
492
-
493
- HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
494
- return
495
1155
  }
496
- })
1156
+ }
497
1157
  }
498
1158
 
499
1159
  const getValueFromReference = (
500
1160
  key: string,
501
1161
  definitions: Record<string, UIDLPropDefinition>
502
- ): string => {
503
- const usedReferenceValue = definitions[key.includes('.') ? key.split('.')[0] : key]
1162
+ ): UIDLPropDefinition | undefined => {
1163
+ const usedReferenceValue = definitions[key.includes('?.') ? key.split('?.')[0] : key]
504
1164
 
505
1165
  if (!usedReferenceValue) {
506
1166
  throw new HTMLComponentGeneratorError(
@@ -508,9 +1168,13 @@ const getValueFromReference = (
508
1168
  )
509
1169
  }
510
1170
 
511
- if (!usedReferenceValue.hasOwnProperty('defaultValue')) {
1171
+ if (
1172
+ ['string', 'number', 'object', 'element', 'array', 'boolean'].includes(
1173
+ usedReferenceValue?.type
1174
+ ) === false
1175
+ ) {
512
1176
  throw new HTMLComponentGeneratorError(
513
- `Default value is missing from dynamic reference - ${JSON.stringify(
1177
+ `Attribute is using dynamic value, but received of type ${JSON.stringify(
514
1178
  usedReferenceValue,
515
1179
  null,
516
1180
  2
@@ -518,9 +1182,12 @@ const getValueFromReference = (
518
1182
  )
519
1183
  }
520
1184
 
521
- if (!['string', 'number', 'object'].includes(usedReferenceValue?.type)) {
1185
+ if (
1186
+ usedReferenceValue.type !== 'element' &&
1187
+ usedReferenceValue.hasOwnProperty('defaultValue') === false
1188
+ ) {
522
1189
  throw new HTMLComponentGeneratorError(
523
- `Attribute is using dynamic value, but received of type ${JSON.stringify(
1190
+ `Default value is missing from dynamic reference - ${JSON.stringify(
524
1191
  usedReferenceValue,
525
1192
  null,
526
1193
  2
@@ -528,5 +1195,33 @@ const getValueFromReference = (
528
1195
  )
529
1196
  }
530
1197
 
531
- return String(usedReferenceValue.defaultValue)
1198
+ return usedReferenceValue
1199
+ }
1200
+
1201
+ const extractDefaultValueFromRefPath = (
1202
+ propDefaultValue: PropDefaultValueTypes,
1203
+ refPath?: string[]
1204
+ ): PropDefaultValueTypes => {
1205
+ if (!refPath || refPath.length === 0) {
1206
+ return propDefaultValue
1207
+ }
1208
+
1209
+ // Directly handle array indexing for the first segment when applicable
1210
+ if (Array.isArray(propDefaultValue)) {
1211
+ const [first, ...rest] = refPath
1212
+ const idx = Number(first)
1213
+ if (!Number.isNaN(idx) && idx >= 0 && idx < propDefaultValue.length) {
1214
+ const nextVal = propDefaultValue[idx] as PropDefaultValueTypes
1215
+ if (rest.length === 0) {
1216
+ return nextVal
1217
+ }
1218
+ return extractDefaultValueFromRefPath(nextVal, rest)
1219
+ }
1220
+ }
1221
+
1222
+ if (typeof propDefaultValue !== 'object') {
1223
+ return propDefaultValue
1224
+ }
1225
+
1226
+ return GenericUtils.getValueFromPath(refPath.join('.'), propDefaultValue) as PropDefaultValueTypes
532
1227
  }