Skip to content

Commit 115b478

Browse files
authored
feat: Component collection syntax, unary support, better error handling (#308)
## Summary Refactors TypeScript typings for the `components` prop to support nested component collections and adds support for additional unary operations. ## What Changed - **Type Safety**: Added `ComponentType` and `ComponentsType` definitions for better component collection support - **Unary Operations**: Added bitwise NOT (`~`) and `typeof` operator support - **Error Handling**: Fixed error handling logic to prevent execution on parse errors - **Developer Experience**: Improved IntelliSense and autocomplete for complex component hierarchies ## Why - Better type safety when passing nested component collections - More complete JavaScript expression evaluation - Cleaner, more maintainable type definitions - More robust error handling Closes #297 Closes #304 Closes #305 Closes #306 Closes #307
1 parent e8237b9 commit 115b478

3 files changed

Lines changed: 53 additions & 31 deletions

File tree

.tool-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
bun 1.2.1
1+
bun 1.2.22

source/components/JsxParser.test.tsx

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -116,30 +116,37 @@ describe('JsxParser Component', () => {
116116

117117
describe('unary operations', () => {
118118
const testCases = [
119-
['+60', 60],
120119
['-60', -60],
121-
['!true', false],
122-
['!false', true],
120+
['!""', true],
123121
['!0', true],
124-
['!1', false],
122+
['!false', true],
123+
['!NaN', true],
125124
['!null', true],
126125
['!undefined', true],
127-
['!NaN', true],
128-
['!""', true],
129-
['!{}', false],
130-
['![]', false],
131-
['+true', 1],
132-
['+false', 0],
133-
['+null', 0],
134-
['+undefined', NaN],
135-
['+""', 0],
136-
['+"123"', 123],
137126
['+"-123"', -123],
127+
['+"123"', 123],
128+
['+60', 60],
129+
['+true', 1],
130+
['~1', -2],
131+
['~2', -3],
132+
['typeof "abc"', 'string'],
133+
['typeof 123', 'number'],
134+
135+
// react doesn't render `false`
136+
['![]', undefined],
137+
['!{}', undefined],
138+
['!1', undefined],
139+
['!true', undefined],
140+
['+""', undefined],
141+
['+false', undefined],
142+
['+null', undefined],
143+
['+undefined', undefined],
138144
]
139145

140146
test.each(testCases)(
141147
'should evaluate unary %s correctly',
142-
({ operation, expected }) => {
148+
(operation, expected) => {
149+
console.log('operation', operation)
143150
const { instance } = render(<JsxParser jsx={`{${operation}}`} />)
144151
if (Number.isNaN(expected)) {
145152
expect(Number.isNaN(instance.ParsedChildren[0])).toBe(true)
@@ -1038,6 +1045,19 @@ describe('JsxParser Component', () => {
10381045
expect(() => render(<JsxParser jsx='{ document.querySelector("body") }' onError={e => { throw e }} />)).toThrow()
10391046
expect(() => render(<JsxParser jsx='{ document.createElement("script") }' onError={e => { throw e }} />)).toThrow()
10401047
})
1048+
1049+
test('renderError catches errors', () => {
1050+
const renderError = jest.fn((...args) => console.error(...args))
1051+
render(
1052+
<JsxParser
1053+
bindings={{ badFn() { throw new Error('Test error') } }}
1054+
jsx="<div>{badFn()}</div>"
1055+
renderError={renderError}
1056+
/>,
1057+
)
1058+
expect(renderError).toHaveBeenCalledWith({ error: 'Error: Test error' })
1059+
})
1060+
10411061
test('supports className prop', () => {
10421062
const { node } = render(<JsxParser className="foo" jsx="Text" />)
10431063
expect(node.classList.contains('foo')).toBeTruthy()
@@ -1192,13 +1212,13 @@ describe('JsxParser Component', () => {
11921212
jsx={`
11931213
<Outer id="outer-id" name="Outer Name">
11941214
{outer => <>
1195-
<h1>Outer ({outer.id}): {outer.name}</h1>
1196-
<Inner id="inner-id" name={outer.name}>
1197-
{inner => <>
1215+
<h1>Outer ({outer.id}): {outer.name}</h1>
1216+
<Inner id="inner-id" name={outer.name}>
1217+
{inner => <>
11981218
<h2>Inner ({inner.id}): {inner.name}</h2>
11991219
<div>{outer.id} &gt; {outer.name}</div>
12001220
</>}
1201-
</Inner>
1221+
</Inner>
12021222
</>}
12031223
</Outer>
12041224
`}

source/components/JsxParser.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ function handleNaN<T>(child: T): T | 'NaN' {
1515
type ParsedJSX = React.ReactNode | boolean | string
1616
type ParsedTree = ParsedJSX | ParsedJSX[] | null
1717

18+
type ComponentType =
19+
| React.ComponentType // allows for class components
20+
| React.ExoticComponent // allows for forwardRef
21+
| (() => React.ReactNode) // allows for function components
22+
type ComponentsType =
23+
| ComponentType
24+
| Record<string, ComponentType>
25+
1826
/**
1927
* Props for the JsxParser component
2028
*/
@@ -47,12 +55,7 @@ export type TProps = {
4755
className?: string,
4856

4957
/** Map of component names to their React component definitions */
50-
components?: Record<
51-
string,
52-
| React.ComponentType // allows for class components
53-
| React.ExoticComponent // allows for forwardRef
54-
| (() => React.ReactNode) // allows for function components
55-
>,
58+
components?: Record<string, ComponentsType>,
5659

5760
/** If true, only renders custom components defined in the components prop */
5861
componentsOnly?: boolean,
@@ -124,16 +127,13 @@ export default class JsxParser extends React.Component<TProps> {
124127
parsed = parser.parse(wrappedJsx, { ecmaVersion: 'latest' })
125128
// @ts-ignore - AcornJsx doesn't have typescript typings
126129
parsed = parsed.body[0].expression.children || []
130+
return parsed.map(p => this.#parseExpression(p)).filter(Boolean)
127131
} catch (error) {
128132
if (this.props.showWarnings) console.warn(error) // eslint-disable-line no-console
129133
if (this.props.onError) this.props.onError(error as Error)
130-
if (this.props.renderError) {
131-
return this.props.renderError({ error: String(error) })
132-
}
134+
if (this.props.renderError) return this.props.renderError({ error: String(error) })
133135
return null
134136
}
135-
136-
return parsed.map(p => this.#parseExpression(p)).filter(Boolean)
137137
}
138138

139139
/**
@@ -247,6 +247,8 @@ export default class JsxParser extends React.Component<TProps> {
247247
case '+': return +unaryValue
248248
case '-': return -unaryValue
249249
case '!': return !unaryValue
250+
case '~': return ~unaryValue // eslint-disable-line no-bitwise
251+
case 'typeof': return typeof unaryValue
250252
}
251253
return undefined
252254
case 'ArrowFunctionExpression':

0 commit comments

Comments
 (0)