@teleporthq/teleport-plugin-common 0.43.22 → 0.43.30

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.
@@ -2,12 +2,12 @@ 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
4
 
5
- import { slotNode, elementNode, staticNode } from '@teleporthq/teleport-uidl-builders'
5
+ import { slotNode, elementNode, staticNode, dynamicNode } from '@teleporthq/teleport-uidl-builders'
6
6
  import {
7
7
  JSXGenerationParams,
8
8
  JSXGenerationOptions,
9
9
  } from '../../../src/node-handlers/node-to-jsx/types'
10
- import { ComponentUIDL } from '@teleporthq/teleport-types'
10
+ import { ComponentUIDL, UIDLNode } from '@teleporthq/teleport-types'
11
11
 
12
12
  const uidl = componentUIDLSample as ComponentUIDL
13
13
 
@@ -110,6 +110,73 @@ describe('generateJSXSyntax', () => {
110
110
  })
111
111
  })
112
112
 
113
+ describe('markdown-node with dynamic/expr children (CMS rich text)', () => {
114
+ const buildParams = (): JSXGenerationParams => ({
115
+ dependencies: {},
116
+ propDefinitions: {},
117
+ stateDefinitions: {},
118
+ globalStateDefinitions: {},
119
+ nodesLookup: {},
120
+ windowImports: {},
121
+ localeReferences: [],
122
+ globalReferences: [],
123
+ globalStateReferences: [],
124
+ hoistedConstants: [],
125
+ })
126
+
127
+ // Asserts the element is a <Markdown>{<expr> || ''}</Markdown> tag from markdown-to-jsx,
128
+ // which renders both markdown syntax and embedded HTML.
129
+ const expectMarkdownTag = (result: types.JSXElement, localParams: JSXGenerationParams) => {
130
+ expect((result.openingElement.name as types.JSXIdentifier).name).toBe('Markdown')
131
+
132
+ const container = result.children[0] as types.JSXExpressionContainer
133
+ expect(container.type).toBe('JSXExpressionContainer')
134
+ // dynamic content is guarded with `<expr> || ''` so a nullish value renders empty
135
+ expect((container.expression as types.LogicalExpression).operator).toBe('||')
136
+
137
+ expect(localParams.dependencies.Markdown).toEqual({
138
+ type: 'package',
139
+ path: 'markdown-to-jsx',
140
+ version: '7.7.12',
141
+ })
142
+ }
143
+
144
+ it('renders an expr child through the markdown-to-jsx <Markdown> component', () => {
145
+ const localParams = buildParams()
146
+ const node = elementNode('markdown-node', {}, [
147
+ { type: 'expr', content: 'props.cmsRichtext' } as UIDLNode,
148
+ ])
149
+
150
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
151
+
152
+ expectMarkdownTag(result, localParams)
153
+ })
154
+
155
+ it('renders a dynamic child through the markdown-to-jsx <Markdown> component', () => {
156
+ const localParams = buildParams()
157
+ const node = elementNode('markdown-node', {}, [dynamicNode('prop', 'cmsRichtext')])
158
+
159
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
160
+
161
+ expectMarkdownTag(result, localParams)
162
+ })
163
+
164
+ it('leaves a non-markdown div with an expr child as a plain value', () => {
165
+ const localParams = buildParams()
166
+ const node = elementNode('container', {}, [
167
+ { type: 'expr', content: 'props.cmsRichtext' } as UIDLNode,
168
+ ])
169
+
170
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
171
+
172
+ // generic expression handling wraps in a typeof/JSON.stringify guard, no markdown render
173
+ const container = result.children[0] as types.JSXExpressionContainer
174
+ expect(container.expression.type).toBe('ConditionalExpression')
175
+ expect((result.openingElement.name as types.JSXIdentifier).name).not.toBe('Markdown')
176
+ expect(localParams.dependencies.Markdown).toBeUndefined()
177
+ })
178
+ })
179
+
113
180
  describe('renderingConditions on elements', () => {
114
181
  it('wraps the element with a logical `&&` when renderingConditions are set', () => {
115
182
  const ownerContainer = elementNode('container', {}, [])
@@ -182,6 +249,76 @@ describe('generateJSXSyntax', () => {
182
249
  })
183
250
  })
184
251
 
252
+ describe('controlled <textarea> must not render children (React SSR contract)', () => {
253
+ const buildParams = (): JSXGenerationParams => ({
254
+ dependencies: {},
255
+ propDefinitions: {},
256
+ stateDefinitions: {},
257
+ globalStateDefinitions: {},
258
+ nodesLookup: {},
259
+ windowImports: {},
260
+ localeReferences: [],
261
+ globalReferences: [],
262
+ globalStateReferences: [],
263
+ hoistedConstants: [],
264
+ })
265
+
266
+ it('drops the children of a textarea that has a value attribute', () => {
267
+ // The details/entity binder can add value={{ ctx.field }} to a <textarea>
268
+ // while leaving its original text child in place. React's server renderer
269
+ // throws "If you supply value on a <textarea> you must not supply children",
270
+ // aborting static export — so the children must not be attached.
271
+ const localParams = buildParams()
272
+ const node = elementNode('textarea', { value: dynamicNode('prop', 'description') }, [
273
+ elementNode('span', {}, [dynamicNode('prop', 'description')]),
274
+ ])
275
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
276
+
277
+ expect((result.openingElement.name as types.JSXIdentifier).name).toBe('textarea')
278
+ expect(result.children.length).toBe(0)
279
+ // The child <span> is still generated into nodesLookup so the later
280
+ // styled-jsx traversal doesn't fail with "missing from the template chunk".
281
+ expect(Object.keys(localParams.nodesLookup).length).toBeGreaterThanOrEqual(2)
282
+ })
283
+
284
+ it('drops the children of a textarea that has a defaultValue attribute', () => {
285
+ const localParams = buildParams()
286
+ const node = elementNode('textarea', { defaultValue: dynamicNode('prop', 'description') }, [
287
+ elementNode('span', {}, [staticNode('initial')]),
288
+ ])
289
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
290
+ expect(result.children.length).toBe(0)
291
+ })
292
+
293
+ it('keeps the children of an uncontrolled textarea (no value/defaultValue)', () => {
294
+ const localParams = buildParams()
295
+ const node = elementNode('textarea', {}, [staticNode('initial content')])
296
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
297
+ expect(result.children.length).toBeGreaterThan(0)
298
+ })
299
+
300
+ it('drops the children of a named textarea controlled by the form-store binding', () => {
301
+ // maybeAddFormStoreFieldBinding injects value={formState['field'] ?? ''}
302
+ // onto named inputs inside a data-store-values-state form AFTER the UIDL
303
+ // attrs are processed — the textarea has NO value in its UIDL attrs but
304
+ // ends up controlled all the same, so its children must also be skipped.
305
+ const localParams = { ...buildParams(), formStoreStateName: 'formValues' }
306
+ const node = elementNode('textarea', { name: staticNode('description') }, [
307
+ staticNode('initial content'),
308
+ ])
309
+ const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
310
+
311
+ const hasInjectedValue = result.openingElement.attributes.some(
312
+ (attr) =>
313
+ attr.type === 'JSXAttribute' &&
314
+ attr.name.type === 'JSXIdentifier' &&
315
+ attr.name.name === 'value'
316
+ )
317
+ expect(hasInjectedValue).toBe(true)
318
+ expect(result.children.length).toBe(0)
319
+ })
320
+ })
321
+
185
322
  describe('plan v15 Layer 4 — codegen safety net for junk attrs and corrupt values', () => {
186
323
  // Suppress the console.warn that fires when junk attrs are dropped so
187
324
  // the test runner output stays clean. Reset between tests.
@@ -36,6 +36,65 @@ describe('JSS Utils ', () => {
36
36
  })
37
37
  })
38
38
 
39
+ it('drops a value with unbalanced parentheses (truncated color-mix)', () => {
40
+ const styleValue: UIDLStyleDefinitions = {
41
+ color: {
42
+ type: 'static',
43
+ content: 'color-mix(in srgb, var(--color-surface) 50%,',
44
+ },
45
+ display: {
46
+ type: 'static',
47
+ content: 'flex',
48
+ },
49
+ }
50
+ // The corrupted `color` is dropped; the surrounding rule stays parseable.
51
+ expect(getContentOfStyleObject(styleValue)).toEqual({ display: 'flex' })
52
+ })
53
+
54
+ it('drops a value with a stray closing parenthesis', () => {
55
+ const styleValue: UIDLStyleDefinitions = {
56
+ width: {
57
+ type: 'static',
58
+ content: 'calc(100% - 10px))',
59
+ },
60
+ }
61
+ expect(getContentOfStyleObject(styleValue)).toEqual({})
62
+ })
63
+
64
+ it('keeps valid values with balanced nested parentheses', () => {
65
+ const styleValue: UIDLStyleDefinitions = {
66
+ color: {
67
+ type: 'static',
68
+ content: 'color-mix(in srgb, var(--color-surface) 50%, transparent)',
69
+ },
70
+ background: {
71
+ type: 'static',
72
+ content: 'linear-gradient(90deg, rgba(0,0,0,0.5), #fff)',
73
+ },
74
+ }
75
+ expect(getContentOfStyleObject(styleValue)).toEqual({
76
+ color: 'color-mix(in srgb, var(--color-surface) 50%, transparent)',
77
+ background: 'linear-gradient(90deg, rgba(0,0,0,0.5), #fff)',
78
+ })
79
+ })
80
+
81
+ it('keeps values whose parentheses/colons live inside a quoted string', () => {
82
+ const styleValue: UIDLStyleDefinitions = {
83
+ background: {
84
+ type: 'static',
85
+ content: 'url("data:image/svg+xml;utf8,<svg viewBox=\'0 0 (1)\'></svg>")',
86
+ },
87
+ fontFamily: {
88
+ type: 'static',
89
+ content: '"Foo (Bold)", sans-serif',
90
+ },
91
+ }
92
+ expect(getContentOfStyleObject(styleValue)).toEqual({
93
+ background: 'url("data:image/svg+xml;utf8,<svg viewBox=\'0 0 (1)\'></svg>")',
94
+ fontFamily: '"Foo (Bold)", sans-serif',
95
+ })
96
+ })
97
+
39
98
  it('fails with other type than static or nested-style', () => {
40
99
  const styleValue: UIDLStyleDefinitions = {
41
100
  content: {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/node-handlers/node-to-jsx/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,EACL,eAAe,EAiBhB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAA+D,SAAS,EAAE,MAAM,SAAS,CAAA;AA4fhG,QAAA,MAAM,mBAAmB,EAAE,SAAS,CAAC,eAAe,EAAE,KAAK,CAAC,UAAU,CAgNrE,CAAA;AAED,eAAe,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/node-handlers/node-to-jsx/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,EACL,eAAe,EAiBhB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAA+D,SAAS,EAAE,MAAM,SAAS,CAAA;AA4fhG,QAAA,MAAM,mBAAmB,EAAE,SAAS,CAAC,eAAe,EAAE,KAAK,CAAC,UAAU,CA2QrE,CAAA;AAED,eAAe,mBAAmB,CAAA"}
@@ -532,11 +532,62 @@ var generateElementNode = function (node, params, jsxOptions) {
532
532
  var handler = (0, ast_utils_1.parseJSExpressionAsAST)(handlerCode);
533
533
  elementTag.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('onClick'), types.jsxExpressionContainer(handler)));
534
534
  }
535
- if (!selfClosing && children) {
535
+ // A markdown-node can carry its rich-text content as a dynamic/expr child
536
+ // (e.g. CMS-bound rich text) rather than as a `raw` attribute. The raw-attribute
537
+ // path is handled in addAttributesToJSXTag; here we handle the children path so
538
+ // the content is injected as HTML (dangerouslySetInnerHTML) instead of being
539
+ // printed as an escaped raw value by the generic expression handler.
540
+ var isMarkdownNode = node.content.semanticType === 'markdown-node' || originalElementName === 'markdown-node';
541
+ var hasRawAttr = attrs
542
+ ? Object.keys(attrs).some(function (attrKey) { var _a; return ((_a = attrs[attrKey]) === null || _a === void 0 ? void 0 : _a.type) === 'raw'; })
543
+ : false;
544
+ var markdownChildrenHandled = false;
545
+ if (isMarkdownNode && !selfClosing && !hasRawAttr && children && children.length > 0) {
546
+ var markdownChild = children.find(function (child) { return child.type === 'expr' || child.type === 'dynamic'; });
547
+ var markdownExpr = void 0;
548
+ if ((markdownChild === null || markdownChild === void 0 ? void 0 : markdownChild.type) === 'expr') {
549
+ markdownExpr = __1.ASTUtils.getExpressionFromUIDLExpressionNode(markdownChild);
550
+ }
551
+ else if ((markdownChild === null || markdownChild === void 0 ? void 0 : markdownChild.type) === 'dynamic') {
552
+ markdownExpr = resolveDynamicReferenceExpression(markdownChild, childParams, options);
553
+ }
554
+ if (markdownExpr) {
555
+ injectMarkdownExpression(elementTag, markdownExpr, '', childParams);
556
+ markdownChildrenHandled = true;
557
+ }
558
+ }
559
+ // A controlled <textarea> — one carrying a `value` (or `defaultValue`) prop —
560
+ // must NOT also render children. React's server renderer throws
561
+ // "If you supply `value` on a <textarea> you must not supply children.", which
562
+ // aborts static export of the whole page. The details-page / entity binder can
563
+ // leave the original text content in place when it adds a `value` binding to a
564
+ // textarea (harmless for <input>, fatal for <textarea>). We still GENERATE the
565
+ // children below so every UIDL node lands in `nodesLookup` (the styled-jsx pass
566
+ // requires it), but we do not attach them to the textarea — the value prop is
567
+ // the single source of truth.
568
+ var isTextArea = originalElementName.toLowerCase() === 'textarea' || node.content.semanticType === 'textarea';
569
+ // The check must run against the JSX opening element (not just UIDL attrs):
570
+ // maybeAddFormStoreFieldBinding injects a `value` attr onto named textareas
571
+ // inside a data-store-values-state form AFTER UIDL attrs are processed, and
572
+ // that injected binding makes the textarea controlled all the same.
573
+ var jsxTagHasControlledValue = elementTag.openingElement.attributes.some(function (attr) {
574
+ return attr.type === 'JSXAttribute' &&
575
+ attr.name.type === 'JSXIdentifier' &&
576
+ (attr.name.name === 'value' || attr.name.name === 'defaultValue');
577
+ });
578
+ var skipAttachingChildren = isTextArea &&
579
+ (jsxTagHasControlledValue ||
580
+ (!!attrs &&
581
+ (Object.prototype.hasOwnProperty.call(attrs, 'value') ||
582
+ Object.prototype.hasOwnProperty.call(attrs, 'defaultValue'))));
583
+ if (!markdownChildrenHandled && !selfClosing && children) {
536
584
  // Reorder children to ensure search nodes appear before DataProvider nodes
537
585
  var reorderedChildren = reorderChildrenForSearch(children);
538
586
  reorderedChildren.forEach(function (child) {
539
587
  var childTags = generateNode(child, childParams, options);
588
+ if (skipAttachingChildren) {
589
+ return;
590
+ }
540
591
  childTags.forEach(function (childTag) {
541
592
  if (typeof childTag === 'string') {
542
593
  (0, ast_utils_1.addChildJSXText)(elementTag, childTag);
@@ -1249,48 +1300,51 @@ var applyDynamicHtmlInjection = function (elementTag, rawValue, params, options)
1249
1300
  });
1250
1301
  };
1251
1302
  var applyStaticMarkdownInjection = function (elementTag, rawValue, params) {
1252
- var content = rawValue.content || '';
1253
- elementTag.openingElement.name.name = 'div';
1254
- elementTag.openingElement.selfClosing = true;
1255
- elementTag.closingElement = null;
1256
- elementTag.children = [];
1257
- elementTag.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('dangerouslySetInnerHTML'), types.jsxExpressionContainer(types.objectExpression([
1258
- types.objectProperty(types.identifier('__html'), types.stringLiteral(content)),
1259
- ]))));
1260
- cleanupMarkdownMappingDependencies(params);
1303
+ injectMarkdownExpression(elementTag, types.stringLiteral(rawValue.content || ''), '', params);
1261
1304
  };
1262
1305
  var applyDynamicMarkdownInjection = function (elementTag, rawValue, params, options) {
1263
1306
  var dynamicExpr = resolveDynamicReferenceExpression(rawValue.dynamic, params, options);
1264
1307
  var fallbackContent = rawValue.fallback || rawValue.content;
1265
- var reactMarkdownTag = (0, ast_builders_1.createJSXTag)('ReactMarkdown');
1266
- (0, ast_utils_1.addChildJSXTag)(reactMarkdownTag, types.jsxExpressionContainer(types.cloneNode(dynamicExpr)));
1267
- var fallbackTag = __1.ASTBuilders.createDOMInjectionNode(fallbackContent);
1268
- var conditionalExpr = types.conditionalExpression(dynamicExpr, reactMarkdownTag, fallbackTag);
1269
- elementTag.openingElement.name.name = 'div';
1308
+ injectMarkdownExpression(elementTag, dynamicExpr, fallbackContent, params);
1309
+ };
1310
+ /**
1311
+ * Renders a markdown-node's rich-text content through the `markdown-to-jsx` <Markdown>
1312
+ * component (the React generator's mapping for markdown-node).
1313
+ *
1314
+ * markdown-to-jsx parses BOTH markdown syntax AND embedded HTML, so it renders correctly
1315
+ * whether the CMS field returns markdown (`# Heading`) or HTML markup (`<p>…</p>`). A raw
1316
+ * HTML approach (dangerouslySetInnerHTML) would ignore markdown, and a markdown-only
1317
+ * renderer (react-markdown) escapes HTML — markdown-to-jsx covers both at once.
1318
+ *
1319
+ * Shared by every way a markdown-node can carry its content:
1320
+ * - a static `raw` attribute (see applyStaticMarkdownInjection)
1321
+ * - a `raw` attribute holding a dynamic reference (see applyDynamicMarkdownInjection)
1322
+ * - a dynamic/expr child, e.g. CMS-bound rich text (see generateElementNode)
1323
+ *
1324
+ * Produces: <Markdown>{expr || fallback}</Markdown>
1325
+ */
1326
+ var injectMarkdownExpression = function (elementTag, expression, fallbackContent, params) {
1327
+ // markdown-to-jsx requires a string child; guard dynamic expressions with a fallback so
1328
+ // a nullish CMS value renders as empty rather than throwing. Static literals need no guard.
1329
+ var childExpr = types.isStringLiteral(expression)
1330
+ ? expression
1331
+ : types.logicalExpression('||', expression, types.stringLiteral(fallbackContent || ''));
1332
+ elementTag.openingElement.name.name = 'Markdown';
1270
1333
  elementTag.openingElement.selfClosing = false;
1271
1334
  if (!elementTag.closingElement) {
1272
- elementTag.closingElement = types.jsxClosingElement(types.jsxIdentifier('div'));
1335
+ elementTag.closingElement = types.jsxClosingElement(types.jsxIdentifier('Markdown'));
1273
1336
  }
1274
1337
  else {
1275
1338
  ;
1276
- elementTag.closingElement.name.name = 'div';
1339
+ elementTag.closingElement.name.name = 'Markdown';
1277
1340
  }
1278
- elementTag.children = [types.jsxExpressionContainer(conditionalExpr)];
1279
- cleanupMarkdownMappingDependencies(params);
1280
- params.dependencies.ReactMarkdown = {
1341
+ elementTag.children = [types.jsxExpressionContainer(childExpr)];
1342
+ params.dependencies.Markdown = {
1281
1343
  type: 'package',
1282
- path: 'react-markdown',
1283
- version: '^8.0.7',
1344
+ path: 'markdown-to-jsx',
1345
+ version: '7.7.12',
1284
1346
  };
1285
1347
  };
1286
- var cleanupMarkdownMappingDependencies = function (params) {
1287
- Object.keys(params.dependencies).forEach(function (key) {
1288
- var _a, _b;
1289
- if ((_b = (_a = params.dependencies[key]) === null || _a === void 0 ? void 0 : _a.path) === null || _b === void 0 ? void 0 : _b.includes('markdown-to-jsx')) {
1290
- delete params.dependencies[key];
1291
- }
1292
- });
1293
- };
1294
1348
  /**
1295
1349
  * Handles raw attributes on a rich-text-editor-node element.
1296
1350
  *