@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
@@ -1,13 +1,14 @@
1
1
  import * as types from '@babel/types'
2
2
  import componentUIDLSample from '../../../../../examples/test-samples/component-sample.json'
3
3
  import generateJSXSyntax from '../../../src/node-handlers/node-to-jsx'
4
+ import generate from '@babel/generator'
4
5
 
5
- import { slotNode, elementNode, staticNode } from '@teleporthq/teleport-uidl-builders'
6
+ import { slotNode, elementNode, staticNode, dynamicNode } from '@teleporthq/teleport-uidl-builders'
6
7
  import {
7
8
  JSXGenerationParams,
8
9
  JSXGenerationOptions,
9
10
  } from '../../../src/node-handlers/node-to-jsx/types'
10
- import { ComponentUIDL } from '@teleporthq/teleport-types'
11
+ import { ComponentUIDL, UIDLNode } from '@teleporthq/teleport-types'
11
12
 
12
13
  const uidl = componentUIDLSample as ComponentUIDL
13
14
 
@@ -16,7 +17,14 @@ describe('generateJSXSyntax', () => {
16
17
  dependencies: {},
17
18
  propDefinitions: uidl.propDefinitions || {},
18
19
  stateDefinitions: uidl.stateDefinitions || {},
20
+ globalStateDefinitions: {},
19
21
  nodesLookup: {},
22
+ windowImports: {},
23
+ localeReferences: [],
24
+ localeAttributeReferences: [],
25
+ globalReferences: [],
26
+ globalStateReferences: [],
27
+ hoistedConstants: [],
20
28
  }
21
29
 
22
30
  const options: JSXGenerationOptions = {
@@ -24,7 +32,6 @@ describe('generateJSXSyntax', () => {
24
32
  prop: 'props',
25
33
  state: '',
26
34
  local: '',
27
- ctx: '',
28
35
  },
29
36
  }
30
37
 
@@ -39,6 +46,38 @@ describe('generateJSXSyntax', () => {
39
46
  })
40
47
  })
41
48
 
49
+ describe('dynamic style values', () => {
50
+ it('keeps a custom property key verbatim in the inline style object', () => {
51
+ // React writes `--*` keys through `style.setProperty`; the camel rule
52
+ // turned `--tq-panel-display` into `-TqPanelDisplay`, which React neither
53
+ // renders nor updates — the state-driven panel never opened.
54
+ const node = elementNode('container', {}, [], undefined, {
55
+ '--tq-panel-display': dynamicNode('state', 'panelDisplay'),
56
+ 'background-color': dynamicNode('state', 'panelColor'),
57
+ })
58
+ const result = generateJSXSyntax(
59
+ node,
60
+ {
61
+ ...params,
62
+ stateDefinitions: {
63
+ panelDisplay: { type: 'string', defaultValue: 'none' },
64
+ panelColor: { type: 'string', defaultValue: 'red' },
65
+ },
66
+ },
67
+ options
68
+ )
69
+ const styleAttr = (result as types.JSXElement).openingElement.attributes.find(
70
+ (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'style'
71
+ ) as types.JSXAttribute
72
+ const object = (styleAttr.value as types.JSXExpressionContainer)
73
+ .expression as types.ObjectExpression
74
+ const keys = object.properties.map(
75
+ (prop) => ((prop as types.ObjectProperty).key as types.StringLiteral).value
76
+ )
77
+ expect(keys).toEqual(['--tq-panel-display', 'backgroundColor'])
78
+ })
79
+ })
80
+
42
81
  describe('slot node', () => {
43
82
  it('returns a props.children expression', () => {
44
83
  const node = elementNode('container', {}, [slotNode()])
@@ -104,4 +143,681 @@ describe('generateJSXSyntax', () => {
104
143
  expect((nameAttr.value as types.StringLiteral).value).toBe('hole')
105
144
  })
106
145
  })
146
+
147
+ describe('markdown-node with dynamic/expr children (CMS rich text)', () => {
148
+ const buildParams = (): JSXGenerationParams => ({
149
+ dependencies: {},
150
+ propDefinitions: {},
151
+ stateDefinitions: {},
152
+ globalStateDefinitions: {},
153
+ nodesLookup: {},
154
+ windowImports: {},
155
+ localeReferences: [],
156
+ localeAttributeReferences: [],
157
+ globalReferences: [],
158
+ globalStateReferences: [],
159
+ hoistedConstants: [],
160
+ })
161
+
162
+ // Asserts the element is a <Markdown>{<expr> || ''}</Markdown> tag from markdown-to-jsx,
163
+ // which renders both markdown syntax and embedded HTML.
164
+ const expectMarkdownTag = (result: types.JSXElement, localParams: JSXGenerationParams) => {
165
+ expect((result.openingElement.name as types.JSXIdentifier).name).toBe('Markdown')
166
+
167
+ const container = result.children[0] as types.JSXExpressionContainer
168
+ expect(container.type).toBe('JSXExpressionContainer')
169
+ // dynamic content is guarded with `<expr> || ''` so a nullish value renders empty
170
+ expect((container.expression as types.LogicalExpression).operator).toBe('||')
171
+
172
+ expect(localParams.dependencies.Markdown).toEqual({
173
+ type: 'package',
174
+ path: 'markdown-to-jsx',
175
+ version: '7.7.12',
176
+ })
177
+ }
178
+
179
+ it('renders an expr child through the markdown-to-jsx <Markdown> component', () => {
180
+ const localParams = buildParams()
181
+ const node = elementNode('markdown-node', {}, [
182
+ { type: 'expr', content: 'props.cmsRichtext' } as UIDLNode,
183
+ ])
184
+
185
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
186
+
187
+ expectMarkdownTag(result, localParams)
188
+ })
189
+
190
+ it('renders a dynamic child through the markdown-to-jsx <Markdown> component', () => {
191
+ const localParams = buildParams()
192
+ const node = elementNode('markdown-node', {}, [dynamicNode('prop', 'cmsRichtext')])
193
+
194
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
195
+
196
+ expectMarkdownTag(result, localParams)
197
+ })
198
+
199
+ it('leaves a non-markdown div with an expr child as a plain value', () => {
200
+ const localParams = buildParams()
201
+ const node = elementNode('container', {}, [
202
+ { type: 'expr', content: 'props.cmsRichtext' } as UIDLNode,
203
+ ])
204
+
205
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
206
+
207
+ // generic expression handling wraps in a typeof/JSON.stringify guard, no markdown render
208
+ const container = result.children[0] as types.JSXExpressionContainer
209
+ expect(container.expression.type).toBe('ConditionalExpression')
210
+ expect((result.openingElement.name as types.JSXIdentifier).name).not.toBe('Markdown')
211
+ expect(localParams.dependencies.Markdown).toBeUndefined()
212
+ })
213
+ })
214
+
215
+ describe('renderingConditions on elements', () => {
216
+ it('wraps the element with a logical `&&` when renderingConditions are set', () => {
217
+ const ownerContainer = elementNode('container', {}, [])
218
+ ownerContainer.content.renderingConditions = {
219
+ reference: {
220
+ type: 'dynamic',
221
+ content: {
222
+ referenceType: 'state',
223
+ id: 'profileViewer',
224
+ refPath: ['currentUserId'],
225
+ },
226
+ },
227
+ condition: {
228
+ conditions: [
229
+ {
230
+ operation: '===',
231
+ operand: {
232
+ type: 'dynamic',
233
+ content: {
234
+ referenceType: 'local',
235
+ id: 'id',
236
+ refPath: ['id'],
237
+ },
238
+ },
239
+ },
240
+ ],
241
+ },
242
+ } as never
243
+
244
+ const wrappingParent = elementNode('container', {}, [ownerContainer])
245
+ const detailsParams: JSXGenerationParams = {
246
+ ...params,
247
+ globalReferences: [],
248
+ detailsPageExposeAsName: 'User',
249
+ }
250
+ const result = generateJSXSyntax(wrappingParent, detailsParams, options)
251
+
252
+ const expressionChild = result.children[0] as types.JSXExpressionContainer
253
+ expect(expressionChild.type).toBe('JSXExpressionContainer')
254
+
255
+ const logical = expressionChild.expression as types.LogicalExpression
256
+ expect(logical.type).toBe('LogicalExpression')
257
+ expect(logical.operator).toBe('&&')
258
+
259
+ // The wrapped content is the original <container/>.
260
+ const wrappedContent = logical.right as types.JSXElement
261
+ expect(wrappedContent.type).toBe('JSXElement')
262
+ expect((wrappedContent.openingElement.name as types.JSXIdentifier).name).toBe('container')
263
+ })
264
+
265
+ const buildTwoReferenceConditions = (matchingCriteria: string) =>
266
+ ({
267
+ reference: {
268
+ type: 'dynamic',
269
+ content: { referenceType: 'state', id: 'newsletterOfferVisible' },
270
+ },
271
+ condition: {
272
+ conditions: [
273
+ { operation: '===', operand: true },
274
+ {
275
+ operation: '===',
276
+ operand: false,
277
+ reference: {
278
+ type: 'dynamic',
279
+ content: { referenceType: 'state', id: 'userIsLoggedIn' },
280
+ },
281
+ },
282
+ ],
283
+ matchingCriteria,
284
+ },
285
+ } as never)
286
+
287
+ it('compares each entry against its own reference when one is set (AND chain)', () => {
288
+ const ownerContainer = elementNode('container', {}, [])
289
+ ownerContainer.content.renderingConditions = buildTwoReferenceConditions('all')
290
+
291
+ const wrappingParent = elementNode('container', {}, [ownerContainer])
292
+ const twoStateParams: JSXGenerationParams = {
293
+ ...params,
294
+ stateDefinitions: {
295
+ newsletterOfferVisible: { type: 'boolean', defaultValue: true },
296
+ userIsLoggedIn: { type: 'boolean', defaultValue: false },
297
+ },
298
+ globalReferences: [],
299
+ globalStateReferences: [],
300
+ }
301
+ const result = generateJSXSyntax(wrappingParent, twoStateParams, options)
302
+
303
+ const expressionChild = result.children[0] as types.JSXExpressionContainer
304
+ const outerLogical = expressionChild.expression as types.LogicalExpression
305
+ expect(outerLogical.operator).toBe('&&')
306
+
307
+ // Left side is the chained condition pair, joined with `&&` for 'all'.
308
+ const conditionChain = outerLogical.left as types.LogicalExpression
309
+ expect(conditionChain.type).toBe('LogicalExpression')
310
+ expect(conditionChain.operator).toBe('&&')
311
+
312
+ // `=== true` compiles to the bare identifier, `=== false` to `!identifier`
313
+ // — i.e. `newsletterOfferVisible && !userIsLoggedIn`, the wild-log case.
314
+ const firstComparison = conditionChain.left as types.Identifier
315
+ const secondComparison = conditionChain.right as types.UnaryExpression
316
+ expect(firstComparison.name).toBe('newsletterOfferVisible')
317
+ expect(secondComparison.operator).toBe('!')
318
+ expect((secondComparison.argument as types.Identifier).name).toBe('userIsLoggedIn')
319
+ })
320
+
321
+ it('chains mixed-reference entries with `||` when matchingCriteria is not "all"', () => {
322
+ const ownerContainer = elementNode('container', {}, [])
323
+ ownerContainer.content.renderingConditions = buildTwoReferenceConditions('||')
324
+
325
+ const wrappingParent = elementNode('container', {}, [ownerContainer])
326
+ const twoStateParams: JSXGenerationParams = {
327
+ ...params,
328
+ stateDefinitions: {
329
+ newsletterOfferVisible: { type: 'boolean', defaultValue: true },
330
+ userIsLoggedIn: { type: 'boolean', defaultValue: false },
331
+ },
332
+ globalReferences: [],
333
+ globalStateReferences: [],
334
+ }
335
+ const result = generateJSXSyntax(wrappingParent, twoStateParams, options)
336
+
337
+ const expressionChild = result.children[0] as types.JSXExpressionContainer
338
+ const outerLogical = expressionChild.expression as types.LogicalExpression
339
+ const conditionChain = outerLogical.left as types.LogicalExpression
340
+ expect(conditionChain.operator).toBe('||')
341
+ expect((conditionChain.left as types.Identifier).name).toBe('newsletterOfferVisible')
342
+ expect(
343
+ ((conditionChain.right as types.UnaryExpression).argument as types.Identifier).name
344
+ ).toBe('userIsLoggedIn')
345
+ })
346
+
347
+ it('mixes a prop-referencing entry into a state-anchored chain with the right prefix', () => {
348
+ const ownerContainer = elementNode('container', {}, [])
349
+ ownerContainer.content.renderingConditions = {
350
+ reference: {
351
+ type: 'dynamic',
352
+ content: { referenceType: 'state', id: 'menuOpen' },
353
+ },
354
+ condition: {
355
+ conditions: [
356
+ { operation: '===', operand: true },
357
+ {
358
+ operation: '===',
359
+ operand: 'admin',
360
+ reference: {
361
+ type: 'dynamic',
362
+ content: { referenceType: 'prop', id: 'role' },
363
+ },
364
+ },
365
+ ],
366
+ matchingCriteria: 'all',
367
+ },
368
+ } as never
369
+
370
+ const wrappingParent = elementNode('container', {}, [ownerContainer])
371
+ const mixedParams: JSXGenerationParams = {
372
+ ...params,
373
+ stateDefinitions: { menuOpen: { type: 'boolean', defaultValue: false } },
374
+ propDefinitions: { role: { type: 'string', defaultValue: 'guest' } },
375
+ globalReferences: [],
376
+ globalStateReferences: [],
377
+ }
378
+ const result = generateJSXSyntax(wrappingParent, mixedParams, options)
379
+
380
+ const expressionChild = result.children[0] as types.JSXExpressionContainer
381
+ const conditionChain = (expressionChild.expression as types.LogicalExpression)
382
+ .left as types.LogicalExpression
383
+
384
+ // `=== true` on the boolean state compiles to the bare identifier.
385
+ const stateComparison = conditionChain.left as types.Identifier
386
+ expect(stateComparison.name).toBe('menuOpen')
387
+
388
+ // The prop entry rides the `props.` prefix from the prefix map.
389
+ const propComparison = conditionChain.right as types.BinaryExpression
390
+ const propMember = propComparison.left as types.MemberExpression
391
+ expect((propMember.object as types.Identifier).name).toBe('props')
392
+ expect((propMember.property as types.Identifier).name).toBe('role')
393
+ expect((propComparison.right as types.StringLiteral).value).toBe('admin')
394
+ })
395
+
396
+ it('reads a URL search-param entry off the page address (router.query.<key>)', () => {
397
+ // A listing's categories overview: shown while no category is picked AND no
398
+ // search is typed — the search lives in `?searchKeyword=`, not in a state.
399
+ const ownerContainer = elementNode('container', {}, [])
400
+ ownerContainer.content.renderingConditions = {
401
+ reference: {
402
+ type: 'dynamic',
403
+ content: { referenceType: 'state', id: 'selectedCategory' },
404
+ },
405
+ condition: {
406
+ conditions: [
407
+ { operation: 'isEmpty' },
408
+ {
409
+ operation: 'isEmpty',
410
+ reference: {
411
+ type: 'dynamic',
412
+ content: {
413
+ referenceType: 'urlSearchParams',
414
+ id: 'searchKeyword',
415
+ refPath: ['searchKeyword'],
416
+ },
417
+ },
418
+ },
419
+ ],
420
+ matchingCriteria: 'all',
421
+ },
422
+ } as never
423
+
424
+ const wrappingParent = elementNode('container', {}, [ownerContainer])
425
+ const listingParams: JSXGenerationParams = {
426
+ ...params,
427
+ stateDefinitions: { selectedCategory: { type: 'string', defaultValue: '' } },
428
+ globalReferences: [],
429
+ globalStateReferences: [],
430
+ }
431
+ const result = generateJSXSyntax(wrappingParent, listingParams, options)
432
+
433
+ const expressionChild = result.children[0] as types.JSXExpressionContainer
434
+ const conditionChain = (expressionChild.expression as types.LogicalExpression).left
435
+ expect(generate(conditionChain).code).toBe(
436
+ '(selectedCategory || []).length === 0 && (router.query.searchKeyword || []).length === 0'
437
+ )
438
+ })
439
+
440
+ it('tracks a globalState per-entry reference on params for context wiring', () => {
441
+ const ownerContainer = elementNode('container', {}, [])
442
+ ownerContainer.content.renderingConditions = {
443
+ reference: {
444
+ type: 'dynamic',
445
+ content: { referenceType: 'state', id: 'menuOpen' },
446
+ },
447
+ condition: {
448
+ conditions: [
449
+ { operation: '===', operand: true },
450
+ {
451
+ operation: '===',
452
+ operand: true,
453
+ reference: {
454
+ type: 'dynamic',
455
+ content: { referenceType: 'globalState', id: 'darkMode' },
456
+ },
457
+ },
458
+ ],
459
+ matchingCriteria: 'all',
460
+ },
461
+ } as never
462
+
463
+ const wrappingParent = elementNode('container', {}, [ownerContainer])
464
+ const globalStateParams: JSXGenerationParams = {
465
+ ...params,
466
+ stateDefinitions: { menuOpen: { type: 'boolean', defaultValue: false } },
467
+ globalStateDefinitions: {
468
+ darkMode: { id: 'darkMode', name: 'darkMode', type: 'boolean', defaultValue: false },
469
+ },
470
+ globalReferences: [],
471
+ globalStateReferences: [],
472
+ }
473
+ generateJSXSyntax(wrappingParent, globalStateParams, options)
474
+
475
+ expect(globalStateParams.globalStateReferences).toEqual([
476
+ { id: 'darkMode', name: 'darkMode' },
477
+ ])
478
+ })
479
+
480
+ it('nests a group entry as a parenthesized sub-chain — (a && b) || c', () => {
481
+ const ownerContainer = elementNode('container', {}, [])
482
+ ownerContainer.content.renderingConditions = {
483
+ reference: {
484
+ type: 'dynamic',
485
+ content: { referenceType: 'state', id: 'a' },
486
+ },
487
+ condition: {
488
+ conditions: [
489
+ {
490
+ conditions: [
491
+ { operation: '===', operand: 1 },
492
+ {
493
+ operation: '===',
494
+ operand: 3,
495
+ reference: {
496
+ type: 'dynamic',
497
+ content: { referenceType: 'state', id: 'b' },
498
+ },
499
+ },
500
+ ],
501
+ matchingCriteria: 'all',
502
+ },
503
+ {
504
+ operation: '===',
505
+ operand: 5,
506
+ reference: {
507
+ type: 'dynamic',
508
+ content: { referenceType: 'state', id: 'c' },
509
+ },
510
+ },
511
+ ],
512
+ matchingCriteria: '||',
513
+ },
514
+ } as never
515
+
516
+ const wrappingParent = elementNode('container', {}, [ownerContainer])
517
+ const groupedParams: JSXGenerationParams = {
518
+ ...params,
519
+ stateDefinitions: {
520
+ a: { type: 'number', defaultValue: 1 },
521
+ b: { type: 'number', defaultValue: 3 },
522
+ c: { type: 'number', defaultValue: 0 },
523
+ },
524
+ globalReferences: [],
525
+ globalStateReferences: [],
526
+ }
527
+ const result = generateJSXSyntax(wrappingParent, groupedParams, options)
528
+
529
+ const expressionChild = result.children[0] as types.JSXExpressionContainer
530
+ const conditionChain = (expressionChild.expression as types.LogicalExpression)
531
+ .left as types.LogicalExpression
532
+
533
+ // (a === 1 && b === 3) || c === 5 — the group nests as the left branch,
534
+ // which the printer emits parenthesized.
535
+ expect(conditionChain.operator).toBe('||')
536
+
537
+ const groupChain = conditionChain.left as types.LogicalExpression
538
+ expect(groupChain.type).toBe('LogicalExpression')
539
+ expect(groupChain.operator).toBe('&&')
540
+ const aComparison = groupChain.left as types.BinaryExpression
541
+ const bComparison = groupChain.right as types.BinaryExpression
542
+ expect((aComparison.left as types.Identifier).name).toBe('a')
543
+ expect((aComparison.right as types.NumericLiteral).value).toBe(1)
544
+ expect((bComparison.left as types.Identifier).name).toBe('b')
545
+ expect((bComparison.right as types.NumericLiteral).value).toBe(3)
546
+
547
+ const cComparison = conditionChain.right as types.BinaryExpression
548
+ expect((cComparison.left as types.Identifier).name).toBe('c')
549
+ expect((cComparison.right as types.NumericLiteral).value).toBe(5)
550
+ })
551
+
552
+ it('tracks references from leaves nested inside groups', () => {
553
+ const ownerContainer = elementNode('container', {}, [])
554
+ ownerContainer.content.renderingConditions = {
555
+ reference: {
556
+ type: 'dynamic',
557
+ content: { referenceType: 'state', id: 'menuOpen' },
558
+ },
559
+ condition: {
560
+ conditions: [
561
+ { operation: '===', operand: true },
562
+ {
563
+ conditions: [
564
+ {
565
+ operation: '===',
566
+ operand: true,
567
+ reference: {
568
+ type: 'dynamic',
569
+ content: { referenceType: 'globalState', id: 'darkMode' },
570
+ },
571
+ },
572
+ ],
573
+ },
574
+ ],
575
+ matchingCriteria: 'all',
576
+ },
577
+ } as never
578
+
579
+ const wrappingParent = elementNode('container', {}, [ownerContainer])
580
+ const nestedTrackingParams: JSXGenerationParams = {
581
+ ...params,
582
+ stateDefinitions: { menuOpen: { type: 'boolean', defaultValue: false } },
583
+ globalStateDefinitions: {
584
+ darkMode: { id: 'darkMode', name: 'darkMode', type: 'boolean', defaultValue: false },
585
+ },
586
+ globalReferences: [],
587
+ globalStateReferences: [],
588
+ }
589
+ generateJSXSyntax(wrappingParent, nestedTrackingParams, options)
590
+
591
+ expect(nestedTrackingParams.globalStateReferences).toEqual([
592
+ { id: 'darkMode', name: 'darkMode' },
593
+ ])
594
+ })
595
+ })
596
+
597
+ describe('navlink href via differentiatorValue', () => {
598
+ // The navlink attribute flow is exercised end-to-end in the resolver tests;
599
+ // here we only confirm that an `expr`-typed attribute which mentions
600
+ // `currentUser` registers a global reference so useGlobalContext() is wired
601
+ // up downstream.
602
+ it('tracks currentUser as a global reference when emitted via an expr attr', () => {
603
+ const linkWithDifferentiatorHref = elementNode('a', {
604
+ href: {
605
+ type: 'expr',
606
+ content: '`/profile/' + '$' + '{' + 'currentUser?.id}' + '`',
607
+ },
608
+ })
609
+ const localParams: JSXGenerationParams = {
610
+ ...params,
611
+ globalReferences: [],
612
+ }
613
+ generateJSXSyntax(linkWithDifferentiatorHref, localParams, options)
614
+ expect(localParams.globalReferences).toContain('currentUser')
615
+ })
616
+ })
617
+
618
+ describe('controlled <textarea> must not render children (React SSR contract)', () => {
619
+ const buildParams = (): JSXGenerationParams => ({
620
+ dependencies: {},
621
+ propDefinitions: {},
622
+ stateDefinitions: {},
623
+ globalStateDefinitions: {},
624
+ nodesLookup: {},
625
+ windowImports: {},
626
+ localeReferences: [],
627
+ localeAttributeReferences: [],
628
+ globalReferences: [],
629
+ globalStateReferences: [],
630
+ hoistedConstants: [],
631
+ })
632
+
633
+ it('drops the children of a textarea that has a value attribute', () => {
634
+ // The details/entity binder can add value={{ ctx.field }} to a <textarea>
635
+ // while leaving its original text child in place. React's server renderer
636
+ // throws "If you supply value on a <textarea> you must not supply children",
637
+ // aborting static export — so the children must not be attached.
638
+ const localParams = buildParams()
639
+ const node = elementNode('textarea', { value: dynamicNode('prop', 'description') }, [
640
+ elementNode('span', {}, [dynamicNode('prop', 'description')]),
641
+ ])
642
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
643
+
644
+ expect((result.openingElement.name as types.JSXIdentifier).name).toBe('textarea')
645
+ expect(result.children.length).toBe(0)
646
+ // The child <span> is still generated into nodesLookup so the later
647
+ // styled-jsx traversal doesn't fail with "missing from the template chunk".
648
+ expect(Object.keys(localParams.nodesLookup).length).toBeGreaterThanOrEqual(2)
649
+ })
650
+
651
+ it('drops the children of a textarea that has a defaultValue attribute', () => {
652
+ const localParams = buildParams()
653
+ const node = elementNode('textarea', { defaultValue: dynamicNode('prop', 'description') }, [
654
+ elementNode('span', {}, [staticNode('initial')]),
655
+ ])
656
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
657
+ expect(result.children.length).toBe(0)
658
+ })
659
+
660
+ it('keeps the children of an uncontrolled textarea (no value/defaultValue)', () => {
661
+ const localParams = buildParams()
662
+ const node = elementNode('textarea', {}, [staticNode('initial content')])
663
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
664
+ expect(result.children.length).toBeGreaterThan(0)
665
+ })
666
+
667
+ it('drops a stray dynamic (element/expression) child of an uncontrolled textarea', () => {
668
+ // Run 97ac2650 "Add Note": a textarea whose value binding arrived as a
669
+ // {{ }} CHILD (no value attr) was demoted to an element child — React
670
+ // stringifies it to the literal "[object Object]". The backstop drops the
671
+ // dynamic child so it can never render "[object Object]". (The primary fix
672
+ // hoists the binding to a value= attr at import time.)
673
+ const localParams = buildParams()
674
+ const node = elementNode('textarea', {}, [
675
+ elementNode('span', {}, [dynamicNode('prop', 'noteBody')]),
676
+ ])
677
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
678
+ expect(result.children.length).toBe(0)
679
+ // The dropped child is still generated into nodesLookup (styled-jsx pass).
680
+ expect(Object.keys(localParams.nodesLookup).length).toBeGreaterThanOrEqual(2)
681
+ })
682
+
683
+ it('drops the children of a named textarea controlled by the form-store binding', () => {
684
+ // maybeAddFormStoreFieldBinding injects value={formState['field'] ?? ''}
685
+ // onto named inputs inside a data-store-values-state form AFTER the UIDL
686
+ // attrs are processed — the textarea has NO value in its UIDL attrs but
687
+ // ends up controlled all the same, so its children must also be skipped.
688
+ const localParams = { ...buildParams(), formStoreStateName: 'formValues' }
689
+ const node = elementNode('textarea', { name: staticNode('description') }, [
690
+ staticNode('initial content'),
691
+ ])
692
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
693
+
694
+ const hasInjectedValue = result.openingElement.attributes.some(
695
+ (attr) =>
696
+ attr.type === 'JSXAttribute' &&
697
+ attr.name.type === 'JSXIdentifier' &&
698
+ attr.name.name === 'value'
699
+ )
700
+ expect(hasInjectedValue).toBe(true)
701
+ expect(result.children.length).toBe(0)
702
+ })
703
+ })
704
+
705
+ describe('plan v15 Layer 4 — codegen safety net for junk attrs and corrupt values', () => {
706
+ // Suppress the console.warn that fires when junk attrs are dropped so
707
+ // the test runner output stays clean. Reset between tests.
708
+ let warnSpy: jest.SpyInstance | null = null
709
+ beforeEach(() => {
710
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
711
+ })
712
+ afterEach(() => {
713
+ warnSpy?.mockRestore()
714
+ })
715
+
716
+ const attributeNames = (el: types.JSXElement): string[] =>
717
+ el.openingElement.attributes
718
+ .filter((a): a is types.JSXAttribute => a.type === 'JSXAttribute')
719
+ .map((a) => (a.name as types.JSXIdentifier).name)
720
+
721
+ const styleAttributeOf = (el: types.JSXElement) =>
722
+ el.openingElement.attributes.find(
723
+ (a): a is types.JSXAttribute =>
724
+ a.type === 'JSXAttribute' && (a.name as types.JSXIdentifier).name === 'style'
725
+ )
726
+
727
+ it('turns a text style attribute into the object React wants (error #62 otherwise)', () => {
728
+ const node = elementNode('div', {
729
+ style: { type: 'static', content: 'color: red; margin-top: 4px; --accent: #fff' },
730
+ } as never)
731
+ const result = generateJSXSyntax(node, params, options) as types.JSXElement
732
+ const styleAttr = styleAttributeOf(result)
733
+ expect(styleAttr?.value?.type).toBe('JSXExpressionContainer')
734
+ const expression = (styleAttr?.value as types.JSXExpressionContainer).expression
735
+ expect(expression.type).toBe('ObjectExpression')
736
+ const keys = (expression as types.ObjectExpression).properties.map((property) => {
737
+ const key = (property as types.ObjectProperty).key
738
+ return key.type === 'Identifier' ? key.name : (key as types.StringLiteral).value
739
+ })
740
+ expect(keys).toEqual(['color', 'marginTop', '--accent'])
741
+ })
742
+
743
+ it('drops a style attribute that is not CSS text instead of emitting a string', () => {
744
+ const node = elementNode('div', {
745
+ style: { type: 'static', content: 'nonsense' },
746
+ class: { type: 'static', content: 'kept' },
747
+ } as never)
748
+ const result = generateJSXSyntax(node, params, options) as types.JSXElement
749
+ expect(styleAttributeOf(result)).toBeUndefined()
750
+ expect(attributeNames(result)).toContain('class')
751
+ })
752
+
753
+ it('drops junk-name attribute true="true"', () => {
754
+ const node = elementNode('div', {
755
+ true: { type: 'static', content: 'true' },
756
+ class: { type: 'static', content: 'kept' },
757
+ } as never)
758
+ const result = generateJSXSyntax(node, params, options) as types.JSXElement
759
+ const names = attributeNames(result)
760
+ expect(names).not.toContain('true')
761
+ expect(names).toContain('class')
762
+ })
763
+
764
+ it('drops junk-name attribute false="false"', () => {
765
+ const node = elementNode('div', {
766
+ false: { type: 'static', content: 'false' },
767
+ class: { type: 'static', content: 'kept' },
768
+ } as never)
769
+ const result = generateJSXSyntax(node, params, options) as types.JSXElement
770
+ expect(attributeNames(result)).not.toContain('false')
771
+ })
772
+
773
+ it('drops numeric-name attributes (e.g. 0="x")', () => {
774
+ const node = elementNode('div', {
775
+ '0': { type: 'static', content: 'x' },
776
+ class: { type: 'static', content: 'kept' },
777
+ } as never)
778
+ const result = generateJSXSyntax(node, params, options) as types.JSXElement
779
+ expect(attributeNames(result)).not.toContain('0')
780
+ })
781
+
782
+ it('drops null and undefined name attrs', () => {
783
+ const node = elementNode('div', {
784
+ null: { type: 'static', content: 'x' },
785
+ undefined: { type: 'static', content: 'y' },
786
+ class: { type: 'static', content: 'kept' },
787
+ } as never)
788
+ const result = generateJSXSyntax(node, params, options) as types.JSXElement
789
+ const names = attributeNames(result)
790
+ expect(names).not.toContain('null')
791
+ expect(names).not.toContain('undefined')
792
+ })
793
+
794
+ it('drops entity-escaped binding markers in static attr values (&#123;)', () => {
795
+ const node = elementNode('tq-if', {
796
+ condition: { type: 'static', content: '&#123;&#123;state.X&#125;&#125;' },
797
+ } as never)
798
+ const result = generateJSXSyntax(node, params, options) as types.JSXElement
799
+ expect(attributeNames(result)).not.toContain('condition')
800
+ })
801
+
802
+ it('drops static attr values with truncated `{{` (no closing `}}`)', () => {
803
+ const node = elementNode('tq-if', {
804
+ condition: { type: 'static', content: '{{state.isMobileMenuOpen' },
805
+ } as never)
806
+ const result = generateJSXSyntax(node, params, options) as types.JSXElement
807
+ expect(attributeNames(result)).not.toContain('condition')
808
+ })
809
+
810
+ it('preserves well-formed static attrs (no false positives)', () => {
811
+ const node = elementNode('div', {
812
+ class: { type: 'static', content: 'foo' },
813
+ 'data-config': { type: 'static', content: '{"k":1}' },
814
+ condition: { type: 'static', content: '{{state.X}}' },
815
+ } as never)
816
+ const result = generateJSXSyntax(node, params, options) as types.JSXElement
817
+ const names = attributeNames(result)
818
+ expect(names).toContain('class')
819
+ expect(names).toContain('data-config')
820
+ expect(names).toContain('condition')
821
+ })
822
+ })
107
823
  })