@teleporthq/teleport-plugin-common 0.44.0-alpha.0 → 0.44.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.
Files changed (190) hide show
  1. package/__tests__/builders/html-builders.ts +9 -2
  2. package/__tests__/builders/style-builders.ts +23 -0
  3. package/__tests__/node-handlers/node-to-html/index.ts +2 -0
  4. package/__tests__/node-handlers/node-to-jsx/attribute-value-mapper.ts +77 -0
  5. package/__tests__/node-handlers/node-to-jsx/calendar-events-reviver.ts +176 -0
  6. package/__tests__/node-handlers/node-to-jsx/cms-list-repeater-ecommerce-source.ts +79 -0
  7. package/__tests__/node-handlers/node-to-jsx/cms-list-repeater-loading.ts +122 -0
  8. package/__tests__/node-handlers/node-to-jsx/index.ts +719 -3
  9. package/__tests__/node-handlers/node-to-jsx/url-search-params-expression.ts +59 -0
  10. package/__tests__/node-handlers/node-to-jsx/utils.ts +602 -3
  11. package/__tests__/utils/ast-utils.ts +188 -0
  12. package/__tests__/utils/content-property-sanitiser.ts +156 -0
  13. package/__tests__/utils/page-seeds-state-from-data-source.ts +70 -0
  14. package/__tests__/utils/parse-string-with-template-expressions.ts +82 -0
  15. package/__tests__/utils/resource-auth-scheme.ts +55 -0
  16. package/__tests__/utils/route-utils.ts +118 -0
  17. package/__tests__/utils/style-utils.ts +59 -0
  18. package/__tests__/utils/template-expression-balance.ts +83 -0
  19. package/__tests__/utils/url-query-writer.ts +239 -0
  20. package/__tests__/utils/url-search-param-sync.ts +138 -0
  21. package/__tests__/utils/url-search-params.ts +48 -0
  22. package/dist/cjs/builders/ast-builders.d.ts +2 -0
  23. package/dist/cjs/builders/ast-builders.d.ts.map +1 -1
  24. package/dist/cjs/builders/ast-builders.js +41 -3
  25. package/dist/cjs/builders/ast-builders.js.map +1 -1
  26. package/dist/cjs/builders/hast-builders.d.ts +1 -0
  27. package/dist/cjs/builders/hast-builders.d.ts.map +1 -1
  28. package/dist/cjs/builders/hast-builders.js +11 -1
  29. package/dist/cjs/builders/hast-builders.js.map +1 -1
  30. package/dist/cjs/builders/style-builders.d.ts +8 -2
  31. package/dist/cjs/builders/style-builders.d.ts.map +1 -1
  32. package/dist/cjs/builders/style-builders.js +65 -19
  33. package/dist/cjs/builders/style-builders.js.map +1 -1
  34. package/dist/cjs/index.d.ts +5 -1
  35. package/dist/cjs/index.d.ts.map +1 -1
  36. package/dist/cjs/index.js +9 -1
  37. package/dist/cjs/index.js.map +1 -1
  38. package/dist/cjs/node-handlers/node-to-html/constants.d.ts.map +1 -1
  39. package/dist/cjs/node-handlers/node-to-html/constants.js +2 -0
  40. package/dist/cjs/node-handlers/node-to-html/constants.js.map +1 -1
  41. package/dist/cjs/node-handlers/node-to-html/index.d.ts.map +1 -1
  42. package/dist/cjs/node-handlers/node-to-html/index.js +69 -1
  43. package/dist/cjs/node-handlers/node-to-html/index.js.map +1 -1
  44. package/dist/cjs/node-handlers/node-to-html/types.d.ts +5 -1
  45. package/dist/cjs/node-handlers/node-to-html/types.d.ts.map +1 -1
  46. package/dist/cjs/node-handlers/node-to-html/utils.d.ts +2 -1
  47. package/dist/cjs/node-handlers/node-to-html/utils.d.ts.map +1 -1
  48. package/dist/cjs/node-handlers/node-to-html/utils.js +174 -13
  49. package/dist/cjs/node-handlers/node-to-html/utils.js.map +1 -1
  50. package/dist/cjs/node-handlers/node-to-jsx/index.d.ts.map +1 -1
  51. package/dist/cjs/node-handlers/node-to-jsx/index.js +1461 -66
  52. package/dist/cjs/node-handlers/node-to-jsx/index.js.map +1 -1
  53. package/dist/cjs/node-handlers/node-to-jsx/types.d.ts +60 -6
  54. package/dist/cjs/node-handlers/node-to-jsx/types.d.ts.map +1 -1
  55. package/dist/cjs/node-handlers/node-to-jsx/utils.d.ts +73 -7
  56. package/dist/cjs/node-handlers/node-to-jsx/utils.d.ts.map +1 -1
  57. package/dist/cjs/node-handlers/node-to-jsx/utils.js +864 -71
  58. package/dist/cjs/node-handlers/node-to-jsx/utils.js.map +1 -1
  59. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  60. package/dist/cjs/utils/ast-utils.d.ts +142 -11
  61. package/dist/cjs/utils/ast-utils.d.ts.map +1 -1
  62. package/dist/cjs/utils/ast-utils.js +931 -75
  63. package/dist/cjs/utils/ast-utils.js.map +1 -1
  64. package/dist/cjs/utils/hast-utils.d.ts +8 -6
  65. package/dist/cjs/utils/hast-utils.d.ts.map +1 -1
  66. package/dist/cjs/utils/hast-utils.js +28 -4
  67. package/dist/cjs/utils/hast-utils.js.map +1 -1
  68. package/dist/cjs/utils/route-utils.d.ts +90 -0
  69. package/dist/cjs/utils/route-utils.d.ts.map +1 -0
  70. package/dist/cjs/utils/route-utils.js +152 -0
  71. package/dist/cjs/utils/route-utils.js.map +1 -0
  72. package/dist/cjs/utils/style-utils.d.ts +1 -0
  73. package/dist/cjs/utils/style-utils.d.ts.map +1 -1
  74. package/dist/cjs/utils/style-utils.js +181 -2
  75. package/dist/cjs/utils/style-utils.js.map +1 -1
  76. package/dist/cjs/utils/template-expression-balance.d.ts +58 -0
  77. package/dist/cjs/utils/template-expression-balance.d.ts.map +1 -0
  78. package/dist/cjs/utils/template-expression-balance.js +181 -0
  79. package/dist/cjs/utils/template-expression-balance.js.map +1 -0
  80. package/dist/cjs/utils/types.d.ts +3 -0
  81. package/dist/cjs/utils/types.d.ts.map +1 -0
  82. package/dist/cjs/utils/types.js +3 -0
  83. package/dist/cjs/utils/types.js.map +1 -0
  84. package/dist/cjs/utils/url-query-writer.d.ts +123 -0
  85. package/dist/cjs/utils/url-query-writer.d.ts.map +1 -0
  86. package/dist/cjs/utils/url-query-writer.js +260 -0
  87. package/dist/cjs/utils/url-query-writer.js.map +1 -0
  88. package/dist/cjs/utils/url-search-param-sync.d.ts +4 -0
  89. package/dist/cjs/utils/url-search-param-sync.d.ts.map +1 -0
  90. package/dist/cjs/utils/url-search-param-sync.js +241 -0
  91. package/dist/cjs/utils/url-search-param-sync.js.map +1 -0
  92. package/dist/cjs/utils/url-search-params.d.ts +48 -0
  93. package/dist/cjs/utils/url-search-params.d.ts.map +1 -0
  94. package/dist/cjs/utils/url-search-params.js +109 -0
  95. package/dist/cjs/utils/url-search-params.js.map +1 -0
  96. package/dist/esm/builders/ast-builders.d.ts +2 -0
  97. package/dist/esm/builders/ast-builders.d.ts.map +1 -1
  98. package/dist/esm/builders/ast-builders.js +40 -4
  99. package/dist/esm/builders/ast-builders.js.map +1 -1
  100. package/dist/esm/builders/hast-builders.d.ts +1 -0
  101. package/dist/esm/builders/hast-builders.d.ts.map +1 -1
  102. package/dist/esm/builders/hast-builders.js +9 -0
  103. package/dist/esm/builders/hast-builders.js.map +1 -1
  104. package/dist/esm/builders/style-builders.d.ts +8 -2
  105. package/dist/esm/builders/style-builders.d.ts.map +1 -1
  106. package/dist/esm/builders/style-builders.js +63 -18
  107. package/dist/esm/builders/style-builders.js.map +1 -1
  108. package/dist/esm/index.d.ts +5 -1
  109. package/dist/esm/index.d.ts.map +1 -1
  110. package/dist/esm/index.js +5 -1
  111. package/dist/esm/index.js.map +1 -1
  112. package/dist/esm/node-handlers/node-to-html/constants.d.ts.map +1 -1
  113. package/dist/esm/node-handlers/node-to-html/constants.js +2 -0
  114. package/dist/esm/node-handlers/node-to-html/constants.js.map +1 -1
  115. package/dist/esm/node-handlers/node-to-html/index.d.ts.map +1 -1
  116. package/dist/esm/node-handlers/node-to-html/index.js +71 -3
  117. package/dist/esm/node-handlers/node-to-html/index.js.map +1 -1
  118. package/dist/esm/node-handlers/node-to-html/types.d.ts +5 -1
  119. package/dist/esm/node-handlers/node-to-html/types.d.ts.map +1 -1
  120. package/dist/esm/node-handlers/node-to-html/utils.d.ts +2 -1
  121. package/dist/esm/node-handlers/node-to-html/utils.d.ts.map +1 -1
  122. package/dist/esm/node-handlers/node-to-html/utils.js +170 -13
  123. package/dist/esm/node-handlers/node-to-html/utils.js.map +1 -1
  124. package/dist/esm/node-handlers/node-to-jsx/index.d.ts.map +1 -1
  125. package/dist/esm/node-handlers/node-to-jsx/index.js +1460 -68
  126. package/dist/esm/node-handlers/node-to-jsx/index.js.map +1 -1
  127. package/dist/esm/node-handlers/node-to-jsx/types.d.ts +60 -6
  128. package/dist/esm/node-handlers/node-to-jsx/types.d.ts.map +1 -1
  129. package/dist/esm/node-handlers/node-to-jsx/utils.d.ts +73 -7
  130. package/dist/esm/node-handlers/node-to-jsx/utils.d.ts.map +1 -1
  131. package/dist/esm/node-handlers/node-to-jsx/utils.js +857 -69
  132. package/dist/esm/node-handlers/node-to-jsx/utils.js.map +1 -1
  133. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  134. package/dist/esm/utils/ast-utils.d.ts +142 -11
  135. package/dist/esm/utils/ast-utils.d.ts.map +1 -1
  136. package/dist/esm/utils/ast-utils.js +911 -75
  137. package/dist/esm/utils/ast-utils.js.map +1 -1
  138. package/dist/esm/utils/hast-utils.d.ts +8 -6
  139. package/dist/esm/utils/hast-utils.d.ts.map +1 -1
  140. package/dist/esm/utils/hast-utils.js +26 -3
  141. package/dist/esm/utils/hast-utils.js.map +1 -1
  142. package/dist/esm/utils/route-utils.d.ts +90 -0
  143. package/dist/esm/utils/route-utils.d.ts.map +1 -0
  144. package/dist/esm/utils/route-utils.js +144 -0
  145. package/dist/esm/utils/route-utils.js.map +1 -0
  146. package/dist/esm/utils/style-utils.d.ts +1 -0
  147. package/dist/esm/utils/style-utils.d.ts.map +1 -1
  148. package/dist/esm/utils/style-utils.js +179 -1
  149. package/dist/esm/utils/style-utils.js.map +1 -1
  150. package/dist/esm/utils/template-expression-balance.d.ts +58 -0
  151. package/dist/esm/utils/template-expression-balance.d.ts.map +1 -0
  152. package/dist/esm/utils/template-expression-balance.js +174 -0
  153. package/dist/esm/utils/template-expression-balance.js.map +1 -0
  154. package/dist/esm/utils/types.d.ts +3 -0
  155. package/dist/esm/utils/types.d.ts.map +1 -0
  156. package/dist/esm/utils/types.js +2 -0
  157. package/dist/esm/utils/types.js.map +1 -0
  158. package/dist/esm/utils/url-query-writer.d.ts +123 -0
  159. package/dist/esm/utils/url-query-writer.d.ts.map +1 -0
  160. package/dist/esm/utils/url-query-writer.js +229 -0
  161. package/dist/esm/utils/url-query-writer.js.map +1 -0
  162. package/dist/esm/utils/url-search-param-sync.d.ts +4 -0
  163. package/dist/esm/utils/url-search-param-sync.d.ts.map +1 -0
  164. package/dist/esm/utils/url-search-param-sync.js +213 -0
  165. package/dist/esm/utils/url-search-param-sync.js.map +1 -0
  166. package/dist/esm/utils/url-search-params.d.ts +48 -0
  167. package/dist/esm/utils/url-search-params.d.ts.map +1 -0
  168. package/dist/esm/utils/url-search-params.js +81 -0
  169. package/dist/esm/utils/url-search-params.js.map +1 -0
  170. package/package.json +4 -4
  171. package/src/builders/ast-builders.ts +80 -3
  172. package/src/builders/hast-builders.ts +11 -0
  173. package/src/builders/style-builders.ts +107 -27
  174. package/src/index.ts +18 -1
  175. package/src/node-handlers/node-to-html/constants.ts +2 -0
  176. package/src/node-handlers/node-to-html/index.ts +126 -6
  177. package/src/node-handlers/node-to-html/types.ts +6 -0
  178. package/src/node-handlers/node-to-html/utils.ts +228 -14
  179. package/src/node-handlers/node-to-jsx/index.ts +2219 -111
  180. package/src/node-handlers/node-to-jsx/types.ts +75 -32
  181. package/src/node-handlers/node-to-jsx/utils.ts +1142 -84
  182. package/src/utils/ast-utils.ts +1223 -127
  183. package/src/utils/hast-utils.ts +37 -9
  184. package/src/utils/route-utils.ts +168 -0
  185. package/src/utils/style-utils.ts +183 -1
  186. package/src/utils/template-expression-balance.ts +196 -0
  187. package/src/utils/types.ts +25 -0
  188. package/src/utils/url-query-writer.ts +318 -0
  189. package/src/utils/url-search-param-sync.ts +303 -0
  190. package/src/utils/url-search-params.ts +122 -0
@@ -3,7 +3,7 @@ import { createHTMLNode, createTextNode } from '../../src/builders/hast-builders
3
3
  describe('HTML Builders', () => {
4
4
  describe('createHTMLNode', () => {
5
5
  it('should create html node', () => {
6
- const htmlNode = createHTMLNode('span', ['span'])
6
+ const htmlNode = createHTMLNode('span', [createHTMLNode('span', [])])
7
7
 
8
8
  expect(htmlNode).toHaveProperty('type')
9
9
  expect(htmlNode).toHaveProperty('tagName')
@@ -12,7 +12,14 @@ describe('HTML Builders', () => {
12
12
  expect(htmlNode.type).toBe('element')
13
13
  expect(htmlNode.tagName).toBe('span')
14
14
  expect(htmlNode.properties).toEqual({})
15
- expect(htmlNode.children).toEqual(['span'])
15
+ expect(htmlNode.children).toEqual([
16
+ {
17
+ children: [],
18
+ tagName: 'span',
19
+ type: 'element',
20
+ properties: {},
21
+ },
22
+ ])
16
23
  })
17
24
  })
18
25
  describe('createTextNode', () => {
@@ -24,6 +24,29 @@ describe('CSS Class Generation', () => {
24
24
  expect(result).toEqual(`.name h1 > h2 .ab.cd #id {
25
25
  some-key: value;
26
26
  other-key: otherValue;
27
+ }`)
28
+ })
29
+
30
+ it('attaches an ATTRIBUTE subselector with no separating space', () => {
31
+ // ⛔ THE SELECTED-PAGE CONTRACT. A numbered pagination strip repeats one
32
+ // authored node per visible page, so the copies cannot differ in the
33
+ // document. Both runtimes instead mark the showing page with
34
+ // `aria-current="page"` (`numbered-pagination.ts` here, `element-node.tsx`
35
+ // in the canvas editor), and the LOOK is a compound project class whose
36
+ // subselector is that attribute.
37
+ //
38
+ // A space before the bracket would turn this into a DESCENDANT selector —
39
+ // `.thq-page-number [aria-current='page']` matches something INSIDE the
40
+ // button, never the button — and the selected page would silently render
41
+ // identically to every other one. Which is exactly the bug the marker was
42
+ // added to fix.
43
+ const result = createCSSClassWithSelector('thq-page-number', "&[aria-current='page']", {
44
+ background: '#2563eb',
45
+ fontWeight: '600',
46
+ })
47
+ expect(result).toEqual(`.thq-page-number[aria-current='page'] {
48
+ background: #2563eb;
49
+ font-weight: 600;
27
50
  }`)
28
51
  })
29
52
  })
@@ -12,6 +12,8 @@ describe('generateHTMLTemplateSyntax', () => {
12
12
  dataObject: {},
13
13
  methodsObject: {},
14
14
  templateLookup: {},
15
+ propDefinitions: {},
16
+ stateDefinitions: {},
15
17
  }
16
18
 
17
19
  describe('uidl node', () => {
@@ -0,0 +1,77 @@
1
+ import generator from '@babel/generator'
2
+ import * as types from '@babel/types'
3
+ import generateJSXSyntax from '../../../src/node-handlers/node-to-jsx'
4
+ import {
5
+ JSXGenerationParams,
6
+ JSXGenerationOptions,
7
+ } from '../../../src/node-handlers/node-to-jsx/types'
8
+ import { UIDLElementNode } from '@teleporthq/teleport-types'
9
+
10
+ /**
11
+ * An attribute bound through a value mapper renders through it, as a text
12
+ * binding does: a filter chip's `aria-pressed` reads the filter's state as
13
+ * "true" or "false", never as the filter's value.
14
+ */
15
+
16
+ const MAPPER = [
17
+ 'function mapValue(value) {',
18
+ " return String(value) === 'On track' ? 'true' : 'false';",
19
+ '}',
20
+ ].join('\n')
21
+
22
+ const chip = (valueMapper?: string): UIDLElementNode => ({
23
+ type: 'element',
24
+ content: {
25
+ key: 'chip',
26
+ elementType: 'button',
27
+ attrs: {
28
+ 'aria-pressed': {
29
+ type: 'dynamic',
30
+ content: {
31
+ referenceType: 'state',
32
+ id: 'jobFilterStatus',
33
+ refPath: [],
34
+ ...(valueMapper ? { valueMapper } : {}),
35
+ },
36
+ },
37
+ },
38
+ children: [],
39
+ },
40
+ })
41
+
42
+ const params = (): JSXGenerationParams => ({
43
+ dependencies: {},
44
+ propDefinitions: {},
45
+ stateDefinitions: {
46
+ jobFilterStatus: { type: 'string', defaultValue: '' },
47
+ },
48
+ globalStateDefinitions: {},
49
+ nodesLookup: {},
50
+ windowImports: {},
51
+ localeReferences: [],
52
+ localeAttributeReferences: [],
53
+ globalReferences: [],
54
+ globalStateReferences: [],
55
+ hoistedConstants: [],
56
+ })
57
+
58
+ const options: JSXGenerationOptions = {
59
+ dynamicReferencePrefixMap: { prop: 'props', state: '', local: '' },
60
+ }
61
+
62
+ const render = (node: UIDLElementNode): string =>
63
+ generator(generateJSXSyntax(node, params(), options) as types.Node).code.replace(/\s+/g, ' ')
64
+
65
+ describe('a dynamic attribute with a value mapper', () => {
66
+ it('renders the mapped value', () => {
67
+ const code = render(chip(MAPPER))
68
+ expect(code).toContain('aria-pressed={')
69
+ expect(code).toContain('mapValue(value)')
70
+ expect(code).toContain('jobFilterStatus')
71
+ expect(code).not.toContain('aria-pressed={jobFilterStatus}')
72
+ })
73
+
74
+ it('keeps a bare reference without one', () => {
75
+ expect(render(chip())).toContain('aria-pressed={jobFilterStatus}')
76
+ })
77
+ })
@@ -0,0 +1,176 @@
1
+ import * as types from '@babel/types'
2
+ import generateJSXSyntax from '../../../src/node-handlers/node-to-jsx'
3
+ import {
4
+ JSXGenerationParams,
5
+ JSXGenerationOptions,
6
+ } from '../../../src/node-handlers/node-to-jsx/types'
7
+ import { UIDLAttributeValue, UIDLDependency, UIDLElementNode } from '@teleporthq/teleport-types'
8
+
9
+ const CALENDARKIT_DEPENDENCY: UIDLDependency = {
10
+ type: 'package',
11
+ path: 'calendarkit-basic',
12
+ version: '1.1.0',
13
+ meta: {
14
+ namedImport: true,
15
+ },
16
+ }
17
+
18
+ // Mirrors the post-resolver shape produced by the GUI's calendar-to-uidl
19
+ // mapper: elementType is the React component name, dependency is embedded.
20
+ const calendarNode = (
21
+ attrs: Record<string, UIDLAttributeValue>,
22
+ dependency: UIDLDependency = CALENDARKIT_DEPENDENCY,
23
+ elementType = 'BasicScheduler'
24
+ ): UIDLElementNode => ({
25
+ type: 'element',
26
+ content: {
27
+ key: 'calendar',
28
+ elementType,
29
+ dependency,
30
+ attrs,
31
+ children: [],
32
+ },
33
+ })
34
+
35
+ const buildParams = (): JSXGenerationParams => ({
36
+ dependencies: {},
37
+ propDefinitions: {},
38
+ stateDefinitions: {},
39
+ globalStateDefinitions: {},
40
+ nodesLookup: {},
41
+ windowImports: {},
42
+ localeReferences: [],
43
+ localeAttributeReferences: [],
44
+ globalReferences: [],
45
+ globalStateReferences: [],
46
+ hoistedConstants: [],
47
+ })
48
+
49
+ const options: JSXGenerationOptions = {
50
+ dynamicReferencePrefixMap: {
51
+ prop: 'props',
52
+ state: '',
53
+ local: '',
54
+ },
55
+ }
56
+
57
+ const getEventsAttr = (element: types.JSXElement): types.JSXAttribute | undefined =>
58
+ element.openingElement.attributes.find(
59
+ (attr): attr is types.JSXAttribute =>
60
+ attr.type === 'JSXAttribute' &&
61
+ attr.name.type === 'JSXIdentifier' &&
62
+ attr.name.name === 'events'
63
+ )
64
+
65
+ // Asserts the `.map((e) => ({ ...e, start: new Date(e.start), end: new Date(e.end) }))`
66
+ // wrapper and returns the expression being mapped over (the guard or the array).
67
+ const expectEventsReviver = (eventsAttr: types.JSXAttribute): types.Expression => {
68
+ expect(eventsAttr.value?.type).toBe('JSXExpressionContainer')
69
+ const expression = (eventsAttr.value as types.JSXExpressionContainer).expression
70
+
71
+ expect(expression.type).toBe('CallExpression')
72
+ const mapCall = expression as types.CallExpression
73
+ expect(mapCall.callee.type).toBe('MemberExpression')
74
+ const mapCallee = mapCall.callee as types.MemberExpression
75
+ expect((mapCallee.property as types.Identifier).name).toBe('map')
76
+
77
+ const arrow = mapCall.arguments[0] as types.ArrowFunctionExpression
78
+ expect(arrow.type).toBe('ArrowFunctionExpression')
79
+ const body = arrow.body as types.ObjectExpression
80
+ expect(body.type).toBe('ObjectExpression')
81
+ expect(body.properties[0].type).toBe('SpreadElement')
82
+
83
+ const dateProps = body.properties.slice(1) as types.ObjectProperty[]
84
+ const propNames = dateProps.map((prop) => (prop.key as types.Identifier).name)
85
+ expect(propNames).toEqual(['start', 'end'])
86
+ dateProps.forEach((prop) => {
87
+ expect(prop.value.type).toBe('NewExpression')
88
+ expect(((prop.value as types.NewExpression).callee as types.Identifier).name).toBe('Date')
89
+ })
90
+
91
+ return mapCallee.object as types.Expression
92
+ }
93
+
94
+ const expectArrayIsArrayGuard = (mapSource: types.Expression) => {
95
+ expect(mapSource.type).toBe('ConditionalExpression')
96
+ const guard = mapSource as types.ConditionalExpression
97
+ expect(guard.test.type).toBe('CallExpression')
98
+ const isArrayCall = guard.test as types.CallExpression
99
+ const isArrayCallee = isArrayCall.callee as types.MemberExpression
100
+ expect((isArrayCallee.object as types.Identifier).name).toBe('Array')
101
+ expect((isArrayCallee.property as types.Identifier).name).toBe('isArray')
102
+ expect(guard.alternate.type).toBe('ArrayExpression')
103
+ expect((guard.alternate as types.ArrayExpression).elements.length).toBe(0)
104
+ }
105
+
106
+ describe('calendarkit-basic BasicScheduler events revival', () => {
107
+ it('wraps a static events array with the Date revival map (no redundant guard)', () => {
108
+ const node = calendarNode({
109
+ view: { type: 'static', content: 'month' },
110
+ events: {
111
+ type: 'static',
112
+ content: [
113
+ { id: '1', title: 'Standup', start: '2026-06-10T09:00:00', end: '2026-06-10T09:30:00' },
114
+ ],
115
+ } as unknown as UIDLAttributeValue,
116
+ })
117
+
118
+ const result = generateJSXSyntax(node, buildParams(), options) as types.JSXElement
119
+ const eventsAttr = getEventsAttr(result)
120
+
121
+ expect(eventsAttr).toBeDefined()
122
+ const mapSource = expectEventsReviver(eventsAttr as types.JSXAttribute)
123
+ expect(mapSource.type).toBe('ArrayExpression')
124
+ })
125
+
126
+ it('wraps a dynamic state-bound events reference with the Array.isArray guard', () => {
127
+ const node = calendarNode({
128
+ events: {
129
+ type: 'dynamic',
130
+ content: { referenceType: 'state', id: 'calendarEvents' },
131
+ },
132
+ })
133
+
134
+ const result = generateJSXSyntax(node, buildParams(), options) as types.JSXElement
135
+ const eventsAttr = getEventsAttr(result)
136
+
137
+ expect(eventsAttr).toBeDefined()
138
+ const mapSource = expectEventsReviver(eventsAttr as types.JSXAttribute)
139
+ expectArrayIsArrayGuard(mapSource)
140
+
141
+ const guard = mapSource as types.ConditionalExpression
142
+ const isArrayArg = (guard.test as types.CallExpression).arguments[0] as types.Identifier
143
+ expect(isArrayArg.name).toBe('calendarEvents')
144
+ })
145
+
146
+ it('leaves the element untouched when events is missing — the library defaults to []', () => {
147
+ const node = calendarNode({
148
+ view: { type: 'static', content: 'week' },
149
+ })
150
+
151
+ const result = generateJSXSyntax(node, buildParams(), options) as types.JSXElement
152
+
153
+ expect(getEventsAttr(result)).toBeUndefined()
154
+ })
155
+
156
+ it('does not touch events attrs on elements from other packages', () => {
157
+ const node = calendarNode(
158
+ {
159
+ events: {
160
+ type: 'dynamic',
161
+ content: { referenceType: 'state', id: 'calendarEvents' },
162
+ },
163
+ },
164
+ { type: 'package', path: 'some-other-lib', version: '1.0.0' },
165
+ 'OtherScheduler'
166
+ )
167
+
168
+ const result = generateJSXSyntax(node, buildParams(), options) as types.JSXElement
169
+ const eventsAttr = getEventsAttr(result)
170
+
171
+ expect(eventsAttr).toBeDefined()
172
+ const expression = ((eventsAttr as types.JSXAttribute).value as types.JSXExpressionContainer)
173
+ .expression
174
+ expect(expression.type).not.toBe('CallExpression')
175
+ })
176
+ })
@@ -0,0 +1,79 @@
1
+ import * as types from '@babel/types'
2
+ import generator from '@babel/generator'
3
+ import { elementNode, staticNode } from '@teleporthq/teleport-uidl-builders'
4
+ import { UIDLNode } from '@teleporthq/teleport-types'
5
+ import generateJSXSyntax from '../../../src/node-handlers/node-to-jsx'
6
+ import {
7
+ JSXGenerationParams,
8
+ JSXGenerationOptions,
9
+ } from '../../../src/node-handlers/node-to-jsx/types'
10
+
11
+ /**
12
+ * A repeater over the `ecommerce` global carries no array path — the editor's
13
+ * mapper keeps only the global's id — so the generator picks the array from the
14
+ * repeater's render-prop identifier. These pin that table: each e-commerce list
15
+ * the editor builds must land on the context array it was bound to, and an
16
+ * identifier the table does not know still means the cart's items.
17
+ */
18
+
19
+ const params = (): JSXGenerationParams => ({
20
+ dependencies: {},
21
+ propDefinitions: {},
22
+ stateDefinitions: {},
23
+ globalStateDefinitions: {},
24
+ nodesLookup: {},
25
+ windowImports: {},
26
+ localeReferences: [],
27
+ localeAttributeReferences: [],
28
+ globalReferences: [],
29
+ globalStateReferences: [],
30
+ hoistedConstants: [],
31
+ })
32
+
33
+ const options: JSXGenerationOptions = {
34
+ dynamicReferencePrefixMap: {
35
+ prop: 'props',
36
+ state: '',
37
+ local: '',
38
+ },
39
+ }
40
+
41
+ const generate = (renderPropIdentifier: string): string => {
42
+ const repeater = {
43
+ type: 'cms-list-repeater',
44
+ content: {
45
+ elementType: 'Repeater',
46
+ name: 'repeater',
47
+ key: 'repeater',
48
+ renderPropIdentifier,
49
+ source: 'ecommerce',
50
+ nodes: {
51
+ list: elementNode('container', {}, [staticNode('row')]),
52
+ },
53
+ },
54
+ } as unknown as UIDLNode
55
+ const tree = elementNode('container', {}, [repeater])
56
+ return generator(generateJSXSyntax(tree, params(), options) as types.JSXElement).code
57
+ }
58
+
59
+ describe('cms-list-repeater over the ecommerce global', () => {
60
+ it('lists the shipping methods a checkout binds, not the cart items', () => {
61
+ expect(generate('shippingMethod')).toContain('items={ecommerce?.Cart?.shippingOptions || []}')
62
+ })
63
+
64
+ it('lists the discount sets a checkout offers, in both fulfilment shapes', () => {
65
+ expect(generate('discountOption')).toContain('items={ecommerce?.Cart?.discountOptions || []}')
66
+ expect(generate('discountOptionPickup')).toContain(
67
+ 'items={ecommerce?.Cart?.discountOptionsPickup || []}'
68
+ )
69
+ })
70
+
71
+ it('keeps the payment-provider and store-location lists on their arrays', () => {
72
+ expect(generate('paymentProvider')).toContain('items={ecommerce?.paymentProviders || []}')
73
+ expect(generate('storeLocation')).toContain('items={ecommerce?.storeLocations || []}')
74
+ })
75
+
76
+ it('reads an identifier it does not know as the cart items', () => {
77
+ expect(generate('orderItem')).toContain('items={ecommerce?.Cart?.items || []}')
78
+ })
79
+ })
@@ -0,0 +1,122 @@
1
+ import * as types from '@babel/types'
2
+ import generator from '@babel/generator'
3
+ import { elementNode, staticNode } from '@teleporthq/teleport-uidl-builders'
4
+ import { UIDLNode } from '@teleporthq/teleport-types'
5
+ import generateJSXSyntax from '../../../src/node-handlers/node-to-jsx'
6
+ import {
7
+ JSXGenerationParams,
8
+ JSXGenerationOptions,
9
+ } from '../../../src/node-handlers/node-to-jsx/types'
10
+
11
+ /**
12
+ * A repeater fed by a plain STATE has no `DataProvider` — nothing hoists its
13
+ * loading branch onto a `renderLoading` slot, so before `isLoading` existed the
14
+ * branch was generated and then thrown away, and a page that was still fetching
15
+ * showed its EMPTY branch ("no results") for the whole request.
16
+ *
17
+ * These tests pin both halves of the contract: the branch renders when the node
18
+ * carries the signal, and the emitted code is UNCHANGED when it does not — which
19
+ * is what keeps every data-source-backed mapper (products list, blog) on exactly
20
+ * the path it was on.
21
+ */
22
+
23
+ const params = (): JSXGenerationParams => ({
24
+ dependencies: {},
25
+ propDefinitions: {},
26
+ stateDefinitions: {},
27
+ globalStateDefinitions: {},
28
+ nodesLookup: {},
29
+ windowImports: {},
30
+ localeReferences: [],
31
+ localeAttributeReferences: [],
32
+ globalReferences: [],
33
+ globalStateReferences: [],
34
+ hoistedConstants: [],
35
+ })
36
+
37
+ const options: JSXGenerationOptions = {
38
+ dynamicReferencePrefixMap: {
39
+ prop: 'props',
40
+ state: '',
41
+ local: '',
42
+ },
43
+ }
44
+
45
+ // Two children, so `unwrapTransparentRepeaterRoot` leaves the fragment in place —
46
+ // the shape a listing's skeleton branch actually has.
47
+ const loadingBranch = () =>
48
+ elementNode('fragment', {}, [
49
+ elementNode('container', {}, [staticNode('skeleton one')]),
50
+ elementNode('container', {}, [staticNode('skeleton two')]),
51
+ ])
52
+
53
+ const repeaterNode = (isLoading?: { type: 'expr'; content: string }): UIDLNode =>
54
+ ({
55
+ type: 'cms-list-repeater',
56
+ content: {
57
+ elementType: 'Repeater',
58
+ name: 'repeater',
59
+ key: 'repeater',
60
+ renderPropIdentifier: 'favouriteProduct',
61
+ source: 'favouriteProducts',
62
+ nodes: {
63
+ list: elementNode('container', {}, [staticNode('row')]),
64
+ empty: elementNode('container', {}, [staticNode('No products available')]),
65
+ loading: loadingBranch(),
66
+ },
67
+ ...(isLoading ? { isLoading } : {}),
68
+ },
69
+ } as unknown as UIDLNode)
70
+
71
+ // The repeater is generated through its PARENT, which is where a non-JSX result
72
+ // gets wrapped in a `{ … }` expression container. Generating it standalone would
73
+ // not exercise that.
74
+ const generate = (isLoading?: { type: 'expr'; content: string }): string => {
75
+ const tree = elementNode('container', {}, [repeaterNode(isLoading)])
76
+ const ast = generateJSXSyntax(tree, params(), options) as types.JSXElement
77
+ return generator(ast).code
78
+ }
79
+
80
+ describe('cms-list-repeater loading branch', () => {
81
+ it('renders the loading branch instead of the repeater when isLoading is set', () => {
82
+ const code = generate({ type: 'expr', content: "isLoadingFavourites || ''" })
83
+
84
+ // `String(x) === "true"`, never a bare truthiness test: the flag is a string
85
+ // state, and the STRING `'false'` is truthy.
86
+ expect(code).toContain(`String(isLoadingFavourites || '') === "true"`)
87
+ expect(code).toContain('skeleton one')
88
+ expect(code).toContain('skeleton two')
89
+ // Still a ternary, not a replacement — the repeater is the other branch.
90
+ expect(code).toContain('<Repeater')
91
+ expect(code.indexOf('skeleton one')).toBeLessThan(code.indexOf('<Repeater'))
92
+ })
93
+
94
+ it('accepts a static flag', () => {
95
+ expect(generate()).not.toContain('String(')
96
+
97
+ const tree = elementNode('container', {}, [
98
+ {
99
+ ...(repeaterNode() as { content: unknown }),
100
+ content: {
101
+ ...(repeaterNode() as unknown as { content: Record<string, unknown> }).content,
102
+ isLoading: { type: 'static', content: 'true' },
103
+ },
104
+ } as unknown as UIDLNode,
105
+ ])
106
+ const code = generator(generateJSXSyntax(tree, params(), options) as types.JSXElement).code
107
+
108
+ expect(code).toContain(`String("true") === "true"`)
109
+ })
110
+
111
+ it('leaves a repeater WITHOUT isLoading exactly as it was', () => {
112
+ const code = generate()
113
+
114
+ // ⛔ The regression guard for every data-source-backed mapper: its loading
115
+ // branch is hoisted onto the `DataProvider`, so emitting it here as well
116
+ // would draw the same branch twice.
117
+ expect(code).not.toContain('String(')
118
+ expect(code).not.toContain('skeleton one')
119
+ expect(code).toContain('<Repeater')
120
+ expect(code).toContain('renderEmpty')
121
+ })
122
+ })