@teleporthq/teleport-uidl-resolver 0.43.44 → 0.43.46
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.
- package/__tests__/unbound-expressions/expression-fallback.ts +64 -0
- package/__tests__/unbound-expressions/expression-identifiers.ts +47 -4
- package/__tests__/unbound-expressions/index.ts +216 -24
- package/dist/cjs/resolvers/unbound-expressions/ambient-identifiers.d.ts +32 -10
- package/dist/cjs/resolvers/unbound-expressions/ambient-identifiers.d.ts.map +1 -1
- package/dist/cjs/resolvers/unbound-expressions/ambient-identifiers.js +34 -27
- package/dist/cjs/resolvers/unbound-expressions/ambient-identifiers.js.map +1 -1
- package/dist/cjs/resolvers/unbound-expressions/expression-fallback.d.ts +22 -0
- package/dist/cjs/resolvers/unbound-expressions/expression-fallback.d.ts.map +1 -0
- package/dist/cjs/resolvers/unbound-expressions/expression-fallback.js +129 -0
- package/dist/cjs/resolvers/unbound-expressions/expression-fallback.js.map +1 -0
- package/dist/cjs/resolvers/unbound-expressions/expression-identifiers.d.ts +30 -16
- package/dist/cjs/resolvers/unbound-expressions/expression-identifiers.d.ts.map +1 -1
- package/dist/cjs/resolvers/unbound-expressions/expression-identifiers.js +130 -144
- package/dist/cjs/resolvers/unbound-expressions/expression-identifiers.js.map +1 -1
- package/dist/cjs/resolvers/unbound-expressions/expression-lexer.d.ts +41 -0
- package/dist/cjs/resolvers/unbound-expressions/expression-lexer.d.ts.map +1 -0
- package/dist/cjs/resolvers/unbound-expressions/expression-lexer.js +158 -0
- package/dist/cjs/resolvers/unbound-expressions/expression-lexer.js.map +1 -0
- package/dist/cjs/resolvers/unbound-expressions/index.d.ts.map +1 -1
- package/dist/cjs/resolvers/unbound-expressions/index.js +119 -61
- package/dist/cjs/resolvers/unbound-expressions/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/resolvers/unbound-expressions/ambient-identifiers.d.ts +32 -10
- package/dist/esm/resolvers/unbound-expressions/ambient-identifiers.d.ts.map +1 -1
- package/dist/esm/resolvers/unbound-expressions/ambient-identifiers.js +33 -26
- package/dist/esm/resolvers/unbound-expressions/ambient-identifiers.js.map +1 -1
- package/dist/esm/resolvers/unbound-expressions/expression-fallback.d.ts +22 -0
- package/dist/esm/resolvers/unbound-expressions/expression-fallback.d.ts.map +1 -0
- package/dist/esm/resolvers/unbound-expressions/expression-fallback.js +125 -0
- package/dist/esm/resolvers/unbound-expressions/expression-fallback.js.map +1 -0
- package/dist/esm/resolvers/unbound-expressions/expression-identifiers.d.ts +30 -16
- package/dist/esm/resolvers/unbound-expressions/expression-identifiers.d.ts.map +1 -1
- package/dist/esm/resolvers/unbound-expressions/expression-identifiers.js +125 -139
- package/dist/esm/resolvers/unbound-expressions/expression-identifiers.js.map +1 -1
- package/dist/esm/resolvers/unbound-expressions/expression-lexer.d.ts +41 -0
- package/dist/esm/resolvers/unbound-expressions/expression-lexer.d.ts.map +1 -0
- package/dist/esm/resolvers/unbound-expressions/expression-lexer.js +151 -0
- package/dist/esm/resolvers/unbound-expressions/expression-lexer.js.map +1 -0
- package/dist/esm/resolvers/unbound-expressions/index.d.ts.map +1 -1
- package/dist/esm/resolvers/unbound-expressions/index.js +121 -63
- package/dist/esm/resolvers/unbound-expressions/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/resolvers/unbound-expressions/ambient-identifiers.ts +33 -26
- package/src/resolvers/unbound-expressions/expression-fallback.ts +141 -0
- package/src/resolvers/unbound-expressions/expression-identifiers.ts +160 -138
- package/src/resolvers/unbound-expressions/expression-lexer.ts +156 -0
- package/src/resolvers/unbound-expressions/index.ts +134 -65
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { extractFallbackLiteral } from '../../src/resolvers/unbound-expressions/expression-fallback'
|
|
2
|
+
|
|
3
|
+
describe('extractFallbackLiteral', () => {
|
|
4
|
+
it('returns the string literal an expression falls back to', () => {
|
|
5
|
+
expect(extractFallbackLiteral('company?.name || "N/A"')).toBe('N/A')
|
|
6
|
+
expect(extractFallbackLiteral("deal?.stage || 'Unassigned'")).toBe('Unassigned')
|
|
7
|
+
expect(extractFallbackLiteral('company?.logo ?? "https://cdn/placeholder.png"')).toBe(
|
|
8
|
+
'https://cdn/placeholder.png'
|
|
9
|
+
)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('returns the last fallback of a chain', () => {
|
|
13
|
+
expect(extractFallbackLiteral('a || b || "last"')).toBe('last')
|
|
14
|
+
expect(extractFallbackLiteral('a ?? b ?? "last"')).toBe('last')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns numeric fallbacks verbatim', () => {
|
|
18
|
+
expect(extractFallbackLiteral('parseFloat(item.price) || 0')).toBe('0')
|
|
19
|
+
expect(extractFallbackLiteral('item.ratio || 1.5')).toBe('1.5')
|
|
20
|
+
expect(extractFallbackLiteral('item.delta || -1')).toBe('-1')
|
|
21
|
+
expect(extractFallbackLiteral('item.delta || +1')).toBe('1')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('accepts a template literal with no interpolation', () => {
|
|
25
|
+
expect(extractFallbackLiteral('item.label || `Untitled`')).toBe('Untitled')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejects a template literal that still reads scope', () => {
|
|
29
|
+
// tslint:disable-next-line no-invalid-template-strings
|
|
30
|
+
expect(extractFallbackLiteral('item.label || `${item.id}`')).toBeNull()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('ignores operators that live inside literals', () => {
|
|
34
|
+
expect(extractFallbackLiteral(`company?.size || "size || '-'"`)).toBe(`size || '-'`)
|
|
35
|
+
expect(extractFallbackLiteral(`"a || b"`)).toBeNull()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('ignores operators nested inside parentheses, brackets or braces', () => {
|
|
39
|
+
expect(extractFallbackLiteral('fn(a || "inner")')).toBeNull()
|
|
40
|
+
expect(extractFallbackLiteral('map[a || "inner"]')).toBeNull()
|
|
41
|
+
expect(extractFallbackLiteral('{ key: a || "inner" }')).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('rejects a fallback that is not a standalone literal', () => {
|
|
45
|
+
expect(extractFallbackLiteral('company?.name || buildName(company)')).toBeNull()
|
|
46
|
+
expect(extractFallbackLiteral(`company?.name || (a ? 'x' : 'y')`)).toBeNull()
|
|
47
|
+
expect(extractFallbackLiteral('company?.flag || true')).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('does not confuse optional chaining or a ternary with a nullish fallback', () => {
|
|
51
|
+
expect(extractFallbackLiteral(`company?.name`)).toBeNull()
|
|
52
|
+
expect(extractFallbackLiteral(`company ? 'yes' : 'no'`)).toBeNull()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('decodes escape sequences in the literal', () => {
|
|
56
|
+
expect(extractFallbackLiteral(`a || 'It\\'s here'`)).toBe("It's here")
|
|
57
|
+
expect(extractFallbackLiteral('a || "line\\nbreak"')).toBe('line\nbreak')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns null for empty or non-string input', () => {
|
|
61
|
+
expect(extractFallbackLiteral('')).toBeNull()
|
|
62
|
+
expect(extractFallbackLiteral(undefined as unknown as string)).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { analyzeExpressionScope } from '../../src/resolvers/unbound-expressions/expression-identifiers'
|
|
2
2
|
|
|
3
3
|
const roots = (expression: string): string[] =>
|
|
4
|
-
Array.from(
|
|
4
|
+
Array.from(analyzeExpressionScope(expression).freeIdentifiers).sort()
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const isResolvable = (expression: string): boolean => analyzeExpressionScope(expression).resolvable
|
|
7
|
+
|
|
8
|
+
describe('analyzeExpressionScope', () => {
|
|
7
9
|
it('returns the leading identifier of a member expression', () => {
|
|
8
10
|
expect(roots('cat.name')).toEqual(['cat'])
|
|
9
11
|
expect(roots('loc.location_id')).toEqual(['loc'])
|
|
@@ -56,6 +58,47 @@ describe('extractRootIdentifiers', () => {
|
|
|
56
58
|
|
|
57
59
|
it('returns an empty set for empty or non-string input', () => {
|
|
58
60
|
expect(roots('')).toEqual([])
|
|
59
|
-
expect(
|
|
61
|
+
expect(
|
|
62
|
+
Array.from(analyzeExpressionScope(undefined as unknown as string).freeIdentifiers)
|
|
63
|
+
).toEqual([])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('subtracts identifiers bound by an arrow function parameter list', () => {
|
|
67
|
+
expect(roots('(event) => event.target.value')).toEqual([])
|
|
68
|
+
expect(roots('item => item.id')).toEqual([])
|
|
69
|
+
expect(roots('rows.map((row, i) => row.name + i)').sort()).toEqual(['rows'])
|
|
70
|
+
expect(roots('(a, b) => a + b + outer').sort()).toEqual(['outer'])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('subtracts identifiers bound by a function expression', () => {
|
|
74
|
+
expect(roots('(function (a) { return a })')).toEqual([])
|
|
75
|
+
expect(roots('(function total(a) { return total(a) })')).toEqual([])
|
|
76
|
+
expect(roots('(function (a) { return a + outer })')).toEqual(['outer'])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('keeps nested arrow scopes independent of the outer expression', () => {
|
|
80
|
+
// tslint:disable-next-line no-invalid-template-strings
|
|
81
|
+
expect(roots('groups.map((group) => group.items.map((entry) => entry.id))')).toEqual(['groups'])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('still sees references that only appear outside a callback', () => {
|
|
85
|
+
expect(roots('outer.list.filter((x) => x.ok)').sort()).toEqual(['outer'])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('reports block-scoped declarations as unresolvable rather than guessing', () => {
|
|
89
|
+
expect(isResolvable('(() => { const total = 1; return total })()')).toBe(false)
|
|
90
|
+
expect(isResolvable('(() => { let total = 1; return total })()')).toBe(false)
|
|
91
|
+
expect(isResolvable('(() => { var total = 1; return total })()')).toBe(false)
|
|
92
|
+
expect(isResolvable('props.value')).toBe(true)
|
|
93
|
+
expect(isResolvable('(event) => event.target.value')).toBe(true)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('does not mistake a property named like a keyword for a declaration', () => {
|
|
97
|
+
expect(isResolvable('props.constant')).toBe(true)
|
|
98
|
+
expect(roots('props.constant')).toEqual(['props'])
|
|
99
|
+
// Reserved words are legal property names.
|
|
100
|
+
expect(isResolvable('props.const')).toBe(true)
|
|
101
|
+
expect(roots('props.const')).toEqual(['props'])
|
|
102
|
+
expect(roots('props.function + Math.max(a, b)').sort()).toEqual(['Math', 'a', 'b', 'props'])
|
|
60
103
|
})
|
|
61
104
|
})
|
|
@@ -24,6 +24,23 @@ const buildComponent = (node: UIDLElementNode): ComponentUIDL =>
|
|
|
24
24
|
},
|
|
25
25
|
} as unknown as ComponentUIDL)
|
|
26
26
|
|
|
27
|
+
const containerWith = (children: UIDLNode[]): UIDLElementNode => ({
|
|
28
|
+
type: 'element',
|
|
29
|
+
content: { elementType: 'container', children },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// `console.warn` is the resolver's diagnostic channel; silence it so the suite
|
|
33
|
+
// output stays readable while still exercising the code path.
|
|
34
|
+
let warnSpy: jest.SpyInstance
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
warnSpy.mockRestore()
|
|
42
|
+
})
|
|
43
|
+
|
|
27
44
|
describe('resolveUnboundExpressions', () => {
|
|
28
45
|
it('neutralises an orphaned option expression that references an unbound iterator', () => {
|
|
29
46
|
const option = optionWith(expr('cat.name'))
|
|
@@ -54,16 +71,65 @@ describe('resolveUnboundExpressions', () => {
|
|
|
54
71
|
},
|
|
55
72
|
},
|
|
56
73
|
} as UIDLNode
|
|
57
|
-
const uidl = buildComponent(
|
|
58
|
-
type: 'element',
|
|
59
|
-
content: { elementType: 'container', children: [repeater] },
|
|
60
|
-
})
|
|
74
|
+
const uidl = buildComponent(containerWith([repeater]))
|
|
61
75
|
|
|
62
76
|
resolveUnboundExpressions(uidl)
|
|
63
77
|
|
|
64
78
|
expect(boundExpr).toEqual({ type: 'expr', content: 'item?.id' })
|
|
65
79
|
})
|
|
66
80
|
|
|
81
|
+
it('keeps the `index` a list repeater binds alongside its render prop', () => {
|
|
82
|
+
const indexExpr = expr('index + 1')
|
|
83
|
+
const repeater: UIDLNode = {
|
|
84
|
+
type: 'cms-list-repeater',
|
|
85
|
+
content: {
|
|
86
|
+
elementType: 'fragment',
|
|
87
|
+
name: 'list',
|
|
88
|
+
key: 'list',
|
|
89
|
+
renderPropIdentifier: 'item',
|
|
90
|
+
nodes: {
|
|
91
|
+
list: {
|
|
92
|
+
type: 'element',
|
|
93
|
+
content: { elementType: 'div', children: [indexExpr] },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
} as UIDLNode
|
|
98
|
+
const uidl = buildComponent(containerWith([repeater]))
|
|
99
|
+
|
|
100
|
+
resolveUnboundExpressions(uidl)
|
|
101
|
+
|
|
102
|
+
expect(indexExpr).toEqual({ type: 'expr', content: 'index + 1' })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('keeps `index` inside a repeat that opts into it and drops it otherwise', () => {
|
|
106
|
+
const indexed: UIDLElementNode = {
|
|
107
|
+
type: 'element',
|
|
108
|
+
content: { elementType: 'div', children: [expr('index')] },
|
|
109
|
+
}
|
|
110
|
+
const notIndexed: UIDLElementNode = {
|
|
111
|
+
type: 'element',
|
|
112
|
+
content: { elementType: 'div', children: [expr('index')] },
|
|
113
|
+
}
|
|
114
|
+
const repeatNode = (child: UIDLElementNode, useIndex: boolean): UIDLNode =>
|
|
115
|
+
({
|
|
116
|
+
type: 'repeat',
|
|
117
|
+
content: {
|
|
118
|
+
node: child,
|
|
119
|
+
dataSource: { type: 'dynamic', content: { referenceType: 'prop', id: 'inventoryItem' } },
|
|
120
|
+
meta: { iteratorName: 'row', useIndex },
|
|
121
|
+
},
|
|
122
|
+
} as UIDLNode)
|
|
123
|
+
const uidl = buildComponent(
|
|
124
|
+
containerWith([repeatNode(indexed, true), repeatNode(notIndexed, false)])
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
resolveUnboundExpressions(uidl)
|
|
128
|
+
|
|
129
|
+
expect(indexed.content.children[0]).toEqual({ type: 'expr', content: 'index' })
|
|
130
|
+
expect(notIndexed.content.children[0]).toEqual({ type: 'static', content: '' })
|
|
131
|
+
})
|
|
132
|
+
|
|
67
133
|
it('flags a repeater iterator referenced outside of its subtree', () => {
|
|
68
134
|
const escaped = optionWith(expr('item?.id'))
|
|
69
135
|
const repeater: UIDLNode = {
|
|
@@ -78,35 +144,49 @@ describe('resolveUnboundExpressions', () => {
|
|
|
78
144
|
},
|
|
79
145
|
},
|
|
80
146
|
} as UIDLNode
|
|
81
|
-
const uidl = buildComponent(
|
|
82
|
-
type: 'element',
|
|
83
|
-
content: { elementType: 'container', children: [repeater, escaped] },
|
|
84
|
-
})
|
|
147
|
+
const uidl = buildComponent(containerWith([repeater, escaped]))
|
|
85
148
|
|
|
86
149
|
resolveUnboundExpressions(uidl)
|
|
87
150
|
|
|
88
151
|
expect(escaped.content.children[0]).toEqual({ type: 'static', content: '' })
|
|
89
152
|
})
|
|
90
153
|
|
|
91
|
-
it('keeps expressions that reference props, state
|
|
154
|
+
it('keeps expressions that reference props, state and globals', () => {
|
|
92
155
|
const propsExpr = expr('props.inventoryItem?.name')
|
|
93
156
|
const stateExpr = expr('categoryOptions')
|
|
94
|
-
const globalExpr = expr('JSON.stringify(
|
|
95
|
-
const
|
|
96
|
-
const uidl = buildComponent({
|
|
97
|
-
type: 'element',
|
|
98
|
-
content: {
|
|
99
|
-
elementType: 'container',
|
|
100
|
-
children: [propsExpr, stateExpr, globalExpr, eventExpr],
|
|
101
|
-
},
|
|
102
|
-
})
|
|
157
|
+
const globalExpr = expr('JSON.stringify(props.inventoryItem)')
|
|
158
|
+
const uidl = buildComponent(containerWith([propsExpr, stateExpr, globalExpr]))
|
|
103
159
|
|
|
104
160
|
resolveUnboundExpressions(uidl)
|
|
105
161
|
|
|
106
162
|
expect(propsExpr).toEqual({ type: 'expr', content: 'props.inventoryItem?.name' })
|
|
107
163
|
expect(stateExpr).toEqual({ type: 'expr', content: 'categoryOptions' })
|
|
108
|
-
expect(globalExpr).toEqual({ type: 'expr', content: 'JSON.stringify(
|
|
109
|
-
|
|
164
|
+
expect(globalExpr).toEqual({ type: 'expr', content: 'JSON.stringify(props.inventoryItem)' })
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('neutralises the renderer-only `state` object, which generated code never declares', () => {
|
|
168
|
+
// Regression: React state is destructured into bare variables, so a `state`
|
|
169
|
+
// reference that survived the editor export compiles to
|
|
170
|
+
// `ReferenceError: state is not defined` and fails `next build`.
|
|
171
|
+
const container = containerWith([expr('state'), expr('state.addressLine')])
|
|
172
|
+
const uidl = buildComponent(container)
|
|
173
|
+
|
|
174
|
+
resolveUnboundExpressions(uidl)
|
|
175
|
+
|
|
176
|
+
expect(container.content.children).toEqual([
|
|
177
|
+
{ type: 'static', content: '' },
|
|
178
|
+
{ type: 'static', content: '' },
|
|
179
|
+
])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('keeps a state named `state` when the component actually declares it', () => {
|
|
183
|
+
const container = containerWith([expr('state')])
|
|
184
|
+
const uidl = buildComponent(container)
|
|
185
|
+
uidl.stateDefinitions.state = { type: 'string', defaultValue: '' }
|
|
186
|
+
|
|
187
|
+
resolveUnboundExpressions(uidl)
|
|
188
|
+
|
|
189
|
+
expect(container.content.children).toEqual([{ type: 'expr', content: 'state' }])
|
|
110
190
|
})
|
|
111
191
|
|
|
112
192
|
it('neutralises an unbound expression used as an attribute value', () => {
|
|
@@ -147,6 +227,79 @@ describe('resolveUnboundExpressions', () => {
|
|
|
147
227
|
])
|
|
148
228
|
})
|
|
149
229
|
|
|
230
|
+
it('keeps the literal fallback an unbound expression was authored with', () => {
|
|
231
|
+
const srcAttr: UIDLElementNode = {
|
|
232
|
+
type: 'element',
|
|
233
|
+
content: {
|
|
234
|
+
elementType: 'image',
|
|
235
|
+
attrs: { src: { type: 'expr', content: `company?.logo ?? 'https://cdn/placeholder.png'` } },
|
|
236
|
+
children: [],
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
const container = containerWith([expr(`company?.name || "N/A"`), srcAttr])
|
|
240
|
+
const uidl = buildComponent(container)
|
|
241
|
+
|
|
242
|
+
resolveUnboundExpressions(uidl)
|
|
243
|
+
|
|
244
|
+
expect(container.content.children[0]).toEqual({ type: 'static', content: 'N/A' })
|
|
245
|
+
expect(srcAttr.content.attrs!.src).toEqual({
|
|
246
|
+
type: 'static',
|
|
247
|
+
content: 'https://cdn/placeholder.png',
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('does not mistake a fallback-looking literal inside a string for a real fallback', () => {
|
|
252
|
+
const container = containerWith([expr(`company?.size || "size || '-'"`)])
|
|
253
|
+
const uidl = buildComponent(container)
|
|
254
|
+
|
|
255
|
+
resolveUnboundExpressions(uidl)
|
|
256
|
+
|
|
257
|
+
expect(container.content.children[0]).toEqual({ type: 'static', content: `size || '-'` })
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('blanks rather than guesses when the fallback is not a plain literal', () => {
|
|
261
|
+
const container = containerWith([expr('company?.name || buildName(company)')])
|
|
262
|
+
const uidl = buildComponent(container)
|
|
263
|
+
|
|
264
|
+
resolveUnboundExpressions(uidl)
|
|
265
|
+
|
|
266
|
+
expect(container.content.children[0]).toEqual({ type: 'static', content: '' })
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('keeps an inline callback whose parameter is declared by the expression itself', () => {
|
|
270
|
+
const callbackExpr = expr('(event) => event.target.value')
|
|
271
|
+
// tslint:disable-next-line no-invalid-template-strings
|
|
272
|
+
const arrowExpr = expr('props.inventoryItem.tags.map((tag, i) => `${i}:${tag}`).join()')
|
|
273
|
+
const container = containerWith([callbackExpr, arrowExpr])
|
|
274
|
+
const uidl = buildComponent(container)
|
|
275
|
+
|
|
276
|
+
resolveUnboundExpressions(uidl)
|
|
277
|
+
|
|
278
|
+
expect(container.content.children).toEqual([callbackExpr, arrowExpr])
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('leaves an expression alone when it declares bindings the analyser cannot resolve', () => {
|
|
282
|
+
const iife = expr('(() => { const total = 2; return total })()')
|
|
283
|
+
const container = containerWith([iife])
|
|
284
|
+
const uidl = buildComponent(container)
|
|
285
|
+
|
|
286
|
+
resolveUnboundExpressions(uidl)
|
|
287
|
+
|
|
288
|
+
expect(container.content.children).toEqual([iife])
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('does not treat quoted JSON payloads as references', () => {
|
|
292
|
+
const filterExpr = expr(
|
|
293
|
+
`JSON.stringify([{ "type": "condition", "source": "id", "operand": "=" }])`
|
|
294
|
+
)
|
|
295
|
+
const container = containerWith([filterExpr])
|
|
296
|
+
const uidl = buildComponent(container)
|
|
297
|
+
|
|
298
|
+
resolveUnboundExpressions(uidl)
|
|
299
|
+
|
|
300
|
+
expect(container.content.children).toEqual([filterExpr])
|
|
301
|
+
})
|
|
302
|
+
|
|
150
303
|
it('keeps a navlink transitionTo that references a global-context identifier', () => {
|
|
151
304
|
// Regression: the abilities resolver emits transitionTo exprs like
|
|
152
305
|
// `/profile/${currentUser?.id}` for details navlinks bound to
|
|
@@ -163,10 +316,7 @@ describe('resolveUnboundExpressions', () => {
|
|
|
163
316
|
children: [],
|
|
164
317
|
},
|
|
165
318
|
}
|
|
166
|
-
const uidl = buildComponent(
|
|
167
|
-
type: 'element',
|
|
168
|
-
content: { elementType: 'container', children: [link] },
|
|
169
|
-
})
|
|
319
|
+
const uidl = buildComponent(containerWith([link]))
|
|
170
320
|
|
|
171
321
|
resolveUnboundExpressions(uidl)
|
|
172
322
|
|
|
@@ -176,4 +326,46 @@ describe('resolveUnboundExpressions', () => {
|
|
|
176
326
|
content: '`/profile/${currentUser?.id}`',
|
|
177
327
|
})
|
|
178
328
|
})
|
|
329
|
+
|
|
330
|
+
it('neutralises an unbound reference inside a template literal', () => {
|
|
331
|
+
const link: UIDLElementNode = {
|
|
332
|
+
type: 'element',
|
|
333
|
+
content: {
|
|
334
|
+
elementType: 'navlink',
|
|
335
|
+
attrs: {
|
|
336
|
+
// tslint:disable-next-line no-invalid-template-strings
|
|
337
|
+
transitionTo: { type: 'expr', content: '`/deal-details/${deal?.id}`' },
|
|
338
|
+
},
|
|
339
|
+
children: [],
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
const uidl = buildComponent(containerWith([link]))
|
|
343
|
+
|
|
344
|
+
resolveUnboundExpressions(uidl)
|
|
345
|
+
|
|
346
|
+
expect(link.content.attrs.transitionTo).toEqual({ type: 'static', content: '' })
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('keeps the render prop a data-source node introduces for its children', () => {
|
|
350
|
+
const boundExpr = expr('dataSourceData?.title')
|
|
351
|
+
const dataSource: UIDLNode = {
|
|
352
|
+
type: 'data-source-item',
|
|
353
|
+
content: {
|
|
354
|
+
elementType: 'DataProvider',
|
|
355
|
+
renderPropIdentifier: 'dataSourceData',
|
|
356
|
+
resourceDefinition: {
|
|
357
|
+
type: 'external-data-source',
|
|
358
|
+
dataSourceId: 'ds-1',
|
|
359
|
+
tableName: 'items',
|
|
360
|
+
dataSourceType: 'postgresql',
|
|
361
|
+
},
|
|
362
|
+
children: [{ type: 'element', content: { elementType: 'div', children: [boundExpr] } }],
|
|
363
|
+
},
|
|
364
|
+
} as UIDLNode
|
|
365
|
+
const uidl = buildComponent(containerWith([dataSource]))
|
|
366
|
+
|
|
367
|
+
resolveUnboundExpressions(uidl)
|
|
368
|
+
|
|
369
|
+
expect(boundExpr).toEqual({ type: 'expr', content: 'dataSourceData?.title' })
|
|
370
|
+
})
|
|
179
371
|
})
|
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Root identifiers that are always in scope
|
|
3
|
-
*
|
|
2
|
+
* Root identifiers that are always in scope where a UIDL `expr` node is
|
|
3
|
+
* rendered — i.e. in the component's RENDER BODY, which is the only place this
|
|
4
|
+
* resolver inspects (element children, element attributes and the link ability
|
|
5
|
+
* the abilities resolver has not yet folded into attributes).
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - standard JavaScript globals / built-ins.
|
|
7
|
+
* Membership here is a promise that the code generator declares the name. It is
|
|
8
|
+
* therefore limited to:
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - `props`, the render function's own parameter,
|
|
11
|
+
* - the global-context values the JSX emitter destructures from
|
|
12
|
+
* `useGlobalContext()` whenever it sees them in an expression
|
|
13
|
+
* (`GLOBAL_EXPRESSION_IDENTIFIERS` in teleport-plugin-common node-to-jsx),
|
|
14
|
+
* - standard JavaScript / browser globals.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT here:
|
|
17
|
+
*
|
|
18
|
+
* - `state` — React state is destructured into bare variables
|
|
19
|
+
* (`dynamicReferencePrefixMap.state` is `''`), so there is no `state` object
|
|
20
|
+
* at runtime and `state` / `state.x` in an expression is always a
|
|
21
|
+
* `ReferenceError`. Individual state names come from `stateDefinitions`.
|
|
22
|
+
* - `params`, `index` and render-prop names — these ARE bound, but only inside
|
|
23
|
+
* the subtree of the node that introduces them (a repeater's `.map()`
|
|
24
|
+
* callback, a CMS/data-source `renderSuccess` prop). The walker tracks them
|
|
25
|
+
* positionally so a reference that escapes its owner is still caught.
|
|
26
|
+
* - `event` / `evt` / `e` — bound only inside event handlers, which this
|
|
27
|
+
* resolver does not visit. When one appears in a render-scope expression it
|
|
28
|
+
* is either an inline callback parameter (the scope analyser sees the
|
|
29
|
+
* binding) or genuinely undeclared.
|
|
30
|
+
* - `context`, `ctx`, `metadata`, `record`, `idx`, `key`, `i` — no generator
|
|
31
|
+
* ever declares these names in a render body.
|
|
32
|
+
*
|
|
33
|
+
* Reserved words and literals (`true`, `null`, `undefined`, …) are filtered out
|
|
34
|
+
* by the lexer itself and need no entry here.
|
|
13
35
|
*/
|
|
14
|
-
export declare const
|
|
36
|
+
export declare const RENDER_SCOPE_IDENTIFIERS: Set<string>;
|
|
15
37
|
/**
|
|
16
38
|
* Identifiers emitted by the code generator itself (hoisted constants, internal
|
|
17
39
|
* helpers) are prefixed with `_` or `$`. Treat them as bound so we never touch
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ambient-identifiers.d.ts","sourceRoot":"","sources":["../../../../src/resolvers/unbound-expressions/ambient-identifiers.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ambient-identifiers.d.ts","sourceRoot":"","sources":["../../../../src/resolvers/unbound-expressions/ambient-identifiers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,wBAAwB,aA6DnC,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,eAAgB,MAAM,KAAG,OACT,CAAA"}
|
|
@@ -1,27 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isGeneratorInternalIdentifier = exports.
|
|
3
|
+
exports.isGeneratorInternalIdentifier = exports.RENDER_SCOPE_IDENTIFIERS = void 0;
|
|
4
4
|
/**
|
|
5
|
-
* Root identifiers that are always in scope
|
|
6
|
-
*
|
|
5
|
+
* Root identifiers that are always in scope where a UIDL `expr` node is
|
|
6
|
+
* rendered — i.e. in the component's RENDER BODY, which is the only place this
|
|
7
|
+
* resolver inspects (element children, element attributes and the link ability
|
|
8
|
+
* the abilities resolver has not yet folded into attributes).
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - standard JavaScript globals / built-ins.
|
|
10
|
+
* Membership here is a promise that the code generator declares the name. It is
|
|
11
|
+
* therefore limited to:
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* - `props`, the render function's own parameter,
|
|
14
|
+
* - the global-context values the JSX emitter destructures from
|
|
15
|
+
* `useGlobalContext()` whenever it sees them in an expression
|
|
16
|
+
* (`GLOBAL_EXPRESSION_IDENTIFIERS` in teleport-plugin-common node-to-jsx),
|
|
17
|
+
* - standard JavaScript / browser globals.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately NOT here:
|
|
20
|
+
*
|
|
21
|
+
* - `state` — React state is destructured into bare variables
|
|
22
|
+
* (`dynamicReferencePrefixMap.state` is `''`), so there is no `state` object
|
|
23
|
+
* at runtime and `state` / `state.x` in an expression is always a
|
|
24
|
+
* `ReferenceError`. Individual state names come from `stateDefinitions`.
|
|
25
|
+
* - `params`, `index` and render-prop names — these ARE bound, but only inside
|
|
26
|
+
* the subtree of the node that introduces them (a repeater's `.map()`
|
|
27
|
+
* callback, a CMS/data-source `renderSuccess` prop). The walker tracks them
|
|
28
|
+
* positionally so a reference that escapes its owner is still caught.
|
|
29
|
+
* - `event` / `evt` / `e` — bound only inside event handlers, which this
|
|
30
|
+
* resolver does not visit. When one appears in a render-scope expression it
|
|
31
|
+
* is either an inline callback parameter (the scope analyser sees the
|
|
32
|
+
* binding) or genuinely undeclared.
|
|
33
|
+
* - `context`, `ctx`, `metadata`, `record`, `idx`, `key`, `i` — no generator
|
|
34
|
+
* ever declares these names in a render body.
|
|
35
|
+
*
|
|
36
|
+
* Reserved words and literals (`true`, `null`, `undefined`, …) are filtered out
|
|
37
|
+
* by the lexer itself and need no entry here.
|
|
16
38
|
*/
|
|
17
|
-
exports.
|
|
18
|
-
//
|
|
39
|
+
exports.RENDER_SCOPE_IDENTIFIERS = new Set([
|
|
40
|
+
// The render function's own parameter
|
|
19
41
|
'props',
|
|
20
|
-
'state',
|
|
21
|
-
'context',
|
|
22
|
-
'ctx',
|
|
23
|
-
'metadata',
|
|
24
|
-
'record',
|
|
25
42
|
// Global-context values: the JSX emitter scans expressions for these
|
|
26
43
|
// (GLOBAL_EXPRESSION_IDENTIFIERS in teleport-plugin-common node-to-jsx) and
|
|
27
44
|
// injects the matching `useGlobalContext()` destructuring, so they are
|
|
@@ -33,15 +50,6 @@ exports.AMBIENT_IDENTIFIERS = new Set([
|
|
|
33
50
|
'cart',
|
|
34
51
|
'locale',
|
|
35
52
|
'locales',
|
|
36
|
-
// Event handler / map callback parameters
|
|
37
|
-
'event',
|
|
38
|
-
'evt',
|
|
39
|
-
'e',
|
|
40
|
-
'params',
|
|
41
|
-
'index',
|
|
42
|
-
'idx',
|
|
43
|
-
'key',
|
|
44
|
-
'i',
|
|
45
53
|
// Browser globals
|
|
46
54
|
'window',
|
|
47
55
|
'document',
|
|
@@ -57,7 +65,6 @@ exports.AMBIENT_IDENTIFIERS = new Set([
|
|
|
57
65
|
'URLSearchParams',
|
|
58
66
|
// Language built-ins
|
|
59
67
|
'globalThis',
|
|
60
|
-
'undefined',
|
|
61
68
|
'NaN',
|
|
62
69
|
'Infinity',
|
|
63
70
|
'JSON',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ambient-identifiers.js","sourceRoot":"","sources":["../../../../src/resolvers/unbound-expressions/ambient-identifiers.ts"],"names":[],"mappings":";;;AAAA
|
|
1
|
+
{"version":3,"file":"ambient-identifiers.js","sourceRoot":"","sources":["../../../../src/resolvers/unbound-expressions/ambient-identifiers.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACU,QAAA,wBAAwB,GAAG,IAAI,GAAG,CAAS;IACtD,sCAAsC;IACtC,OAAO;IACP,qEAAqE;IACrE,4EAA4E;IAC5E,uEAAuE;IACvE,2EAA2E;IAC3E,gEAAgE;IAChE,aAAa;IACb,gBAAgB;IAChB,WAAW;IACX,MAAM;IACN,QAAQ;IACR,SAAS;IACT,kBAAkB;IAClB,QAAQ;IACR,UAAU;IACV,WAAW;IACX,UAAU;IACV,SAAS;IACT,QAAQ;IACR,cAAc;IACd,gBAAgB;IAChB,SAAS;IACT,OAAO;IACP,KAAK;IACL,iBAAiB;IACjB,qBAAqB;IACrB,YAAY;IACZ,KAAK;IACL,UAAU;IACV,MAAM;IACN,MAAM;IACN,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,MAAM;IACN,QAAQ;IACR,KAAK;IACL,KAAK;IACL,SAAS;IACT,SAAS;IACT,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,WAAW;IACX,UAAU;IACV,OAAO;IACP,SAAS;IACT,MAAM;IACN,UAAU;IACV,YAAY;IACZ,OAAO;IACP,UAAU;IACV,oBAAoB;IACpB,oBAAoB;IACpB,WAAW;IACX,WAAW;CACZ,CAAC,CAAA;AAEF;;;;GAIG;AACI,IAAM,6BAA6B,GAAG,UAAC,UAAkB;IAC9D,OAAA,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC;AAAxD,CAAwD,CAAA;AAD7C,QAAA,6BAA6B,iCACgB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovers the literal an expression was authored to fall back to.
|
|
3
|
+
*
|
|
4
|
+
* Content bindings produced by the editor almost always carry a human-authored
|
|
5
|
+
* default — `company?.name || "N/A"`, `featured?.image_url || "https://…"`.
|
|
6
|
+
* When the reference at the head of such an expression turns out to be
|
|
7
|
+
* unbound, dropping the WHOLE expression throws that default away and leaves an
|
|
8
|
+
* empty cell / a broken `src`. Keeping the fallback preserves what the author
|
|
9
|
+
* actually wanted to show when the data is missing — which is precisely the
|
|
10
|
+
* situation an unbound reference represents.
|
|
11
|
+
*
|
|
12
|
+
* Only the tail of a top-level `||` / `??` chain is considered, and only when it
|
|
13
|
+
* is a single string or number literal. Anything else (a call, a ternary, a
|
|
14
|
+
* template with interpolations) is not a safe static substitute, so the caller
|
|
15
|
+
* falls back to an empty value.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Returns the literal an expression falls back to, or `null` when it has no
|
|
19
|
+
* literal fallback that can stand on its own.
|
|
20
|
+
*/
|
|
21
|
+
export declare const extractFallbackLiteral: (expression: string) => string | null;
|
|
22
|
+
//# sourceMappingURL=expression-fallback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expression-fallback.d.ts","sourceRoot":"","sources":["../../../../src/resolvers/unbound-expressions/expression-fallback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA8GH;;;GAGG;AACH,eAAO,MAAM,sBAAsB,eAAgB,MAAM,KAAG,MAAM,GAAG,IAWpE,CAAA"}
|