@teleporthq/teleport-plugin-html-base-component 0.38.0-alpha.0 → 0.38.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.
@@ -19,37 +19,79 @@ import {
19
19
  UIDLRouteDefinitions,
20
20
  ComponentPlugin,
21
21
  ComponentStructure,
22
+ UIDLComponentOutputOptions,
23
+ UIDLElement,
24
+ ElementsLookup,
25
+ UIDLConditionalNode,
26
+ PropDefaultValueTypes,
22
27
  } from '@teleporthq/teleport-types'
23
- import { HASTBuilders, HASTUtils } from '@teleporthq/teleport-plugin-common'
24
- import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
28
+ import { join, relative } from 'path'
29
+ import { HASTBuilders, HASTUtils, ASTUtils } from '@teleporthq/teleport-plugin-common'
30
+ import { GenericUtils, StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
25
31
  import { staticNode } from '@teleporthq/teleport-uidl-builders'
26
32
  import { createCSSPlugin } from '@teleporthq/teleport-plugin-css'
33
+ import { generateUniqueKeys, createNodesLookup } from '@teleporthq/teleport-uidl-resolver'
27
34
  import { DEFAULT_COMPONENT_CHUNK_NAME } from './constants'
28
35
 
36
+ const isValidURL = (url: string) => {
37
+ try {
38
+ /* tslint:disable:no-unused-expression */
39
+ new URL(url)
40
+ return true
41
+ } catch (error) {
42
+ return false
43
+ }
44
+ }
45
+
46
+ const addNodeToLookup = (
47
+ key: string,
48
+ node: UIDLElementNode,
49
+ tag: HastNode | HastText,
50
+ nodesLoookup: Record<string, HastNode | HastText>,
51
+ hierarchy: string[] = []
52
+ ) => {
53
+ // In html code-generation we combine the nodes of the component that is being consumed with the current component.
54
+ // As html can't load the component at runtime like react or any other frameworks. So, we merge the component as a standalone
55
+ // component in the current component.
56
+ if (nodesLoookup[key]) {
57
+ throw new HTMLComponentGeneratorError(
58
+ `\n${hierarchy.join(' -> ')} \n
59
+ Duplicate key found in nodesLookup: ${node.content.key} \n
60
+
61
+ A node with the same key already exists\n
62
+ Received \n\n ${JSON.stringify(tag)}\n ${JSON.stringify(node)}
63
+ Existing \n\n ${JSON.stringify(nodesLoookup[key])} \n\n`
64
+ )
65
+ }
66
+
67
+ nodesLoookup[key] = tag
68
+ }
69
+
29
70
  type NodeToHTML<NodeType, ReturnType> = (
30
71
  node: NodeType,
31
- templatesLookUp: Record<string, unknown>,
72
+ componentName: string,
73
+ nodesLookup: Record<string, HastNode | HastText>,
32
74
  propDefinitions: Record<string, UIDLPropDefinition>,
33
75
  stateDefinitions: Record<string, UIDLStateDefinition>,
34
76
  subComponentOptions: {
35
77
  externals: Record<string, ComponentUIDL>
36
78
  plugins: ComponentPlugin[]
37
79
  },
38
- routeDefinitions: UIDLRouteDefinitions,
39
80
  structure: {
40
81
  chunks: ChunkDefinition[]
41
82
  dependencies: Record<string, UIDLDependency>
42
83
  options: GeneratorOptions
84
+ outputOptions: UIDLComponentOutputOptions
43
85
  }
44
86
  ) => ReturnType
45
87
 
46
- export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastText>> = async (
88
+ export const generateHtmlSyntax: NodeToHTML<UIDLNode, Promise<HastNode | HastText>> = async (
47
89
  node,
48
- templatesLookUp,
90
+ compName,
91
+ nodesLookup,
49
92
  propDefinitions,
50
93
  stateDefinitions,
51
94
  subComponentOptions,
52
- routeDefinitions,
53
95
  structure
54
96
  ) => {
55
97
  switch (node.type) {
@@ -64,26 +106,103 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
64
106
  return HASTBuilders.createHTMLNode(node.type)
65
107
 
66
108
  case 'element':
67
- return generatElementNode(
109
+ const elementNode = await generateElementNode(
68
110
  node,
69
- templatesLookUp,
111
+ compName,
112
+ nodesLookup,
70
113
  propDefinitions,
71
114
  stateDefinitions,
72
115
  subComponentOptions,
73
- routeDefinitions,
74
116
  structure
75
117
  )
118
+ return elementNode
76
119
 
77
120
  case 'dynamic':
78
- return generateDynamicNode(
121
+ const dynamicNode = await generateDynamicNode(
79
122
  node,
80
- templatesLookUp,
123
+ compName,
124
+ nodesLookup,
81
125
  propDefinitions,
82
126
  stateDefinitions,
83
127
  subComponentOptions,
84
- routeDefinitions,
85
128
  structure
86
129
  )
130
+ return dynamicNode
131
+
132
+ case 'conditional':
133
+ const conditionalNodeComment = HASTBuilders.createComment(
134
+ 'Conditional nodes are not supported in HTML'
135
+ )
136
+ const {
137
+ value: staticValue,
138
+ reference,
139
+ condition: { conditions, matchingCriteria },
140
+ } = node.content
141
+
142
+ if (reference.type !== 'dynamic') {
143
+ return conditionalNodeComment
144
+ }
145
+
146
+ const {
147
+ content: { referenceType, id, refPath },
148
+ } = reference
149
+
150
+ switch (referenceType) {
151
+ case 'prop': {
152
+ const usedProp = propDefinitions[id]
153
+ if (usedProp === undefined || usedProp.defaultValue === undefined) {
154
+ return conditionalNodeComment
155
+ }
156
+
157
+ let defaultValue = usedProp.defaultValue
158
+ for (const path of refPath) {
159
+ defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
160
+ }
161
+
162
+ // Safety measure in case no value is found
163
+ if (!defaultValue) {
164
+ defaultValue = usedProp.defaultValue
165
+ }
166
+
167
+ // Since we know the operand and the default value from the prop.
168
+ // We can try building the condition and check if the condition is true or false.
169
+ // @todo: You can only use a 'value' in UIDL or 'conditions' but not both.
170
+ // UIDL validations need to be improved on this aspect.
171
+ const dynamicConditions = createConditionalStatement(
172
+ staticValue !== undefined ? [{ operand: staticValue, operation: '===' }] : conditions,
173
+ defaultValue
174
+ )
175
+ const matchCondition = matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
176
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
177
+
178
+ try {
179
+ // tslint:disable-next-line function-constructor
180
+ const isConditionPassing = new Function(`return ${conditionString}`)()
181
+ if (isConditionPassing) {
182
+ return generateHtmlSyntax(
183
+ node.content.node,
184
+ compName,
185
+ nodesLookup,
186
+ propDefinitions,
187
+ stateDefinitions,
188
+ subComponentOptions,
189
+ structure
190
+ )
191
+ }
192
+ } catch (error) {
193
+ return conditionalNodeComment
194
+ }
195
+
196
+ return conditionalNodeComment
197
+ }
198
+
199
+ case 'state':
200
+ default:
201
+ return conditionalNodeComment
202
+ }
203
+
204
+ case 'expr':
205
+ return HASTBuilders.createComment('Expressions are not supported in HTML')
87
206
 
88
207
  default:
89
208
  throw new HTMLComponentGeneratorError(
@@ -96,13 +215,47 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
96
215
  }
97
216
  }
98
217
 
99
- const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastText>> = async (
218
+ const createConditionalStatement = (
219
+ conditions: UIDLConditionalNode['content']['condition']['conditions'],
220
+ leftOperand: UIDLPropDefinition['defaultValue']
221
+ ) => {
222
+ return conditions.map((condition) => {
223
+ const { operation, operand } = condition
224
+
225
+ if (operand === undefined) {
226
+ return `${ASTUtils.convertToUnaryOperator(operation)}${getValueType(operand)}`
227
+ }
228
+
229
+ return `${getValueType(leftOperand)} ${ASTUtils.convertToBinaryOperator(
230
+ operation
231
+ )} ${getValueType(operand)}`
232
+ })
233
+ }
234
+
235
+ const getValueType = (value: UIDLPropDefinition['defaultValue']) => {
236
+ const valueType = typeof value
237
+ switch (valueType) {
238
+ case 'string':
239
+ return `"${value}"`
240
+ case 'number':
241
+ return value
242
+ case 'boolean':
243
+ return value
244
+ default:
245
+ throw new HTMLComponentGeneratorError(
246
+ `Conditional node received an operand of type ${valueType} \n
247
+ Received ${JSON.stringify(value)}`
248
+ )
249
+ }
250
+ }
251
+
252
+ const generateElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastText>> = async (
100
253
  node,
101
- templatesLookUp,
254
+ compName,
255
+ nodesLookup,
102
256
  propDefinitions,
103
257
  stateDefinitions,
104
258
  subComponentOptions,
105
- routeDefinitions,
106
259
  structure
107
260
  ) => {
108
261
  const {
@@ -112,45 +265,44 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
112
265
  style = {},
113
266
  referencedStyles = {},
114
267
  dependency,
115
- key,
116
268
  } = node.content
117
-
118
- const elementNode = HASTBuilders.createHTMLNode(elementType)
119
- templatesLookUp[key] = elementNode
120
-
121
269
  const { dependencies } = structure
122
- if (dependency && (dependency as UIDLDependency)?.type !== 'local') {
123
- dependencies[dependency.path] = dependency
124
- }
125
-
126
270
  if (dependency && (dependency as UIDLDependency)?.type === 'local') {
127
271
  const compTag = await generateComponentContent(
128
272
  node,
273
+ compName,
274
+ nodesLookup,
129
275
  propDefinitions,
130
276
  stateDefinitions,
131
277
  subComponentOptions,
132
- routeDefinitions,
133
278
  structure
134
279
  )
280
+
281
+ if ('tagName' in compTag) {
282
+ compTag.children.unshift(HASTBuilders.createComment(`${node.content.semanticType} component`))
283
+ }
284
+
135
285
  return compTag
136
286
  }
137
287
 
288
+ if (dependency && (dependency as UIDLDependency)?.type !== 'local') {
289
+ dependencies[dependency.path] = dependency
290
+ }
291
+
292
+ const elementNode = HASTBuilders.createHTMLNode(elementType)
293
+
138
294
  if (children) {
139
295
  for (const child of children) {
140
- const childTag = await generateHtmlSynatx(
296
+ const childTag = await generateHtmlSyntax(
141
297
  child,
142
- templatesLookUp,
298
+ compName,
299
+ nodesLookup,
143
300
  propDefinitions,
144
301
  stateDefinitions,
145
302
  subComponentOptions,
146
- routeDefinitions,
147
303
  structure
148
304
  )
149
305
 
150
- if (!childTag) {
151
- return
152
- }
153
-
154
306
  if (typeof childTag === 'string') {
155
307
  HASTUtils.addTextNode(elementNode, childTag)
156
308
  } else {
@@ -164,7 +316,6 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
164
316
  const refStyle = referencedStyles[styleRef]
165
317
  if (refStyle.content.mapType === 'inlined') {
166
318
  handleStyles(node, refStyle.content.styles, propDefinitions, stateDefinitions)
167
- return
168
319
  }
169
320
  })
170
321
  }
@@ -173,43 +324,66 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
173
324
  handleStyles(node, style, propDefinitions, stateDefinitions)
174
325
  }
175
326
 
176
- if (Object.keys(attrs).length > 0) {
177
- handleAttributes(elementNode, attrs, propDefinitions, stateDefinitions, routeDefinitions)
178
- }
327
+ handleAttributes(
328
+ elementType,
329
+ elementNode,
330
+ attrs,
331
+ propDefinitions,
332
+ stateDefinitions,
333
+ structure.options.projectRouteDefinition,
334
+ structure.outputOptions
335
+ )
179
336
 
337
+ addNodeToLookup(node.content.key, node, elementNode, nodesLookup, [compName])
180
338
  return elementNode
181
339
  }
182
340
 
341
+ const createLookupTable = (
342
+ component: ComponentUIDL,
343
+ nodesLookup: Record<string, HastNode | HastText>
344
+ ): ElementsLookup => {
345
+ const lookup: ElementsLookup = {}
346
+ for (const node of Object.keys(nodesLookup)) {
347
+ lookup[node] = {
348
+ count: 1,
349
+ nextKey: '1',
350
+ }
351
+ }
352
+ createNodesLookup(component, lookup)
353
+ return lookup
354
+ }
355
+
183
356
  const generateComponentContent = async (
184
357
  node: UIDLElementNode,
358
+ compName: string,
359
+ nodesLookup: Record<string, HastNode | HastText>,
185
360
  propDefinitions: Record<string, UIDLPropDefinition>,
186
361
  stateDefinitions: Record<string, UIDLStateDefinition>,
187
362
  subComponentOptions: {
188
363
  externals: Record<string, ComponentUIDL>
189
364
  plugins: ComponentPlugin[]
190
365
  },
191
- routeDefinitions: UIDLRouteDefinitions,
192
366
  structure: {
193
367
  chunks: ChunkDefinition[]
194
368
  dependencies: Record<string, UIDLDependency>
195
369
  options: GeneratorOptions
370
+ outputOptions: UIDLComponentOutputOptions
196
371
  }
197
372
  ) => {
198
373
  const { externals, plugins } = subComponentOptions
199
- const { elementType, attrs = {}, key, children = [] } = node.content
374
+ const { elementType, attrs = {}, children = [] } = node.content
200
375
  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)}`)
376
+ // "Component" will not exist when generating a component because the resolver checks for illegal class names
377
+ const componentName = elementType === 'Component' ? 'AppComponent' : elementType
378
+ const component = externals[componentName]
379
+ if (component === undefined) {
380
+ throw new HTMLComponentGeneratorError(`${componentName} is missing from externals object`)
208
381
  }
209
382
 
383
+ const componentClone = UIDLUtils.cloneObject<ComponentUIDL>(component)
384
+
210
385
  if (children.length) {
211
- compHasSlots = true
212
- UIDLUtils.traverseNodes(comp.node, (childNode, parentNode) => {
386
+ UIDLUtils.traverseNodes(componentClone.node, (childNode, parentNode) => {
213
387
  if (childNode.type === 'slot' && parentNode.type === 'element') {
214
388
  const nonSlotNodes = parentNode.content?.children?.filter((n) => n.type !== 'slot')
215
389
  parentNode.content.children = [
@@ -219,6 +393,7 @@ const generateComponentContent = async (
219
393
  content: {
220
394
  key: 'custom-slot',
221
395
  elementType: 'slot',
396
+ name: componentClone.name + 'slot',
222
397
  style: {
223
398
  display: {
224
399
  type: 'static',
@@ -238,31 +413,49 @@ const generateComponentContent = async (
238
413
  node.content.children = []
239
414
  }
240
415
 
241
- const combinedProps = { ...propDefinitions, ...(comp?.propDefinitions || {}) }
242
-
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,
416
+ // In UIDL, we define only the link between a component and a page.
417
+ // We define this link using the UIDLLocalDependency approach.
418
+ // So, during the page resolution step, where we ideally generate the unique keys for the components.
419
+ // We can't generate the unique keys for the components because we don't have the full UIDL of the component.
420
+ // When we are using components in a page, the `addExternalComponents` step of the
421
+ // html-component-generator will add the full UIDL of the component to the externals object after resolving them.
422
+ // But when a component is used multiple number of times, we are basically using the same nodes again and again.
423
+ // Which indivates duplication. So, we create a lookup table of all the nodes present with us in the page
424
+ // And then pass it to the component to avoid any coilissions.
425
+ const lookupTableForCurrentPage = createLookupTable(componentClone, nodesLookup)
426
+ generateUniqueKeys(componentClone, lookupTableForCurrentPage)
427
+
428
+ // We are combining props of the current component
429
+ // with props of the component that we need to generate.
430
+ // Refer to line 309, for element props. We either pick from the attr of the current instance of component
431
+ // or from the propDefinitions of the component that we are generating.
432
+ // We don't need to keep passing the props of the current component to the child component and so on
433
+ // for the case of element nodes in attributes or propDefinitions.
434
+ const combinedProps: Record<string, UIDLPropDefinition> = {
435
+ ...Object.keys(propDefinitions).reduce<Record<string, UIDLPropDefinition>>(
436
+ (acc: Record<string, UIDLPropDefinition>, propKey) => {
437
+ if (propDefinitions[propKey]?.type === 'element') {
438
+ return acc
249
439
  }
250
- } else {
251
- acc[propKey] = combinedProps[propKey]
252
- }
253
-
254
- return acc
255
- },
256
- {}
257
- )
258
-
259
- const combinedStates = { ...stateDefinitions, ...(comp?.stateDefinitions || {}) }
440
+ acc[propKey] = propDefinitions[propKey]
441
+ return acc
442
+ },
443
+ {}
444
+ ),
445
+ ...(componentClone?.propDefinitions || {}),
446
+ }
447
+ const combinedStates = { ...stateDefinitions, ...(componentClone?.stateDefinitions || {}) }
260
448
  const statesForInstance = Object.keys(combinedStates).reduce(
261
449
  (acc: Record<string, UIDLStateDefinition>, propKey) => {
262
- if (attrs[propKey]) {
450
+ const attr = attrs[propKey]
451
+ if (attr.type === 'object') {
452
+ throw new Error(`Object attributes are not supported in html exports`)
453
+ }
454
+
455
+ if (attr) {
263
456
  acc[propKey] = {
264
457
  ...combinedStates[propKey],
265
- defaultValue: attrs[propKey]?.content || combinedStates[propKey]?.defaultValue,
458
+ defaultValue: attr?.content || combinedStates[propKey]?.defaultValue,
266
459
  }
267
460
  } else {
268
461
  acc[propKey] = combinedStates[propKey]
@@ -273,43 +466,127 @@ const generateComponentContent = async (
273
466
  {}
274
467
  )
275
468
 
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
- },
469
+ const propsForInstance: Record<string, UIDLPropDefinition> = {}
470
+ // this is where we check if the component we are conusming is actually passing any props to the instance.
471
+ // 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
472
+ // the component instance that we are using here.
473
+ for (const propKey of Object.keys(combinedProps)) {
474
+ const attribute = attrs[propKey]
475
+
476
+ if (attribute?.type === 'element') {
477
+ propsForInstance[propKey] = {
478
+ ...combinedProps[propKey],
479
+ defaultValue: attrs[propKey],
480
+ }
481
+ await generateHtmlSyntax(
482
+ attrs[propKey] as UIDLElementNode,
483
+ component.name,
484
+ nodesLookup,
485
+ propDefinitions,
486
+ stateDefinitions,
487
+ subComponentOptions,
488
+ structure
489
+ )
490
+ }
491
+
492
+ if (attribute?.type === 'dynamic') {
493
+ // When we are using a component instance in a component and the attribute
494
+ // that is passed to the component is of dynamic reference.
495
+ // If means, the component is redirecting the prop that is received to the prop of the component that it is consuming.
496
+ // 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.
497
+ // And similary we do the same for the states.
498
+ switch (attribute.content.referenceType) {
499
+ case 'prop':
500
+ propsForInstance[propKey] = combinedProps[propKey]
501
+ break
502
+ case 'state':
503
+ propsForInstance[propKey] = combinedStates[propKey]
504
+ break
505
+ default:
506
+ throw new Error(
507
+ `ReferenceType ${attribute.content.referenceType} is not supported in HTML Export.`
508
+ )
509
+ }
510
+ }
511
+
512
+ if (attribute?.type === 'object') {
513
+ propsForInstance[propKey] = {
514
+ ...combinedProps[propKey],
515
+ defaultValue: (attribute?.content as object) || combinedProps[propKey]?.defaultValue,
516
+ }
517
+ }
518
+
519
+ if (
520
+ attribute?.type !== 'dynamic' &&
521
+ attribute?.type !== 'element' &&
522
+ attribute?.type !== 'object'
523
+ ) {
524
+ propsForInstance[propKey] = {
525
+ ...combinedProps[propKey],
526
+ defaultValue: attribute?.content || combinedProps[propKey]?.defaultValue,
527
+ }
528
+ }
529
+
530
+ if (attribute === undefined) {
531
+ const propFromCurrentComponent = combinedProps[propKey]
532
+ if (propFromCurrentComponent.type === 'element' && propFromCurrentComponent.defaultValue) {
533
+ await generateHtmlSyntax(
534
+ propFromCurrentComponent.defaultValue as UIDLElementNode,
535
+ component.name,
536
+ nodesLookup,
537
+ propDefinitions,
538
+ stateDefinitions,
539
+ subComponentOptions,
540
+ structure
541
+ )
542
+ }
543
+ propsForInstance[propKey] = propFromCurrentComponent
544
+ }
545
+ }
546
+
547
+ let componentWrapper = StringUtils.camelCaseToDashCase(`${componentName}-wrapper`)
548
+ const isExistingNode = nodesLookup[componentWrapper]
549
+ if (isExistingNode !== undefined) {
550
+ componentWrapper = `${componentWrapper}-${StringUtils.generateRandomString()}`
551
+ }
552
+
553
+ const componentInstanceToGenerate: UIDLElementNode = {
554
+ type: 'element',
555
+ content: {
556
+ elementType: componentWrapper,
557
+ key: componentWrapper,
558
+ children: [componentClone.node],
559
+ style: {
560
+ display: {
561
+ type: 'static',
562
+ content: 'contents',
290
563
  },
291
564
  },
292
565
  },
293
- lookUpTemplates,
566
+ }
567
+
568
+ const compTag = await generateHtmlSyntax(
569
+ componentInstanceToGenerate,
570
+ component.name,
571
+ nodesLookup,
294
572
  propsForInstance,
295
573
  statesForInstance,
296
574
  subComponentOptions,
297
- routeDefinitions,
298
575
  structure
299
- )) as unknown as HastNode
576
+ )
300
577
 
301
578
  const cssPlugin = createCSSPlugin({
302
579
  templateStyle: 'html',
303
580
  templateChunkName: DEFAULT_COMPONENT_CHUNK_NAME,
304
581
  declareDependency: 'import',
305
- forceScoping: true,
306
- chunkName: comp.name,
582
+ chunkName: componentClone.name,
307
583
  staticPropReferences: true,
308
584
  })
309
585
 
310
586
  const initialStructure: ComponentStructure = {
311
587
  uidl: {
312
- ...comp,
588
+ ...componentClone,
589
+ node: componentInstanceToGenerate,
313
590
  propDefinitions: propsForInstance,
314
591
  stateDefinitions: statesForInstance,
315
592
  },
@@ -321,7 +598,7 @@ const generateComponentContent = async (
321
598
  linkAfter: [],
322
599
  content: compTag,
323
600
  meta: {
324
- nodesLookup: lookUpTemplates,
601
+ nodesLookup,
325
602
  },
326
603
  },
327
604
  ],
@@ -337,41 +614,81 @@ const generateComponentContent = async (
337
614
  Promise.resolve(initialStructure)
338
615
  )
339
616
 
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)
617
+ result.chunks.forEach((chunk) => {
618
+ if (chunk.fileType === FileType.CSS) {
619
+ chunks.push(chunk)
356
620
  }
357
- }
621
+ })
358
622
 
623
+ addNodeToLookup(node.content.key, node, compTag, nodesLookup, [compName, component.name])
359
624
  return compTag
360
625
  }
361
626
 
362
- const generateDynamicNode: NodeToHTML<UIDLDynamicReference, HastNode> = (
627
+ const generateDynamicNode: NodeToHTML<UIDLDynamicReference, Promise<HastNode | HastText>> = async (
363
628
  node,
364
- _,
629
+ compName,
630
+ nodesLookup,
365
631
  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)
632
+ stateDefinitions,
633
+ subComponentOptions,
634
+ structure
635
+ ): Promise<HastNode | HastText> => {
636
+ if (node.content.referenceType === 'locale') {
637
+ const localeTag = HASTBuilders.createHTMLNode('span')
638
+ const commentNode = HASTBuilders.createComment(`Content for locale ${node.content.id}`)
639
+ HASTUtils.addChildNode(localeTag, commentNode)
640
+ return localeTag
641
+ }
373
642
 
374
- HASTUtils.addTextNode(spanTag, String(usedReferenceValue))
643
+ const usedReferenceValue = getValueFromReference(
644
+ node.content.id,
645
+ node.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
646
+ )
647
+
648
+ if (
649
+ (usedReferenceValue.type === 'object' || usedReferenceValue.type === 'array') &&
650
+ usedReferenceValue.defaultValue
651
+ ) {
652
+ // Let's say users are biding the prop to a node using something like this "fields.Title"
653
+ // But the fields in the object is the value where the object is defined either in propDefinitions
654
+ // or on the attrs. So, we just need to parsed the rest of the object path and get the value from the object.
655
+ return HASTBuilders.createTextNode(
656
+ String(
657
+ extractDefaultValueFromRefPath(
658
+ usedReferenceValue.defaultValue as Record<string, UIDLPropDefinition>,
659
+ node.content.refPath
660
+ )
661
+ )
662
+ )
663
+ }
664
+
665
+ if (usedReferenceValue.type === 'element') {
666
+ const elementNode = usedReferenceValue.defaultValue as UIDLElementNode
667
+ if (elementNode) {
668
+ if (elementNode.content.key in nodesLookup) {
669
+ return nodesLookup[elementNode.content.key]
670
+ } else {
671
+ const elementTag = await generateHtmlSyntax(
672
+ elementNode,
673
+ compName,
674
+ nodesLookup,
675
+ propDefinitions,
676
+ stateDefinitions,
677
+ subComponentOptions,
678
+ structure
679
+ )
680
+ return elementTag
681
+ }
682
+ }
683
+
684
+ const spanTagWrapper = HASTBuilders.createHTMLNode('span')
685
+ const commentNode = HASTBuilders.createComment(`Content for slot ${node.content.id}`)
686
+ HASTUtils.addChildNode(spanTagWrapper, commentNode)
687
+ return spanTagWrapper
688
+ }
689
+
690
+ const spanTag = HASTBuilders.createHTMLNode('span')
691
+ HASTUtils.addTextNode(spanTag, String(usedReferenceValue.defaultValue))
375
692
  return spanTag
376
693
  }
377
694
 
@@ -384,10 +701,14 @@ const handleStyles = (
384
701
  Object.keys(styles).forEach((styleKey) => {
385
702
  let style: string | UIDLStyleValue = styles[styleKey]
386
703
  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)
704
+ const referencedValue = getValueFromReference(
705
+ style.content.id,
706
+ style.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
707
+ )
708
+ if (referencedValue.type === 'string' || referencedValue.type === 'number') {
709
+ style = String(
710
+ extractDefaultValueFromRefPath(referencedValue.defaultValue, style?.content?.refPath)
711
+ )
391
712
  }
392
713
  node.content.style[styleKey] = typeof style === 'string' ? staticNode(style) : style
393
714
  }
@@ -395,62 +716,125 @@ const handleStyles = (
395
716
  }
396
717
 
397
718
  const handleAttributes = (
719
+ elementType: UIDLElement['elementType'],
398
720
  htmlNode: HastNode,
399
721
  attrs: Record<string, UIDLAttributeValue>,
400
722
  propDefinitions: Record<string, UIDLPropDefinition>,
401
723
  stateDefinitions: Record<string, UIDLStateDefinition>,
402
- routeDefinitions: UIDLRouteDefinitions
724
+ routeDefinitions: UIDLRouteDefinitions,
725
+ outputOptions: UIDLComponentOutputOptions
403
726
  ) => {
404
- Object.keys(attrs).forEach((attrKey) => {
405
- let attrValue = attrs[attrKey]
727
+ for (const attrKey of Object.keys(attrs)) {
728
+ const attrValue = attrs[attrKey]
729
+ const { type, content } = attrValue
730
+
731
+ switch (type) {
732
+ case 'static': {
733
+ if (attrKey === 'href' && typeof content === 'string' && content.startsWith('/')) {
734
+ let targetLink
735
+
736
+ const targetRoute = (routeDefinitions?.values || []).find(
737
+ (route) => route.pageOptions.navLink === content
738
+ )
739
+
740
+ if (targetRoute) {
741
+ targetLink = targetRoute.pageOptions.navLink
742
+ }
743
+
744
+ if (!targetRoute && content === '/home') {
745
+ targetLink = '/'
746
+ }
747
+
748
+ if (!targetLink && !targetRoute) {
749
+ targetLink = content
750
+ }
751
+
752
+ const currentPageRoute = join(...(outputOptions?.folderPath || []), './')
753
+ const localPrefix = relative(
754
+ `/${currentPageRoute}`,
755
+ `/${targetLink === '/' ? 'index' : targetLink}`
756
+ )
757
+
758
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, `${localPrefix}.html`)
759
+ break
760
+ }
406
761
 
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
- }
762
+ if (typeof content === 'boolean') {
763
+ htmlNode.properties[attrKey] = content === true ? 'true' : 'false'
764
+ } else if (typeof content === 'string' || typeof attrValue.content === 'number') {
765
+ let value = StringUtils.encode(String(attrValue.content))
766
+
767
+ /*
768
+ elementType of image is always mapped to img.
769
+ For reference, check `html-mapping` file.
770
+ */
771
+ if (elementType === 'img' && attrKey === 'src' && !isValidURL(value)) {
772
+ /*
773
+ By default we just prefix all the asset paths with just the
774
+ assetPrefix that is configured in the project. But for `html` generators
775
+ we need to prefix that with the current file location.
776
+
777
+ Because, all the other frameworks have a build setup. which serves all the
778
+ assets from the `public` folder. But in the case of `html` here is how it works
779
+
780
+ We load a file from `index.html` the request for the image goes from
781
+ '...url.../public/...image...'
782
+ If it's a nested url, then the request goes from
783
+ '...url/nested/public/...image..'
784
+
785
+ But the nested folder is available only on the root. With this
786
+ The url changes prefixes to
787
+
788
+ ../public/playground_assets/..image.. etc depending on the dept the file is in.
789
+ */
790
+ value = join(relative(join(...outputOptions.folderPath), './'), value)
791
+ }
792
+
793
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
794
+ }
424
795
 
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
- }
796
+ break
797
+ }
433
798
 
434
- if (attrValue.type === 'raw') {
435
- HASTUtils.addAttributeToNode(htmlNode, attrKey, attrValue.content)
436
- return
437
- }
799
+ case 'dynamic': {
800
+ const value = getValueFromReference(
801
+ content.id,
802
+ content.referenceType === 'prop' ? propDefinitions : stateDefinitions
803
+ )
804
+
805
+ HASTUtils.addAttributeToNode(
806
+ htmlNode,
807
+ attrKey,
808
+ String(extractDefaultValueFromRefPath(value.defaultValue, content.refPath))
809
+ )
810
+ break
811
+ }
812
+
813
+ case 'raw': {
814
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, content)
815
+ break
816
+ }
817
+
818
+ case 'element':
819
+ case 'import':
820
+ case 'expr':
821
+ case 'object':
822
+ break
438
823
 
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
824
+ default: {
825
+ throw new HTMLComponentGeneratorError(
826
+ `Received ${JSON.stringify(attrValue, null, 2)} \n in handleAttributes for html`
827
+ )
828
+ }
445
829
  }
446
- })
830
+ }
447
831
  }
448
832
 
449
833
  const getValueFromReference = (
450
834
  key: string,
451
835
  definitions: Record<string, UIDLPropDefinition>
452
- ): string => {
453
- const usedReferenceValue = definitions[key.includes('.') ? key.split('.')[0] : key]
836
+ ): UIDLPropDefinition | undefined => {
837
+ const usedReferenceValue = definitions[key.includes('?.') ? key.split('?.')[0] : key]
454
838
 
455
839
  if (!usedReferenceValue) {
456
840
  throw new HTMLComponentGeneratorError(
@@ -458,9 +842,11 @@ const getValueFromReference = (
458
842
  )
459
843
  }
460
844
 
461
- if (!usedReferenceValue.hasOwnProperty('defaultValue')) {
845
+ if (
846
+ ['string', 'number', 'object', 'element', 'array'].includes(usedReferenceValue?.type) === false
847
+ ) {
462
848
  throw new HTMLComponentGeneratorError(
463
- `Default value is missing from dynamic reference - ${JSON.stringify(
849
+ `Attribute is using dynamic value, but received of type ${JSON.stringify(
464
850
  usedReferenceValue,
465
851
  null,
466
852
  2
@@ -468,9 +854,12 @@ const getValueFromReference = (
468
854
  )
469
855
  }
470
856
 
471
- if (!['string', 'number', 'object'].includes(usedReferenceValue?.type)) {
857
+ if (
858
+ usedReferenceValue.type !== 'element' &&
859
+ usedReferenceValue.hasOwnProperty('defaultValue') === false
860
+ ) {
472
861
  throw new HTMLComponentGeneratorError(
473
- `Attribute is using dynamic value, but received of type ${JSON.stringify(
862
+ `Default value is missing from dynamic reference - ${JSON.stringify(
474
863
  usedReferenceValue,
475
864
  null,
476
865
  2
@@ -478,5 +867,16 @@ const getValueFromReference = (
478
867
  )
479
868
  }
480
869
 
481
- return String(usedReferenceValue.defaultValue)
870
+ return usedReferenceValue
871
+ }
872
+
873
+ const extractDefaultValueFromRefPath = (
874
+ propDefaultValue: PropDefaultValueTypes,
875
+ refPath?: string[]
876
+ ) => {
877
+ if (typeof propDefaultValue !== 'object' || !refPath?.length) {
878
+ return propDefaultValue
879
+ }
880
+
881
+ return GenericUtils.getValueFromPath(refPath.join('.'), propDefaultValue) as PropDefaultValueTypes
482
882
  }