Skip to content

Commit

Permalink
actually fix
Browse files Browse the repository at this point in the history
  • Loading branch information
Menduist committed Sep 28, 2024
1 parent ed02f2c commit 3496c1c
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 8 deletions.
22 changes: 20 additions & 2 deletions source/components/JsxParser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,24 @@ describe('JsxParser Component', () => {
)
expect(node.innerHTML).toMatch('baz')
})
it('optional short-cut', () => {
const { node } = render(
<JsxParser
bindings={{ foo: { bar: { baz: 'baz' } }, foo2: undefined }}
jsx="{foo?.bar.baz} {foo2?.bar.baz}"
/>,
)
expect(node.innerHTML).toMatch('baz')
})
it('optional function call', () => {
const { node } = render(
<JsxParser
bindings={{ foo: { bar: () => 'baz' }, foo2: undefined }}
jsx="{foo?.bar()} {foo2?.bar()}"
/>,
)
expect(node.innerHTML).toMatch('baz')
})
/* eslint-enable dot-notation,no-useless-concat */
})
})
Expand Down Expand Up @@ -1284,12 +1302,12 @@ describe('JsxParser Component', () => {
})

it('supports math with scope', () => {
const { node } = render(<JsxParser jsx="[1, 2, 3].map(num => num * 2)" />)
const { node } = render(<JsxParser jsx="{[1, 2, 3].map(num => num * 2)}" />)
expect(node.innerHTML).toEqual('246')
})

it('supports conditional with scope', () => {
const { node } = render(<JsxParser jsx="[1, 2, 3].map(num == 1 || num == 3 ? num : -1)" />)
const { node } = render(<JsxParser jsx="{[1, 2, 3].map(num => num == 1 || num == 3 ? num : -1)}" />)
expect(node.innerHTML).toEqual('1-13')
})
})
Expand Down
37 changes: 34 additions & 3 deletions source/components/JsxParser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ export type TProps = {
}
type Scope = Record<string, any>

class NullishShortCircuit extends Error {
constructor(message = 'Nullish value encountered') {
super(message)
this.name = 'NullishShortCircuit'
}
}

/* eslint-disable consistent-return */
export default class JsxParser extends React.Component<TProps> {
static displayName = 'JsxParser'
Expand Down Expand Up @@ -118,6 +125,9 @@ export default class JsxParser extends React.Component<TProps> {
case 'CallExpression':
const parsedCallee = this.#parseExpression(expression.callee, scope)
if (parsedCallee === undefined) {
if (expression.optional) {
throw new NullishShortCircuit()
}
this.props.onError!(new Error(`The expression '${expression.callee}' could not be resolved, resulting in an undefined return value.`))
return undefined
}
Expand Down Expand Up @@ -147,6 +157,8 @@ export default class JsxParser extends React.Component<TProps> {
return false
case 'MemberExpression':
return this.#parseMemberExpression(expression, scope)
case 'ChainExpression':
return this.#parseChainExpression(expression, scope)
case 'ObjectExpression':
const object: Record<string, any> = {}
expression.properties.forEach(prop => {
Expand Down Expand Up @@ -181,13 +193,26 @@ export default class JsxParser extends React.Component<TProps> {
})
return this.#parseExpression(expression.body, functionScope)
}
default:
this.props.onError!(new Error(`The expression type '${expression.type}' is not supported.`))
return undefined
}
}

#parseChainExpression = (expression: AcornJSX.ChainExpression, scope?: Scope): any => {
try {
return this.#parseExpression(expression.expression, scope)
} catch (error) {
if (error instanceof NullishShortCircuit) return undefined
throw error
}
}

#parseMemberExpression = (expression: AcornJSX.MemberExpression, scope?: Scope): any => {
const object = this.#parseExpression(expression.object, scope)

let property

if (expression.computed) {
property = this.#parseExpression(expression.property, scope)
} else if (expression.property.type === 'Identifier') {
Expand All @@ -197,11 +222,17 @@ export default class JsxParser extends React.Component<TProps> {
return undefined
}

if (expression.optional) {
if (object === null || object === undefined) return undefined
if (object === null || object === undefined) {
if (expression.optional) throw new NullishShortCircuit()
}

const member = object[property]
let member
try {
member = object[property]
} catch (error) {
this.props.onError!(new Error(`The property '${property}' could not be resolved on the object '${object}'.`))
return undefined
}
if (typeof member === 'function') return member.bind(object)
return member
}
Expand Down
11 changes: 8 additions & 3 deletions source/types/acorn-jsx.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ declare module 'acorn-jsx' {
type: 'CallExpression';
arguments: Expression[];
callee: Expression;
optional: boolean;
}

export interface ConditionalExpression extends BaseExpression {
Expand Down Expand Up @@ -126,7 +127,11 @@ declare module 'acorn-jsx' {
name?: string;
object: Expression;
property: Expression;
raw?: string;
}

export interface ChainExpression extends BaseExpression {
type: 'ChainExpression';
expression: MemberExpression | CallExpression;
}

export interface ObjectExpression extends BaseExpression {
Expand Down Expand Up @@ -156,9 +161,9 @@ declare module 'acorn-jsx' {

export type Expression =
JSXAttribute | JSXAttributeExpression | JSXElement | JSXExpressionContainer |
JSXSpreadAttribute | JSXFragment | JSXText |
JSXSpreadAttribute | JSXFragment | JSXText | ChainExpression | MemberExpression |
ArrayExpression | BinaryExpression | CallExpression | ConditionalExpression |
ExpressionStatement | Identifier | Literal | LogicalExpression | MemberExpression |
ExpressionStatement | Identifier | Literal | LogicalExpression |
ObjectExpression | TemplateElement | TemplateLiteral | UnaryExpression |
ArrowFunctionExpression

Expand Down

0 comments on commit 3496c1c

Please sign in to comment.