@teleporthq/teleport-plugin-html-base-component 0.33.0-alpha.0 → 0.33.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.
@@ -17,24 +17,42 @@ import {
17
17
  UIDLStyleValue,
18
18
  GeneratorOptions,
19
19
  UIDLRouteDefinitions,
20
+ ComponentPlugin,
21
+ ComponentStructure,
22
+ UIDLComponentOutputOptions,
23
+ UIDLElement,
20
24
  } from '@teleporthq/teleport-types'
25
+ import { join, relative } from 'path'
21
26
  import { HASTBuilders, HASTUtils } from '@teleporthq/teleport-plugin-common'
22
27
  import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
23
28
  import { staticNode } from '@teleporthq/teleport-uidl-builders'
24
29
  import { createCSSPlugin } from '@teleporthq/teleport-plugin-css'
25
30
  import { DEFAULT_COMPONENT_CHUNK_NAME } from './constants'
26
31
 
32
+ const isValidURL = (url: string) => {
33
+ try {
34
+ /* tslint:disable:no-unused-expression */
35
+ new URL(url)
36
+ return true
37
+ } catch (error) {
38
+ return false
39
+ }
40
+ }
41
+
27
42
  type NodeToHTML<NodeType, ReturnType> = (
28
43
  node: NodeType,
29
44
  templatesLookUp: Record<string, unknown>,
30
45
  propDefinitions: Record<string, UIDLPropDefinition>,
31
46
  stateDefinitions: Record<string, UIDLStateDefinition>,
32
- externals: Record<string, ComponentUIDL>,
33
- routeDefinitions: UIDLRouteDefinitions,
47
+ subComponentOptions: {
48
+ externals: Record<string, ComponentUIDL>
49
+ plugins: ComponentPlugin[]
50
+ },
34
51
  structure: {
35
52
  chunks: ChunkDefinition[]
36
53
  dependencies: Record<string, UIDLDependency>
37
54
  options: GeneratorOptions
55
+ outputOptions: UIDLComponentOutputOptions
38
56
  }
39
57
  ) => ReturnType
40
58
 
@@ -43,11 +61,11 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
43
61
  templatesLookUp,
44
62
  propDefinitions,
45
63
  stateDefinitions,
46
- externals,
47
- routeDefinitions,
64
+ subComponentOptions,
48
65
  structure
49
66
  ) => {
50
67
  switch (node.type) {
68
+ case 'inject':
51
69
  case 'raw':
52
70
  return HASTBuilders.createTextNode(node.content.toString())
53
71
 
@@ -58,27 +76,28 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
58
76
  return HASTBuilders.createHTMLNode(node.type)
59
77
 
60
78
  case 'element':
61
- return generatElementNode(
79
+ const elementNode = await generateElementNode(
62
80
  node,
63
81
  templatesLookUp,
64
82
  propDefinitions,
65
83
  stateDefinitions,
66
- externals,
67
- routeDefinitions,
84
+ subComponentOptions,
68
85
  structure
69
86
  )
87
+ return elementNode
70
88
 
71
89
  case 'dynamic':
72
- return generateDynamicNode(
90
+ const dynamicNode = await generateDynamicNode(
73
91
  node,
74
92
  templatesLookUp,
75
93
  propDefinitions,
76
94
  stateDefinitions,
77
- externals,
78
- routeDefinitions,
95
+ subComponentOptions,
79
96
  structure
80
97
  )
81
98
 
99
+ return dynamicNode
100
+
82
101
  default:
83
102
  throw new HTMLComponentGeneratorError(
84
103
  `generateHtmlSyntax encountered a node of unsupported type: ${JSON.stringify(
@@ -90,13 +109,12 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
90
109
  }
91
110
  }
92
111
 
93
- const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastText>> = async (
112
+ const generateElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastText>> = async (
94
113
  node,
95
114
  templatesLookUp,
96
115
  propDefinitions,
97
116
  stateDefinitions,
98
- externals,
99
- routeDefinitions,
117
+ subComponentOptions,
100
118
  structure
101
119
  ) => {
102
120
  const {
@@ -122,8 +140,8 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
122
140
  node,
123
141
  propDefinitions,
124
142
  stateDefinitions,
125
- externals,
126
- routeDefinitions,
143
+ templatesLookUp,
144
+ subComponentOptions,
127
145
  structure
128
146
  )
129
147
  return compTag
@@ -136,8 +154,7 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
136
154
  templatesLookUp,
137
155
  propDefinitions,
138
156
  stateDefinitions,
139
- externals,
140
- routeDefinitions,
157
+ subComponentOptions,
141
158
  structure
142
159
  )
143
160
 
@@ -167,9 +184,15 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
167
184
  handleStyles(node, style, propDefinitions, stateDefinitions)
168
185
  }
169
186
 
170
- if (Object.keys(attrs).length > 0) {
171
- handleAttributes(elementNode, attrs, propDefinitions, stateDefinitions, routeDefinitions)
172
- }
187
+ handleAttributes(
188
+ elementType,
189
+ elementNode,
190
+ attrs,
191
+ propDefinitions,
192
+ stateDefinitions,
193
+ structure.options.projectRouteDefinition,
194
+ structure.outputOptions
195
+ )
173
196
 
174
197
  return elementNode
175
198
  }
@@ -178,18 +201,24 @@ const generateComponentContent = async (
178
201
  node: UIDLElementNode,
179
202
  propDefinitions: Record<string, UIDLPropDefinition>,
180
203
  stateDefinitions: Record<string, UIDLStateDefinition>,
181
- externals: Record<string, ComponentUIDL>,
182
- routeDefinitions: UIDLRouteDefinitions,
204
+ templateLookup: Record<string, unknown>,
205
+ subComponentOptions: {
206
+ externals: Record<string, ComponentUIDL>
207
+ plugins: ComponentPlugin[]
208
+ },
183
209
  structure: {
184
210
  chunks: ChunkDefinition[]
185
211
  dependencies: Record<string, UIDLDependency>
186
212
  options: GeneratorOptions
213
+ outputOptions: UIDLComponentOutputOptions
187
214
  }
188
215
  ) => {
216
+ const { externals, plugins } = subComponentOptions
189
217
  const { elementType, attrs = {}, key, children = [] } = node.content
190
- const { dependencies, chunks, options } = structure
191
- const comp = UIDLUtils.cloneObject(externals[elementType] || {}) as ComponentUIDL
192
- const lookUpTemplates: Record<string, unknown> = {}
218
+ const { dependencies, chunks = [], options } = structure
219
+ // "Component" will not exist when generating a component because the resolver checks for illegal class names
220
+ const compName = elementType === 'Component' ? 'AppComponent' : elementType
221
+ const comp = UIDLUtils.cloneObject(externals[compName] || {}) as ComponentUIDL
193
222
  let compHasSlots: boolean = false
194
223
 
195
224
  if (!comp || !comp?.node) {
@@ -229,22 +258,26 @@ const generateComponentContent = async (
229
258
  }
230
259
 
231
260
  const combinedProps = { ...propDefinitions, ...(comp?.propDefinitions || {}) }
232
-
233
- const propsForInstance = Object.keys(combinedProps).reduce(
234
- (acc: Record<string, UIDLPropDefinition>, propKey) => {
235
- if (attrs[propKey]) {
236
- acc[propKey] = {
237
- ...combinedProps[propKey],
238
- defaultValue: attrs[propKey]?.content || combinedProps[propKey]?.defaultValue,
239
- }
240
- } else {
241
- acc[propKey] = combinedProps[propKey]
261
+ const propsForInstance: Record<string, UIDLPropDefinition> = {}
262
+ for (const propKey of Object.keys(combinedProps)) {
263
+ // If the attribute is a named-slot, then we can directly pass the value instead of just the content
264
+ if (attrs[propKey]?.type === 'element') {
265
+ propsForInstance[propKey] = {
266
+ ...combinedProps[propKey],
267
+ defaultValue: attrs[propKey],
242
268
  }
269
+ continue
270
+ }
243
271
 
244
- return acc
245
- },
246
- {}
247
- )
272
+ if (attrs[propKey]) {
273
+ propsForInstance[propKey] = {
274
+ ...combinedProps[propKey],
275
+ defaultValue: attrs[propKey]?.content || combinedProps[propKey]?.defaultValue,
276
+ }
277
+ } else {
278
+ propsForInstance[propKey] = combinedProps[propKey]
279
+ }
280
+ }
248
281
 
249
282
  const combinedStates = { ...stateDefinitions, ...(comp?.stateDefinitions || {}) }
250
283
  const statesForInstance = Object.keys(combinedStates).reduce(
@@ -264,7 +297,7 @@ const generateComponentContent = async (
264
297
  )
265
298
 
266
299
  const elementNode = HASTBuilders.createHTMLNode(StringUtils.camelCaseToDashCase(elementType))
267
- lookUpTemplates[key] = elementNode
300
+ templateLookup[key] = elementNode
268
301
 
269
302
  const compTag = (await generateHtmlSynatx(
270
303
  {
@@ -280,24 +313,23 @@ const generateComponentContent = async (
280
313
  },
281
314
  },
282
315
  },
283
- lookUpTemplates,
316
+ templateLookup,
284
317
  propsForInstance,
285
318
  statesForInstance,
286
- externals,
287
- routeDefinitions,
319
+ subComponentOptions,
288
320
  structure
289
321
  )) as unknown as HastNode
290
322
 
291
323
  const cssPlugin = createCSSPlugin({
292
324
  templateStyle: 'html',
293
- templateChunkName: 'html-template',
325
+ templateChunkName: DEFAULT_COMPONENT_CHUNK_NAME,
294
326
  declareDependency: 'import',
295
327
  forceScoping: true,
296
328
  chunkName: comp.name,
297
329
  staticPropReferences: true,
298
330
  })
299
331
 
300
- const result = await cssPlugin({
332
+ const initialStructure: ComponentStructure = {
301
333
  uidl: {
302
334
  ...comp,
303
335
  propDefinitions: propsForInstance,
@@ -311,13 +343,21 @@ const generateComponentContent = async (
311
343
  linkAfter: [],
312
344
  content: compTag,
313
345
  meta: {
314
- nodesLookup: lookUpTemplates,
346
+ nodesLookup: templateLookup,
315
347
  },
316
348
  },
317
349
  ],
318
350
  dependencies,
319
351
  options,
320
- })
352
+ }
353
+
354
+ const result = await [cssPlugin, ...plugins].reduce(
355
+ async (previousPluginOperation: Promise<ComponentStructure>, plugin) => {
356
+ const modifiedStructure = await previousPluginOperation
357
+ return plugin(modifiedStructure)
358
+ },
359
+ Promise.resolve(initialStructure)
360
+ )
321
361
 
322
362
  if (compHasSlots) {
323
363
  result.chunks.forEach((chunk) => {
@@ -328,7 +368,12 @@ const generateComponentContent = async (
328
368
  } else {
329
369
  const chunk = chunks.find((item) => item.name === comp.name)
330
370
  if (!chunk) {
331
- const styleChunk = result.chunks.find((item: ChunkDefinition) => item.name === comp.name)
371
+ const styleChunk = result.chunks.find(
372
+ (item: ChunkDefinition) => item.fileType === FileType.CSS
373
+ )
374
+ if (!styleChunk) {
375
+ return
376
+ }
332
377
  chunks.push(styleChunk)
333
378
  }
334
379
  }
@@ -336,19 +381,41 @@ const generateComponentContent = async (
336
381
  return compTag
337
382
  }
338
383
 
339
- const generateDynamicNode: NodeToHTML<UIDLDynamicReference, HastNode> = (
384
+ const generateDynamicNode: NodeToHTML<UIDLDynamicReference, Promise<HastNode>> = async (
340
385
  node,
341
- _,
386
+ templateLookup,
342
387
  propDefinitions,
343
- stateDefinitions
344
- ) => {
345
- const spanTag = HASTBuilders.createHTMLNode('span')
346
- const usedReferenceValue =
347
- node.content.referenceType === 'prop'
348
- ? getValueFromReference(node.content.id, propDefinitions)
349
- : getValueFromReference(node.content.id, stateDefinitions)
388
+ stateDefinitions,
389
+ subComponentOptions,
390
+ structure
391
+ ): Promise<HastNode> => {
392
+ const usedReferenceValue = getValueFromReference(
393
+ node.content.id,
394
+ node.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
395
+ )
396
+
397
+ if (usedReferenceValue.type === 'element' && usedReferenceValue.defaultValue) {
398
+ const slotNode = await generateElementNode(
399
+ usedReferenceValue.defaultValue as UIDLElementNode,
400
+ templateLookup,
401
+ propDefinitions,
402
+ stateDefinitions,
403
+ subComponentOptions,
404
+ structure
405
+ )
406
+
407
+ return slotNode as HastNode
408
+ }
350
409
 
351
- HASTUtils.addTextNode(spanTag, String(usedReferenceValue))
410
+ if (usedReferenceValue.type === 'element' && usedReferenceValue.defaultValue === undefined) {
411
+ const spanTagWrapper = HASTBuilders.createHTMLNode('span')
412
+ const commentNode = HASTBuilders.createComment(`Content for slot ${node.content.id}`)
413
+ HASTUtils.addChildNode(spanTagWrapper, commentNode)
414
+ return spanTagWrapper
415
+ }
416
+
417
+ const spanTag = HASTBuilders.createHTMLNode('span')
418
+ HASTUtils.addTextNode(spanTag, String(usedReferenceValue.defaultValue))
352
419
  return spanTag
353
420
  }
354
421
 
@@ -361,10 +428,12 @@ const handleStyles = (
361
428
  Object.keys(styles).forEach((styleKey) => {
362
429
  let style: string | UIDLStyleValue = styles[styleKey]
363
430
  if (style.type === 'dynamic' && style.content?.referenceType !== 'token') {
364
- if (style.content.referenceType === 'prop') {
365
- style = getValueFromReference(style.content.id, propDefinitions)
366
- } else if (style.content.referenceType === 'state') {
367
- style = getValueFromReference(style.content.id, stateDefinitions)
431
+ const referencedValue = getValueFromReference(
432
+ style.content.id,
433
+ style.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
434
+ )
435
+ if (referencedValue.type === 'string' || referencedValue.type === 'number') {
436
+ style = String(referencedValue.defaultValue)
368
437
  }
369
438
  node.content.style[styleKey] = typeof style === 'string' ? staticNode(style) : style
370
439
  }
@@ -372,62 +441,119 @@ const handleStyles = (
372
441
  }
373
442
 
374
443
  const handleAttributes = (
444
+ elementType: UIDLElement['elementType'],
375
445
  htmlNode: HastNode,
376
446
  attrs: Record<string, UIDLAttributeValue>,
377
447
  propDefinitions: Record<string, UIDLPropDefinition>,
378
448
  stateDefinitions: Record<string, UIDLStateDefinition>,
379
- routeDefinitions: UIDLRouteDefinitions
449
+ routeDefinitions: UIDLRouteDefinitions,
450
+ outputOptions: UIDLComponentOutputOptions
380
451
  ) => {
381
- Object.keys(attrs).forEach((attrKey) => {
382
- let attrValue = attrs[attrKey]
383
-
384
- if (
385
- attrKey === 'href' &&
386
- attrValue.type === 'static' &&
387
- typeof attrValue.content === 'string' &&
388
- attrValue.content.startsWith('/')
389
- ) {
390
- attrValue =
391
- attrValue.content === '/' ||
392
- attrValue.content ===
393
- `/${StringUtils.camelCaseToDashCase(
394
- StringUtils.removeIllegalCharacters(routeDefinitions?.defaultValue || '')
395
- )}`
396
- ? staticNode('index.html')
397
- : staticNode(`${attrValue.content.split('/').pop()}.html`)
398
- HASTUtils.addAttributeToNode(htmlNode, attrKey, String(attrValue.content))
399
- return
400
- }
452
+ for (const attrKey of Object.keys(attrs)) {
453
+ const attrValue = attrs[attrKey]
454
+ const { type, content } = attrValue
455
+
456
+ switch (type) {
457
+ case 'static': {
458
+ if (attrKey === 'href' && typeof content === 'string' && content.startsWith('/')) {
459
+ let targetLink
460
+
461
+ const targetRoute = (routeDefinitions?.values || []).find(
462
+ (route) => route.pageOptions.navLink === content
463
+ )
464
+
465
+ if (targetRoute) {
466
+ targetLink = targetRoute.pageOptions.navLink
467
+ }
468
+
469
+ if (!targetRoute && content === '/home') {
470
+ targetLink = '/'
471
+ }
472
+
473
+ if (!targetLink && !targetRoute) {
474
+ targetLink = content
475
+ }
476
+
477
+ const currentPageRoute = join(...(outputOptions?.folderPath || []), './')
478
+ const localPrefix = relative(
479
+ `/${currentPageRoute}`,
480
+ `/${targetLink === '/' ? 'index' : targetLink}`
481
+ )
482
+
483
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, `${localPrefix}.html`)
484
+ break
485
+ }
401
486
 
402
- if (attrValue.type === 'dynamic') {
403
- const value =
404
- attrValue.content.referenceType === 'prop'
405
- ? getValueFromReference(attrValue.content.id, propDefinitions)
406
- : getValueFromReference(attrValue.content.id, stateDefinitions)
407
- HASTUtils.addAttributeToNode(htmlNode, attrKey, String(value))
408
- return
409
- }
487
+ if (typeof content === 'boolean') {
488
+ htmlNode.properties[attrKey] = content === true ? 'true' : 'false'
489
+ } else if (typeof content === 'string' || typeof attrValue.content === 'number') {
490
+ let value = StringUtils.encode(String(attrValue.content))
491
+
492
+ /*
493
+ elementType of image is always mapped to img.
494
+ For reference, check `html-mapping` file.
495
+ */
496
+ if (elementType === 'img' && attrKey === 'src' && !isValidURL(value)) {
497
+ /*
498
+ By default we just prefix all the asset paths with just the
499
+ assetPrefix that is configured in the project. But for `html` generators
500
+ we need to prefix that with the current file location.
501
+
502
+ Because, all the other frameworks have a build setup. which serves all the
503
+ assets from the `public` folder. But in the case of `html` here is how it works
504
+
505
+ We load a file from `index.html` the request for the image goes from
506
+ '...url.../public/...image...'
507
+ If it's a nested url, then the request goes from
508
+ '...url/nested/public/...image..'
509
+
510
+ But the nested folder is available only on the root. With this
511
+ The url changes prefixes to
512
+
513
+ ../public/playground_assets/..image.. etc depending on the dept the file is in.
514
+ */
515
+ value = join(relative(join(...outputOptions.folderPath), './'), value)
516
+ }
517
+
518
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
519
+ }
410
520
 
411
- if (attrValue.type === 'raw') {
412
- HASTUtils.addAttributeToNode(htmlNode, attrKey, String(attrValue.content))
413
- return
414
- }
521
+ break
522
+ }
523
+
524
+ case 'dynamic': {
525
+ const value = getValueFromReference(
526
+ content.id,
527
+ content.referenceType === 'prop' ? propDefinitions : stateDefinitions
528
+ )
529
+
530
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, String(value.defaultValue))
531
+ break
532
+ }
533
+
534
+ case 'raw': {
535
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, content)
536
+ break
537
+ }
415
538
 
416
- if (typeof attrValue.content === 'boolean') {
417
- HASTUtils.addBooleanAttributeToNode(htmlNode, attrKey)
418
- return
419
- } else if (typeof attrValue.content === 'string' || typeof attrValue.content === 'number') {
420
- HASTUtils.addAttributeToNode(htmlNode, attrKey, StringUtils.encode(String(attrValue.content)))
421
- return
539
+ case 'element':
540
+ case 'import':
541
+ break
542
+
543
+ default: {
544
+ throw new HTMLComponentGeneratorError(
545
+ `Received ${JSON.stringify(attrValue, null, 2)} \n in handleAttributes for html`
546
+ )
547
+ }
422
548
  }
423
- })
549
+ }
424
550
  }
425
551
 
426
552
  const getValueFromReference = (
427
553
  key: string,
428
554
  definitions: Record<string, UIDLPropDefinition>
429
- ): string => {
430
- const usedReferenceValue = definitions[key]
555
+ ): UIDLPropDefinition | undefined => {
556
+ const usedReferenceValue = definitions[key.includes('.') ? key.split('.')[0] : key]
431
557
 
432
558
  if (!usedReferenceValue) {
433
559
  throw new HTMLComponentGeneratorError(
@@ -435,9 +561,9 @@ const getValueFromReference = (
435
561
  )
436
562
  }
437
563
 
438
- if (!usedReferenceValue.hasOwnProperty('defaultValue')) {
564
+ if (['string', 'number', 'object', 'element'].includes(usedReferenceValue?.type) === false) {
439
565
  throw new HTMLComponentGeneratorError(
440
- `Default value is missing from dynamic reference - ${JSON.stringify(
566
+ `Attribute is using dynamic value, but received of type ${JSON.stringify(
441
567
  usedReferenceValue,
442
568
  null,
443
569
  2
@@ -445,9 +571,12 @@ const getValueFromReference = (
445
571
  )
446
572
  }
447
573
 
448
- if (!['string', 'number'].includes(usedReferenceValue?.type)) {
574
+ if (
575
+ usedReferenceValue.type !== 'element' &&
576
+ usedReferenceValue.hasOwnProperty('defaultValue') === false
577
+ ) {
449
578
  throw new HTMLComponentGeneratorError(
450
- `Attribute is using dynamic value, but received of type ${JSON.stringify(
579
+ `Default value is missing from dynamic reference - ${JSON.stringify(
451
580
  usedReferenceValue,
452
581
  null,
453
582
  2
@@ -455,5 +584,5 @@ const getValueFromReference = (
455
584
  )
456
585
  }
457
586
 
458
- return String(usedReferenceValue.defaultValue)
587
+ return usedReferenceValue
459
588
  }