@teleporthq/teleport-plugin-html-base-component 0.36.0-alpha.0 → 0.36.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.
@@ -19,37 +19,76 @@ import {
19
19
  UIDLRouteDefinitions,
20
20
  ComponentPlugin,
21
21
  ComponentStructure,
22
+ UIDLComponentOutputOptions,
23
+ UIDLElement,
24
+ ElementsLookup,
22
25
  } from '@teleporthq/teleport-types'
26
+ import { join, relative } from 'path'
23
27
  import { HASTBuilders, HASTUtils } from '@teleporthq/teleport-plugin-common'
24
28
  import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
25
29
  import { staticNode } from '@teleporthq/teleport-uidl-builders'
26
30
  import { createCSSPlugin } from '@teleporthq/teleport-plugin-css'
31
+ import { generateUniqueKeys, createNodesLookup } from '@teleporthq/teleport-uidl-resolver'
27
32
  import { DEFAULT_COMPONENT_CHUNK_NAME } from './constants'
28
33
 
34
+ const isValidURL = (url: string) => {
35
+ try {
36
+ /* tslint:disable:no-unused-expression */
37
+ new URL(url)
38
+ return true
39
+ } catch (error) {
40
+ return false
41
+ }
42
+ }
43
+
44
+ const addNodeToLookup = (
45
+ node: UIDLElementNode,
46
+ tag: HastNode | HastText,
47
+ nodesLoookup: Record<string, HastNode | HastText>,
48
+ hierarchy: string[] = []
49
+ ) => {
50
+ // In html code-generation we combine the nodes of the component that is being consumed with the current component.
51
+ // As html can't load the component at runtime like react or any other frameworks. So, we merge the component as a standalone
52
+ // component in the current component.
53
+ if (nodesLoookup[node.content.key]) {
54
+ throw new HTMLComponentGeneratorError(
55
+ `\n${hierarchy.join(' -> ')} \n
56
+ Duplicate key found in nodesLookup: ${node.content.key} \n
57
+
58
+ A node with the same key already exists\n
59
+ Received \n\n ${JSON.stringify(tag)}\n ${JSON.stringify(node)}
60
+ Existing \n\n ${JSON.stringify(nodesLoookup[node.content.key])} \n\n`
61
+ )
62
+ }
63
+
64
+ nodesLoookup[node.content.key] = tag
65
+ }
66
+
29
67
  type NodeToHTML<NodeType, ReturnType> = (
30
68
  node: NodeType,
31
- templatesLookUp: Record<string, unknown>,
69
+ componentName: string,
70
+ nodesLookup: Record<string, HastNode | HastText>,
32
71
  propDefinitions: Record<string, UIDLPropDefinition>,
33
72
  stateDefinitions: Record<string, UIDLStateDefinition>,
34
73
  subComponentOptions: {
35
74
  externals: Record<string, ComponentUIDL>
36
75
  plugins: ComponentPlugin[]
37
76
  },
38
- routeDefinitions: UIDLRouteDefinitions,
39
77
  structure: {
40
78
  chunks: ChunkDefinition[]
41
79
  dependencies: Record<string, UIDLDependency>
42
80
  options: GeneratorOptions
81
+ outputOptions: UIDLComponentOutputOptions
43
82
  }
44
83
  ) => ReturnType
45
84
 
46
- export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastText>> = async (
85
+ export const generateHtmlSyntax: NodeToHTML<UIDLNode, Promise<HastNode | HastText>> = async (
47
86
  node,
48
- templatesLookUp,
87
+ compName,
88
+ nodesLookup,
49
89
  propDefinitions,
50
90
  stateDefinitions,
51
91
  subComponentOptions,
52
- routeDefinitions,
53
92
  structure
54
93
  ) => {
55
94
  switch (node.type) {
@@ -64,26 +103,28 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
64
103
  return HASTBuilders.createHTMLNode(node.type)
65
104
 
66
105
  case 'element':
67
- return generatElementNode(
106
+ const elementNode = await generateElementNode(
68
107
  node,
69
- templatesLookUp,
108
+ compName,
109
+ nodesLookup,
70
110
  propDefinitions,
71
111
  stateDefinitions,
72
112
  subComponentOptions,
73
- routeDefinitions,
74
113
  structure
75
114
  )
115
+ return elementNode
76
116
 
77
117
  case 'dynamic':
78
- return generateDynamicNode(
118
+ const dynamicNode = await generateDynamicNode(
79
119
  node,
80
- templatesLookUp,
120
+ compName,
121
+ nodesLookup,
81
122
  propDefinitions,
82
123
  stateDefinitions,
83
124
  subComponentOptions,
84
- routeDefinitions,
85
125
  structure
86
126
  )
127
+ return dynamicNode
87
128
 
88
129
  default:
89
130
  throw new HTMLComponentGeneratorError(
@@ -96,13 +137,13 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
96
137
  }
97
138
  }
98
139
 
99
- const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastText>> = async (
140
+ const generateElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastText>> = async (
100
141
  node,
101
- templatesLookUp,
142
+ compName,
143
+ nodesLookup,
102
144
  propDefinitions,
103
145
  stateDefinitions,
104
146
  subComponentOptions,
105
- routeDefinitions,
106
147
  structure
107
148
  ) => {
108
149
  const {
@@ -112,45 +153,48 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
112
153
  style = {},
113
154
  referencedStyles = {},
114
155
  dependency,
115
- key,
116
156
  } = node.content
117
-
118
- const elementNode = HASTBuilders.createHTMLNode(elementType)
119
- templatesLookUp[key] = elementNode
120
-
121
157
  const { dependencies } = structure
122
158
  if (dependency && (dependency as UIDLDependency)?.type !== 'local') {
123
159
  dependencies[dependency.path] = dependency
124
160
  }
125
161
 
126
162
  if (dependency && (dependency as UIDLDependency)?.type === 'local') {
163
+ if (nodesLookup[node.content.key]) {
164
+ return nodesLookup[node.content.key]
165
+ }
166
+
127
167
  const compTag = await generateComponentContent(
128
168
  node,
169
+ compName,
170
+ nodesLookup,
129
171
  propDefinitions,
130
172
  stateDefinitions,
131
173
  subComponentOptions,
132
- routeDefinitions,
133
174
  structure
134
175
  )
176
+
177
+ if ('tagName' in compTag) {
178
+ compTag.children.unshift(HASTBuilders.createComment(`${node.content.semanticType} component`))
179
+ }
180
+
135
181
  return compTag
136
182
  }
137
183
 
184
+ const elementNode = HASTBuilders.createHTMLNode(elementType)
185
+
138
186
  if (children) {
139
187
  for (const child of children) {
140
- const childTag = await generateHtmlSynatx(
188
+ const childTag = await generateHtmlSyntax(
141
189
  child,
142
- templatesLookUp,
190
+ compName,
191
+ nodesLookup,
143
192
  propDefinitions,
144
193
  stateDefinitions,
145
194
  subComponentOptions,
146
- routeDefinitions,
147
195
  structure
148
196
  )
149
197
 
150
- if (!childTag) {
151
- return
152
- }
153
-
154
198
  if (typeof childTag === 'string') {
155
199
  HASTUtils.addTextNode(elementNode, childTag)
156
200
  } else {
@@ -164,7 +208,6 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
164
208
  const refStyle = referencedStyles[styleRef]
165
209
  if (refStyle.content.mapType === 'inlined') {
166
210
  handleStyles(node, refStyle.content.styles, propDefinitions, stateDefinitions)
167
- return
168
211
  }
169
212
  })
170
213
  }
@@ -173,43 +216,68 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
173
216
  handleStyles(node, style, propDefinitions, stateDefinitions)
174
217
  }
175
218
 
176
- if (Object.keys(attrs).length > 0) {
177
- handleAttributes(elementNode, attrs, propDefinitions, stateDefinitions, routeDefinitions)
178
- }
219
+ handleAttributes(
220
+ elementType,
221
+ elementNode,
222
+ attrs,
223
+ propDefinitions,
224
+ stateDefinitions,
225
+ structure.options.projectRouteDefinition,
226
+ structure.outputOptions
227
+ )
179
228
 
229
+ addNodeToLookup(node, elementNode, nodesLookup, [compName])
180
230
  return elementNode
181
231
  }
182
232
 
233
+ const createLookupTable = (
234
+ component: ComponentUIDL,
235
+ nodesLookup: Record<string, HastNode | HastText>
236
+ ): ElementsLookup => {
237
+ const lookup: ElementsLookup = {}
238
+ for (const node of Object.keys(nodesLookup)) {
239
+ lookup[node] = {
240
+ count: 1,
241
+ nextKey: '1',
242
+ }
243
+ }
244
+ createNodesLookup(component, lookup)
245
+ return lookup
246
+ }
247
+
183
248
  const generateComponentContent = async (
184
249
  node: UIDLElementNode,
250
+ compName: string,
251
+ nodesLookup: Record<string, HastNode | HastText>,
185
252
  propDefinitions: Record<string, UIDLPropDefinition>,
186
253
  stateDefinitions: Record<string, UIDLStateDefinition>,
187
254
  subComponentOptions: {
188
255
  externals: Record<string, ComponentUIDL>
189
256
  plugins: ComponentPlugin[]
190
257
  },
191
- routeDefinitions: UIDLRouteDefinitions,
192
258
  structure: {
193
259
  chunks: ChunkDefinition[]
194
260
  dependencies: Record<string, UIDLDependency>
195
261
  options: GeneratorOptions
262
+ outputOptions: UIDLComponentOutputOptions
196
263
  }
197
264
  ) => {
198
265
  const { externals, plugins } = subComponentOptions
199
- const { elementType, attrs = {}, key, children = [] } = node.content
266
+ const { elementType, attrs = {}, children = [] } = node.content
200
267
  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)}`)
268
+ // "Component" will not exist when generating a component because the resolver checks for illegal class names
269
+ const componentName = elementType === 'Component' ? 'AppComponent' : elementType
270
+ const component = externals[componentName]
271
+ if (component === undefined) {
272
+ throw new HTMLComponentGeneratorError(`${componentName} is missing from externals object`)
208
273
  }
209
274
 
275
+ const componentClone = UIDLUtils.cloneObject<ComponentUIDL>(component)
276
+
277
+ let compHasSlots: boolean = false
210
278
  if (children.length) {
211
279
  compHasSlots = true
212
- UIDLUtils.traverseNodes(comp.node, (childNode, parentNode) => {
280
+ UIDLUtils.traverseNodes(componentClone.node, (childNode, parentNode) => {
213
281
  if (childNode.type === 'slot' && parentNode.type === 'element') {
214
282
  const nonSlotNodes = parentNode.content?.children?.filter((n) => n.type !== 'slot')
215
283
  parentNode.content.children = [
@@ -219,6 +287,7 @@ const generateComponentContent = async (
219
287
  content: {
220
288
  key: 'custom-slot',
221
289
  elementType: 'slot',
290
+ name: componentClone.name + 'slot',
222
291
  style: {
223
292
  display: {
224
293
  type: 'static',
@@ -238,25 +307,38 @@ const generateComponentContent = async (
238
307
  node.content.children = []
239
308
  }
240
309
 
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,
310
+ // In UIDL, we define only the link between a component and a page.
311
+ // We define this link using the UIDLLocalDependency approach.
312
+ // So, during the page resolution step, where we ideally generate the unique keys for the components.
313
+ // We can't generate the unique keys for the components because we don't have the full UIDL of the component.
314
+ // When we are using components in a page, the `addExternalComponents` step of the
315
+ // html-component-generator will add the full UIDL of the component to the externals object after resolving them.
316
+ // But when a component is used multiple number of times, we are basically using the same nodes again and again.
317
+ // Which indivates duplication. So, we create a lookup table of all the nodes present with us in the page
318
+ // And then pass it to the component to avoid any coilissions.
319
+ const lookupTableForCurrentPage = createLookupTable(componentClone, nodesLookup)
320
+ generateUniqueKeys(componentClone, lookupTableForCurrentPage)
321
+
322
+ // We are combining props of the current component
323
+ // with props of the component that we need to generate.
324
+ // Refer to line 309, for element props. We either pick from the attr of the current instance of component
325
+ // or from the propDefinitions of the component that we are generating.
326
+ // We don't need to keep passing the props of the current component to the child component and so on
327
+ // for the case of element nodes in attributes or propDefinitions.
328
+ const combinedProps: Record<string, UIDLPropDefinition> = {
329
+ ...Object.keys(propDefinitions).reduce<Record<string, UIDLPropDefinition>>(
330
+ (acc: Record<string, UIDLPropDefinition>, propKey) => {
331
+ if (propDefinitions[propKey]?.type === 'element') {
332
+ return acc
249
333
  }
250
- } else {
251
- acc[propKey] = combinedProps[propKey]
252
- }
253
-
254
- return acc
255
- },
256
- {}
257
- )
258
-
259
- const combinedStates = { ...stateDefinitions, ...(comp?.stateDefinitions || {}) }
334
+ acc[propKey] = propDefinitions[propKey]
335
+ return acc
336
+ },
337
+ {}
338
+ ),
339
+ ...(componentClone?.propDefinitions || {}),
340
+ }
341
+ const combinedStates = { ...stateDefinitions, ...(componentClone?.stateDefinitions || {}) }
260
342
  const statesForInstance = Object.keys(combinedStates).reduce(
261
343
  (acc: Record<string, UIDLStateDefinition>, propKey) => {
262
344
  if (attrs[propKey]) {
@@ -273,43 +355,109 @@ const generateComponentContent = async (
273
355
  {}
274
356
  )
275
357
 
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
- },
358
+ const propsForInstance: Record<string, UIDLPropDefinition> = {}
359
+ // this is where we check if the component we are conusming is actually passing any props to the instance.
360
+ // 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
361
+ // the component instance that we are using here.
362
+ for (const propKey of Object.keys(combinedProps)) {
363
+ const prop = attrs[propKey]
364
+ if (prop?.type === 'element') {
365
+ propsForInstance[propKey] = {
366
+ ...combinedProps[propKey],
367
+ defaultValue: attrs[propKey],
368
+ }
369
+ await generateHtmlSyntax(
370
+ attrs[propKey] as UIDLElementNode,
371
+ component.name,
372
+ nodesLookup,
373
+ propDefinitions,
374
+ stateDefinitions,
375
+ subComponentOptions,
376
+ structure
377
+ )
378
+ } else if (prop?.type === 'dynamic') {
379
+ // When we are using a component instance in a component and the attribute
380
+ // that is passed to the component is of dynamic reference.
381
+ // If means, the component is redirecting the prop that is received to the prop of the component that it is consuming.
382
+ // 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.
383
+ // And similary we do the same for the states.
384
+ switch (prop.content.referenceType) {
385
+ case 'prop':
386
+ propsForInstance[propKey] = combinedProps[propKey]
387
+ break
388
+ case 'state':
389
+ propsForInstance[propKey] = combinedStates[propKey]
390
+ break
391
+ default:
392
+ throw new Error(
393
+ `ReferenceType ${prop.content.referenceType} is not supported in HTML Export.`
394
+ )
395
+ }
396
+ } else if (prop) {
397
+ propsForInstance[propKey] = {
398
+ ...combinedProps[propKey],
399
+ defaultValue: attrs[propKey]?.content || combinedProps[propKey]?.defaultValue,
400
+ }
401
+ } else {
402
+ const propFromCurrentComponent = combinedProps[propKey]
403
+ if (propFromCurrentComponent.type === 'element' && propFromCurrentComponent.defaultValue) {
404
+ await generateHtmlSyntax(
405
+ propFromCurrentComponent.defaultValue as UIDLElementNode,
406
+ component.name,
407
+ nodesLookup,
408
+ propDefinitions,
409
+ stateDefinitions,
410
+ subComponentOptions,
411
+ structure
412
+ )
413
+ }
414
+ propsForInstance[propKey] = propFromCurrentComponent
415
+ }
416
+ }
417
+
418
+ let componentWrapper = StringUtils.camelCaseToDashCase(`${componentName}-wrapper`)
419
+ const isExistingNode = nodesLookup[componentWrapper]
420
+ if (isExistingNode !== undefined) {
421
+ componentWrapper = `${componentWrapper}-${StringUtils.generateRandomString()}`
422
+ }
423
+
424
+ const componentInstanceToGenerate: UIDLElementNode = {
425
+ type: 'element',
426
+ content: {
427
+ elementType: componentWrapper,
428
+ key: componentWrapper,
429
+ children: [componentClone.node],
430
+ style: {
431
+ display: {
432
+ type: 'static',
433
+ content: 'contents',
290
434
  },
291
435
  },
292
436
  },
293
- lookUpTemplates,
437
+ }
438
+
439
+ const compTag = await generateHtmlSyntax(
440
+ componentInstanceToGenerate,
441
+ component.name,
442
+ nodesLookup,
294
443
  propsForInstance,
295
444
  statesForInstance,
296
445
  subComponentOptions,
297
- routeDefinitions,
298
446
  structure
299
- )) as unknown as HastNode
447
+ )
300
448
 
301
449
  const cssPlugin = createCSSPlugin({
302
450
  templateStyle: 'html',
303
451
  templateChunkName: DEFAULT_COMPONENT_CHUNK_NAME,
304
452
  declareDependency: 'import',
305
- forceScoping: true,
306
- chunkName: comp.name,
453
+ chunkName: componentClone.name,
307
454
  staticPropReferences: true,
308
455
  })
309
456
 
310
457
  const initialStructure: ComponentStructure = {
311
458
  uidl: {
312
- ...comp,
459
+ ...componentClone,
460
+ node: componentInstanceToGenerate,
313
461
  propDefinitions: propsForInstance,
314
462
  stateDefinitions: statesForInstance,
315
463
  },
@@ -321,7 +469,7 @@ const generateComponentContent = async (
321
469
  linkAfter: [],
322
470
  content: compTag,
323
471
  meta: {
324
- nodesLookup: lookUpTemplates,
472
+ nodesLookup,
325
473
  },
326
474
  },
327
475
  ],
@@ -344,7 +492,7 @@ const generateComponentContent = async (
344
492
  }
345
493
  })
346
494
  } else {
347
- const chunk = chunks.find((item) => item.name === comp.name)
495
+ const chunk = chunks.find((item) => item.name === componentClone.name)
348
496
  if (!chunk) {
349
497
  const styleChunk = result.chunks.find(
350
498
  (item: ChunkDefinition) => item.fileType === FileType.CSS
@@ -356,22 +504,45 @@ const generateComponentContent = async (
356
504
  }
357
505
  }
358
506
 
507
+ addNodeToLookup(node, compTag, nodesLookup, [compName, component.name])
359
508
  return compTag
360
509
  }
361
510
 
362
- const generateDynamicNode: NodeToHTML<UIDLDynamicReference, HastNode> = (
511
+ const generateDynamicNode: NodeToHTML<UIDLDynamicReference, Promise<HastNode | HastText>> = async (
363
512
  node,
364
- _,
513
+ /* tslint:disable variable-name */
514
+ _compName,
515
+ nodesLookup,
365
516
  propDefinitions,
366
517
  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)
518
+ ): Promise<HastNode | HastText> => {
519
+ const usedReferenceValue = getValueFromReference(
520
+ node.content.id,
521
+ node.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
522
+ )
523
+
524
+ if (usedReferenceValue.type === 'element' && usedReferenceValue.defaultValue) {
525
+ const elementNode = usedReferenceValue.defaultValue as UIDLElementNode
373
526
 
374
- HASTUtils.addTextNode(spanTag, String(usedReferenceValue))
527
+ if (elementNode.content.key in nodesLookup) {
528
+ return nodesLookup[elementNode.content.key]
529
+ }
530
+
531
+ const spanTagWrapper = HASTBuilders.createHTMLNode('span')
532
+ const commentNode = HASTBuilders.createComment(`Content for slot ${node.content.id}`)
533
+ HASTUtils.addChildNode(spanTagWrapper, commentNode)
534
+ return spanTagWrapper
535
+ }
536
+
537
+ if (usedReferenceValue.type === 'element' && usedReferenceValue.defaultValue === undefined) {
538
+ const spanTagWrapper = HASTBuilders.createHTMLNode('span')
539
+ const commentNode = HASTBuilders.createComment(`Content for slot ${node.content.id}`)
540
+ HASTUtils.addChildNode(spanTagWrapper, commentNode)
541
+ return spanTagWrapper
542
+ }
543
+
544
+ const spanTag = HASTBuilders.createHTMLNode('span')
545
+ HASTUtils.addTextNode(spanTag, String(usedReferenceValue.defaultValue))
375
546
  return spanTag
376
547
  }
377
548
 
@@ -384,10 +555,12 @@ const handleStyles = (
384
555
  Object.keys(styles).forEach((styleKey) => {
385
556
  let style: string | UIDLStyleValue = styles[styleKey]
386
557
  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)
558
+ const referencedValue = getValueFromReference(
559
+ style.content.id,
560
+ style.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
561
+ )
562
+ if (referencedValue.type === 'string' || referencedValue.type === 'number') {
563
+ style = String(referencedValue.defaultValue)
391
564
  }
392
565
  node.content.style[styleKey] = typeof style === 'string' ? staticNode(style) : style
393
566
  }
@@ -395,61 +568,119 @@ const handleStyles = (
395
568
  }
396
569
 
397
570
  const handleAttributes = (
571
+ elementType: UIDLElement['elementType'],
398
572
  htmlNode: HastNode,
399
573
  attrs: Record<string, UIDLAttributeValue>,
400
574
  propDefinitions: Record<string, UIDLPropDefinition>,
401
575
  stateDefinitions: Record<string, UIDLStateDefinition>,
402
- routeDefinitions: UIDLRouteDefinitions
576
+ routeDefinitions: UIDLRouteDefinitions,
577
+ outputOptions: UIDLComponentOutputOptions
403
578
  ) => {
404
- Object.keys(attrs).forEach((attrKey) => {
405
- let attrValue = attrs[attrKey]
406
-
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
- }
579
+ for (const attrKey of Object.keys(attrs)) {
580
+ const attrValue = attrs[attrKey]
581
+ const { type, content } = attrValue
582
+
583
+ switch (type) {
584
+ case 'static': {
585
+ if (attrKey === 'href' && typeof content === 'string' && content.startsWith('/')) {
586
+ let targetLink
587
+
588
+ const targetRoute = (routeDefinitions?.values || []).find(
589
+ (route) => route.pageOptions.navLink === content
590
+ )
591
+
592
+ if (targetRoute) {
593
+ targetLink = targetRoute.pageOptions.navLink
594
+ }
595
+
596
+ if (!targetRoute && content === '/home') {
597
+ targetLink = '/'
598
+ }
599
+
600
+ if (!targetLink && !targetRoute) {
601
+ targetLink = content
602
+ }
603
+
604
+ const currentPageRoute = join(...(outputOptions?.folderPath || []), './')
605
+ const localPrefix = relative(
606
+ `/${currentPageRoute}`,
607
+ `/${targetLink === '/' ? 'index' : targetLink}`
608
+ )
609
+
610
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, `${localPrefix}.html`)
611
+ break
612
+ }
424
613
 
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
- }
614
+ if (typeof content === 'boolean') {
615
+ htmlNode.properties[attrKey] = content === true ? 'true' : 'false'
616
+ } else if (typeof content === 'string' || typeof attrValue.content === 'number') {
617
+ let value = StringUtils.encode(String(attrValue.content))
618
+
619
+ /*
620
+ elementType of image is always mapped to img.
621
+ For reference, check `html-mapping` file.
622
+ */
623
+ if (elementType === 'img' && attrKey === 'src' && !isValidURL(value)) {
624
+ /*
625
+ By default we just prefix all the asset paths with just the
626
+ assetPrefix that is configured in the project. But for `html` generators
627
+ we need to prefix that with the current file location.
628
+
629
+ Because, all the other frameworks have a build setup. which serves all the
630
+ assets from the `public` folder. But in the case of `html` here is how it works
631
+
632
+ We load a file from `index.html` the request for the image goes from
633
+ '...url.../public/...image...'
634
+ If it's a nested url, then the request goes from
635
+ '...url/nested/public/...image..'
636
+
637
+ But the nested folder is available only on the root. With this
638
+ The url changes prefixes to
639
+
640
+ ../public/playground_assets/..image.. etc depending on the dept the file is in.
641
+ */
642
+ value = join(relative(join(...outputOptions.folderPath), './'), value)
643
+ }
644
+
645
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
646
+ }
433
647
 
434
- if (attrValue.type === 'raw') {
435
- HASTUtils.addAttributeToNode(htmlNode, attrKey, attrValue.content)
436
- return
437
- }
648
+ break
649
+ }
650
+
651
+ case 'dynamic': {
652
+ const value = getValueFromReference(
653
+ content.id,
654
+ content.referenceType === 'prop' ? propDefinitions : stateDefinitions
655
+ )
438
656
 
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
657
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, String(value.defaultValue))
658
+ break
659
+ }
660
+
661
+ case 'raw': {
662
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, content)
663
+ break
664
+ }
665
+
666
+ case 'element':
667
+ case 'import':
668
+ case 'expr':
669
+ break
670
+
671
+ default: {
672
+ throw new HTMLComponentGeneratorError(
673
+ `Received ${JSON.stringify(attrValue, null, 2)} \n in handleAttributes for html`
674
+ )
675
+ }
445
676
  }
446
- })
677
+ }
447
678
  }
448
679
 
449
680
  const getValueFromReference = (
450
681
  key: string,
451
682
  definitions: Record<string, UIDLPropDefinition>
452
- ): string => {
683
+ ): UIDLPropDefinition | undefined => {
453
684
  const usedReferenceValue = definitions[key.includes('.') ? key.split('.')[0] : key]
454
685
 
455
686
  if (!usedReferenceValue) {
@@ -458,9 +689,9 @@ const getValueFromReference = (
458
689
  )
459
690
  }
460
691
 
461
- if (!usedReferenceValue.hasOwnProperty('defaultValue')) {
692
+ if (['string', 'number', 'object', 'element'].includes(usedReferenceValue?.type) === false) {
462
693
  throw new HTMLComponentGeneratorError(
463
- `Default value is missing from dynamic reference - ${JSON.stringify(
694
+ `Attribute is using dynamic value, but received of type ${JSON.stringify(
464
695
  usedReferenceValue,
465
696
  null,
466
697
  2
@@ -468,9 +699,12 @@ const getValueFromReference = (
468
699
  )
469
700
  }
470
701
 
471
- if (!['string', 'number', 'object'].includes(usedReferenceValue?.type)) {
702
+ if (
703
+ usedReferenceValue.type !== 'element' &&
704
+ usedReferenceValue.hasOwnProperty('defaultValue') === false
705
+ ) {
472
706
  throw new HTMLComponentGeneratorError(
473
- `Attribute is using dynamic value, but received of type ${JSON.stringify(
707
+ `Default value is missing from dynamic reference - ${JSON.stringify(
474
708
  usedReferenceValue,
475
709
  null,
476
710
  2
@@ -478,5 +712,5 @@ const getValueFromReference = (
478
712
  )
479
713
  }
480
714
 
481
- return String(usedReferenceValue.defaultValue)
715
+ return usedReferenceValue
482
716
  }