@teleporthq/teleport-plugin-html-base-component 0.40.0-alpha.0 → 0.40.1

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