safe-mdx 1.6.0 → 1.7.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.
- package/README.md +22 -12
- package/dist/safe-mdx.d.ts +13 -0
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +187 -0
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +284 -11
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +1 -1
- package/src/safe-mdx.test.tsx +355 -11
- package/src/safe-mdx.tsx +246 -0
package/src/safe-mdx.test.tsx
CHANGED
|
@@ -3818,7 +3818,7 @@ test('scope with function in spread attribute', () => {
|
|
|
3818
3818
|
expect(html).toMatchInlineSnapshot(`"<h1>Spread test</h1>"`)
|
|
3819
3819
|
})
|
|
3820
3820
|
|
|
3821
|
-
test('scope with .map and arrow function callback
|
|
3821
|
+
test('scope with .map and arrow function callback works without generate (safe interpreter)', () => {
|
|
3822
3822
|
const scope = {
|
|
3823
3823
|
items: [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }],
|
|
3824
3824
|
}
|
|
@@ -3828,16 +3828,8 @@ test('scope with .map and arrow function callback fails without generate', () =>
|
|
|
3828
3828
|
`
|
|
3829
3829
|
|
|
3830
3830
|
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
3831
|
-
expect(errors).toMatchInlineSnapshot(`
|
|
3832
|
-
|
|
3833
|
-
{
|
|
3834
|
-
"line": 1,
|
|
3835
|
-
"message": "Failed to evaluate expression: items.map(item => item.name).join(", "). Expected options.generate to be the "generate" function from "escodegen"",
|
|
3836
|
-
"type": "expression",
|
|
3837
|
-
},
|
|
3838
|
-
]
|
|
3839
|
-
`)
|
|
3840
|
-
expect(html).toMatchInlineSnapshot(`""`)
|
|
3831
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3832
|
+
expect(html).toMatchInlineSnapshot(`"Alice, Bob, Charlie"`)
|
|
3841
3833
|
})
|
|
3842
3834
|
|
|
3843
3835
|
test('scope with .map and arrow function callback works with generate', () => {
|
|
@@ -3853,3 +3845,355 @@ test('scope with .map and arrow function callback works with generate', () => {
|
|
|
3853
3845
|
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3854
3846
|
expect(html).toMatchInlineSnapshot(`"Alice, Bob, Charlie"`)
|
|
3855
3847
|
})
|
|
3848
|
+
|
|
3849
|
+
test('safe interpreter: arrow with block body and return', () => {
|
|
3850
|
+
const scope = {
|
|
3851
|
+
items: [1, 2, 3],
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
const code = dedent`
|
|
3855
|
+
{items.map(x => { return x * 2 }).join(", ")}
|
|
3856
|
+
`
|
|
3857
|
+
|
|
3858
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
3859
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3860
|
+
expect(html).toMatchInlineSnapshot(`"2, 4, 6"`)
|
|
3861
|
+
})
|
|
3862
|
+
|
|
3863
|
+
test('safe interpreter: arrow with multiple params', () => {
|
|
3864
|
+
const scope = {
|
|
3865
|
+
items: ['a', 'b', 'c'],
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
const code = dedent`
|
|
3869
|
+
{items.map((item, i) => i + ":" + item).join(", ")}
|
|
3870
|
+
`
|
|
3871
|
+
|
|
3872
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
3873
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3874
|
+
expect(html).toMatchInlineSnapshot(`"0:a, 1:b, 2:c"`)
|
|
3875
|
+
})
|
|
3876
|
+
|
|
3877
|
+
test('safe interpreter: arrow with object destructuring', () => {
|
|
3878
|
+
const scope = {
|
|
3879
|
+
items: [
|
|
3880
|
+
{ name: 'Alice', age: 30 },
|
|
3881
|
+
{ name: 'Bob', age: 25 },
|
|
3882
|
+
],
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
const code = dedent`
|
|
3886
|
+
{items.map(({ name, age }) => name + "(" + age + ")").join(", ")}
|
|
3887
|
+
`
|
|
3888
|
+
|
|
3889
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
3890
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3891
|
+
expect(html).toMatchInlineSnapshot(`"Alice(30), Bob(25)"`)
|
|
3892
|
+
})
|
|
3893
|
+
|
|
3894
|
+
test('safe interpreter: arrow with ternary expression', () => {
|
|
3895
|
+
const scope = {
|
|
3896
|
+
items: [
|
|
3897
|
+
{ name: 'Alice', active: true },
|
|
3898
|
+
{ name: 'Bob', active: false },
|
|
3899
|
+
],
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
const code = dedent`
|
|
3903
|
+
{items.map(item => item.active ? item.name : "inactive").join(", ")}
|
|
3904
|
+
`
|
|
3905
|
+
|
|
3906
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
3907
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3908
|
+
expect(html).toMatchInlineSnapshot(`"Alice, inactive"`)
|
|
3909
|
+
})
|
|
3910
|
+
|
|
3911
|
+
test('safe interpreter: .filter with arrow function', () => {
|
|
3912
|
+
const scope = {
|
|
3913
|
+
items: [1, 2, 3, 4, 5, 6],
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
const code = dedent`
|
|
3917
|
+
{items.filter(x => x > 3).join(", ")}
|
|
3918
|
+
`
|
|
3919
|
+
|
|
3920
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
3921
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3922
|
+
expect(html).toMatchInlineSnapshot(`"4, 5, 6"`)
|
|
3923
|
+
})
|
|
3924
|
+
|
|
3925
|
+
test('safe interpreter: .reduce with arrow function', () => {
|
|
3926
|
+
const scope = {
|
|
3927
|
+
items: [1, 2, 3, 4],
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
const code = dedent`
|
|
3931
|
+
{items.reduce((acc, x) => acc + x, 0)}
|
|
3932
|
+
`
|
|
3933
|
+
|
|
3934
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
3935
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3936
|
+
expect(html).toMatchInlineSnapshot(`"10"`)
|
|
3937
|
+
})
|
|
3938
|
+
|
|
3939
|
+
test('safe interpreter: chained .filter.map', () => {
|
|
3940
|
+
const scope = {
|
|
3941
|
+
users: [
|
|
3942
|
+
{ name: 'Alice', role: 'admin' },
|
|
3943
|
+
{ name: 'Bob', role: 'user' },
|
|
3944
|
+
{ name: 'Charlie', role: 'admin' },
|
|
3945
|
+
],
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
const code = dedent`
|
|
3949
|
+
{users.filter(u => u.role === "admin").map(u => u.name).join(", ")}
|
|
3950
|
+
`
|
|
3951
|
+
|
|
3952
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
3953
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3954
|
+
expect(html).toMatchInlineSnapshot(`"Alice, Charlie"`)
|
|
3955
|
+
})
|
|
3956
|
+
|
|
3957
|
+
test('safe interpreter: .find with arrow function', () => {
|
|
3958
|
+
const scope = {
|
|
3959
|
+
items: [
|
|
3960
|
+
{ id: 1, name: 'Alice' },
|
|
3961
|
+
{ id: 2, name: 'Bob' },
|
|
3962
|
+
],
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
const code = dedent`
|
|
3966
|
+
{items.find(item => item.id === 2).name}
|
|
3967
|
+
`
|
|
3968
|
+
|
|
3969
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
3970
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3971
|
+
expect(html).toMatchInlineSnapshot(`"Bob"`)
|
|
3972
|
+
})
|
|
3973
|
+
|
|
3974
|
+
test('safe interpreter: .some and .every with arrow functions', () => {
|
|
3975
|
+
const scope = {
|
|
3976
|
+
nums: [2, 4, 6],
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
const code = dedent`
|
|
3980
|
+
{nums.every(n => n > 0) ? "all positive" : "nope"}
|
|
3981
|
+
`
|
|
3982
|
+
|
|
3983
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
3984
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3985
|
+
expect(html).toMatchInlineSnapshot(`"all positive"`)
|
|
3986
|
+
})
|
|
3987
|
+
|
|
3988
|
+
test('safe interpreter: nested arrow functions', () => {
|
|
3989
|
+
const scope = {
|
|
3990
|
+
matrix: [[1, 2], [3, 4], [5, 6]],
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
const code = dedent`
|
|
3994
|
+
{matrix.map(row => row.map(x => x * 10).join("-")).join(", ")}
|
|
3995
|
+
`
|
|
3996
|
+
|
|
3997
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
3998
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
3999
|
+
expect(html).toMatchInlineSnapshot(`"10-20, 30-40, 50-60"`)
|
|
4000
|
+
})
|
|
4001
|
+
|
|
4002
|
+
test('safe interpreter: arrow accessing outer scope variables', () => {
|
|
4003
|
+
const scope = {
|
|
4004
|
+
items: [1, 2, 3],
|
|
4005
|
+
multiplier: 5,
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
const code = dedent`
|
|
4009
|
+
{items.map(x => x * multiplier).join(", ")}
|
|
4010
|
+
`
|
|
4011
|
+
|
|
4012
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4013
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4014
|
+
expect(html).toMatchInlineSnapshot(`"5, 10, 15"`)
|
|
4015
|
+
})
|
|
4016
|
+
|
|
4017
|
+
test('safe interpreter: arrow with block body and variable declaration', () => {
|
|
4018
|
+
const scope = {
|
|
4019
|
+
items: [{ first: 'John', last: 'Doe' }, { first: 'Jane', last: 'Smith' }],
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
const code = dedent`
|
|
4023
|
+
{items.map(item => { const full = item.first + " " + item.last; return full }).join(", ")}
|
|
4024
|
+
`
|
|
4025
|
+
|
|
4026
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4027
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4028
|
+
expect(html).toMatchInlineSnapshot(`"John Doe, Jane Smith"`)
|
|
4029
|
+
})
|
|
4030
|
+
|
|
4031
|
+
test('safe interpreter: arrow with if/else in block body', () => {
|
|
4032
|
+
const scope = {
|
|
4033
|
+
items: [1, 2, 3, 4, 5],
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
const code = dedent`
|
|
4037
|
+
{items.map(x => { if (x > 3) { return "big" } else { return "small" } }).join(", ")}
|
|
4038
|
+
`
|
|
4039
|
+
|
|
4040
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4041
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4042
|
+
expect(html).toMatchInlineSnapshot(`"small, small, small, big, big"`)
|
|
4043
|
+
})
|
|
4044
|
+
|
|
4045
|
+
test('safe interpreter: .sort with comparator arrow', () => {
|
|
4046
|
+
const scope = {
|
|
4047
|
+
items: [3, 1, 4, 1, 5],
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
const code = dedent`
|
|
4051
|
+
{items.sort((a, b) => a - b).join(", ")}
|
|
4052
|
+
`
|
|
4053
|
+
|
|
4054
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4055
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4056
|
+
expect(html).toMatchInlineSnapshot(`"1, 1, 3, 4, 5"`)
|
|
4057
|
+
})
|
|
4058
|
+
|
|
4059
|
+
test('safe interpreter: arrow in JSX attribute', () => {
|
|
4060
|
+
const scope = {
|
|
4061
|
+
items: [{ name: 'Alice' }, { name: 'Bob' }],
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
const code = dedent`
|
|
4065
|
+
<Heading level={items.map(i => i.name).join(", ")}>Title</Heading>
|
|
4066
|
+
`
|
|
4067
|
+
|
|
4068
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4069
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4070
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Title</h1>"`)
|
|
4071
|
+
})
|
|
4072
|
+
|
|
4073
|
+
test('safe interpreter: arrow with array destructuring', () => {
|
|
4074
|
+
const scope = {
|
|
4075
|
+
pairs: [['a', 1], ['b', 2], ['c', 3]],
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
const code = dedent`
|
|
4079
|
+
{pairs.map(([letter, num]) => letter + num).join(", ")}
|
|
4080
|
+
`
|
|
4081
|
+
|
|
4082
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4083
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4084
|
+
expect(html).toMatchInlineSnapshot(`"a1, b2, c3"`)
|
|
4085
|
+
})
|
|
4086
|
+
|
|
4087
|
+
test('safe interpreter: calling scope functions inside arrow callback', () => {
|
|
4088
|
+
const scope = {
|
|
4089
|
+
items: [{ name: 'alice' }, { name: 'bob' }],
|
|
4090
|
+
formatName: (s: string) => s.toUpperCase(),
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
const code = dedent`
|
|
4094
|
+
{items.map(item => formatName(item.name)).join(", ")}
|
|
4095
|
+
`
|
|
4096
|
+
|
|
4097
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4098
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4099
|
+
expect(html).toMatchInlineSnapshot(`"ALICE, BOB"`)
|
|
4100
|
+
})
|
|
4101
|
+
|
|
4102
|
+
test('safe interpreter: calling scope function with multiple args inside arrow', () => {
|
|
4103
|
+
const scope = {
|
|
4104
|
+
items: [1, 2, 3],
|
|
4105
|
+
add: (a: number, b: number) => a + b,
|
|
4106
|
+
base: 10,
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
const code = dedent`
|
|
4110
|
+
{items.map(x => add(x, base)).join(", ")}
|
|
4111
|
+
`
|
|
4112
|
+
|
|
4113
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4114
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4115
|
+
expect(html).toMatchInlineSnapshot(`"11, 12, 13"`)
|
|
4116
|
+
})
|
|
4117
|
+
|
|
4118
|
+
test('safe interpreter: scope function returning object used in arrow', () => {
|
|
4119
|
+
const scope = {
|
|
4120
|
+
ids: [1, 2, 3],
|
|
4121
|
+
getUser: (id: number) => ({ id, name: 'User' + id }),
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4124
|
+
const code = dedent`
|
|
4125
|
+
{ids.map(id => getUser(id).name).join(", ")}
|
|
4126
|
+
`
|
|
4127
|
+
|
|
4128
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4129
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4130
|
+
expect(html).toMatchInlineSnapshot(`"User1, User2, User3"`)
|
|
4131
|
+
})
|
|
4132
|
+
|
|
4133
|
+
test('safe interpreter: arrow with default parameter', () => {
|
|
4134
|
+
const scope = {
|
|
4135
|
+
items: [undefined, 'hello', undefined],
|
|
4136
|
+
fallback: 'default',
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
const code = dedent`
|
|
4140
|
+
{items.map((x = fallback) => x).join(", ")}
|
|
4141
|
+
`
|
|
4142
|
+
|
|
4143
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4144
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4145
|
+
expect(html).toMatchInlineSnapshot(`"default, hello, default"`)
|
|
4146
|
+
})
|
|
4147
|
+
|
|
4148
|
+
test('scope with template literal in expression', () => {
|
|
4149
|
+
const scope = {
|
|
4150
|
+
name: 'World',
|
|
4151
|
+
count: 3,
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
const code = dedent`
|
|
4155
|
+
{${'`'}Hello ${'${'}name${'}'}, you have ${'${'}count${'}'} items${'`'}}
|
|
4156
|
+
`
|
|
4157
|
+
|
|
4158
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4159
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4160
|
+
expect(html).toMatchInlineSnapshot(`"Hello World, you have 3 items"`)
|
|
4161
|
+
})
|
|
4162
|
+
|
|
4163
|
+
test('scope with tagged template literal function', () => {
|
|
4164
|
+
const myTag = (strings: TemplateStringsArray, ...values: any[]) => {
|
|
4165
|
+
return strings.reduce((result, str, i) => {
|
|
4166
|
+
return result + str + (values[i] !== undefined ? String(values[i]).toUpperCase() : '')
|
|
4167
|
+
}, '')
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
const scope = {
|
|
4171
|
+
myTag,
|
|
4172
|
+
name: 'world',
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
const code = `{myTag${'`'}hello ${'${'}name${'}'}${'`'}}`
|
|
4176
|
+
|
|
4177
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope, { generate })
|
|
4178
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4179
|
+
expect(html).toMatchInlineSnapshot(`"hello WORLD"`)
|
|
4180
|
+
})
|
|
4181
|
+
|
|
4182
|
+
test('scope with tagged template literal without generate', () => {
|
|
4183
|
+
const myTag = (strings: TemplateStringsArray, ...values: any[]) => {
|
|
4184
|
+
return strings.reduce((result, str, i) => {
|
|
4185
|
+
return result + str + (values[i] !== undefined ? String(values[i]).toUpperCase() : '')
|
|
4186
|
+
}, '')
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
const scope = {
|
|
4190
|
+
myTag,
|
|
4191
|
+
name: 'world',
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
const code = `{myTag${'`'}hello ${'${'}name${'}'}${'`'}}`
|
|
4195
|
+
|
|
4196
|
+
const { html, errors } = render(code, undefined, undefined, undefined, scope)
|
|
4197
|
+
expect(errors).toMatchInlineSnapshot(`[]`)
|
|
4198
|
+
expect(html).toMatchInlineSnapshot(`"hello WORLD"`)
|
|
4199
|
+
})
|
package/src/safe-mdx.tsx
CHANGED
|
@@ -534,6 +534,19 @@ export class MdastToJsx {
|
|
|
534
534
|
const options = hasScope || this.evaluateOptions
|
|
535
535
|
? { ...(hasScope ? { functions: true } : {}), ...this.evaluateOptions }
|
|
536
536
|
: undefined
|
|
537
|
+
|
|
538
|
+
// When functions are enabled and the user hasn't provided their own
|
|
539
|
+
// `generate` (escodegen), inject our safe AST-interpreting visitors
|
|
540
|
+
// that handle ArrowFunctionExpression and FunctionExpression without
|
|
541
|
+
// using `new Function()` or `eval()`. This makes arrow function
|
|
542
|
+
// callbacks like `.map(x => x.name)` work in Cloudflare Workers.
|
|
543
|
+
if (options && options.functions && !options.generate) {
|
|
544
|
+
;(options as any).visitors = {
|
|
545
|
+
...(options as any).visitors,
|
|
546
|
+
...createSafeFunctionVisitors(),
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
537
550
|
return Evaluate.evaluate.sync(expression, context, options)
|
|
538
551
|
}
|
|
539
552
|
|
|
@@ -1145,3 +1158,236 @@ export function mdastBfs(
|
|
|
1145
1158
|
type ComponentsMap = { [k in (typeof nativeTags)[number]]?: any } & {
|
|
1146
1159
|
[key: string]: any
|
|
1147
1160
|
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Bind function parameters to argument values, handling Identifier,
|
|
1164
|
+
* ObjectPattern, ArrayPattern, RestElement, and AssignmentPattern nodes.
|
|
1165
|
+
* Writes bindings into `ctx` in place.
|
|
1166
|
+
*/
|
|
1167
|
+
function bindParams(
|
|
1168
|
+
params: any[],
|
|
1169
|
+
args: any[],
|
|
1170
|
+
ctx: Record<string, any>,
|
|
1171
|
+
visit: (node: any, context: any, parent?: any) => any,
|
|
1172
|
+
) {
|
|
1173
|
+
for (let i = 0; i < params.length; i++) {
|
|
1174
|
+
const param = params[i]
|
|
1175
|
+
switch (param.type) {
|
|
1176
|
+
case 'Identifier':
|
|
1177
|
+
ctx[param.name] = args[i]
|
|
1178
|
+
break
|
|
1179
|
+
case 'RestElement':
|
|
1180
|
+
if (param.argument.type === 'Identifier') {
|
|
1181
|
+
ctx[param.argument.name] = args.slice(i)
|
|
1182
|
+
}
|
|
1183
|
+
break
|
|
1184
|
+
case 'AssignmentPattern': {
|
|
1185
|
+
const val =
|
|
1186
|
+
args[i] !== undefined
|
|
1187
|
+
? args[i]
|
|
1188
|
+
: visit(param.right, ctx, param)
|
|
1189
|
+
if (param.left.type === 'Identifier') {
|
|
1190
|
+
ctx[param.left.name] = val
|
|
1191
|
+
}
|
|
1192
|
+
break
|
|
1193
|
+
}
|
|
1194
|
+
case 'ObjectPattern': {
|
|
1195
|
+
const obj = args[i] || {}
|
|
1196
|
+
for (const prop of param.properties) {
|
|
1197
|
+
if (prop.type === 'RestElement') {
|
|
1198
|
+
const used = new Set(
|
|
1199
|
+
param.properties
|
|
1200
|
+
.filter((p: any) => p !== prop)
|
|
1201
|
+
.map(
|
|
1202
|
+
(p: any) =>
|
|
1203
|
+
p.key?.name ?? p.key?.value,
|
|
1204
|
+
),
|
|
1205
|
+
)
|
|
1206
|
+
const rest: Record<string, any> = {}
|
|
1207
|
+
for (const key of Object.keys(obj)) {
|
|
1208
|
+
if (!used.has(key)) rest[key] = obj[key]
|
|
1209
|
+
}
|
|
1210
|
+
if (prop.argument.type === 'Identifier') {
|
|
1211
|
+
ctx[prop.argument.name] = rest
|
|
1212
|
+
}
|
|
1213
|
+
} else {
|
|
1214
|
+
const key =
|
|
1215
|
+
prop.key.type === 'Identifier'
|
|
1216
|
+
? prop.key.name
|
|
1217
|
+
: prop.key.value
|
|
1218
|
+
if (prop.value.type === 'Identifier') {
|
|
1219
|
+
ctx[prop.value.name] = obj[key]
|
|
1220
|
+
} else if (
|
|
1221
|
+
prop.value.type === 'AssignmentPattern'
|
|
1222
|
+
) {
|
|
1223
|
+
const val =
|
|
1224
|
+
obj[key] !== undefined
|
|
1225
|
+
? obj[key]
|
|
1226
|
+
: visit(
|
|
1227
|
+
prop.value.right,
|
|
1228
|
+
ctx,
|
|
1229
|
+
prop.value,
|
|
1230
|
+
)
|
|
1231
|
+
if (
|
|
1232
|
+
prop.value.left.type === 'Identifier'
|
|
1233
|
+
) {
|
|
1234
|
+
ctx[prop.value.left.name] = val
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
break
|
|
1240
|
+
}
|
|
1241
|
+
case 'ArrayPattern': {
|
|
1242
|
+
const arr = args[i] || []
|
|
1243
|
+
for (let j = 0; j < param.elements.length; j++) {
|
|
1244
|
+
const elem = param.elements[j]
|
|
1245
|
+
if (!elem) continue
|
|
1246
|
+
if (elem.type === 'Identifier') {
|
|
1247
|
+
ctx[elem.name] = arr[j]
|
|
1248
|
+
} else if (
|
|
1249
|
+
elem.type === 'RestElement' &&
|
|
1250
|
+
elem.argument.type === 'Identifier'
|
|
1251
|
+
) {
|
|
1252
|
+
ctx[elem.argument.name] = arr.slice(j)
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
break
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Sentinel value to signal a return from inside a block body
|
|
1262
|
+
const RETURN_SENTINEL = Symbol('return')
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Execute a block statement body (array of statements) using the
|
|
1266
|
+
* eval-estree-expression visitor's `this.visit`. Returns the value
|
|
1267
|
+
* from the first ReturnStatement encountered, or undefined.
|
|
1268
|
+
*/
|
|
1269
|
+
function executeBlockBody(
|
|
1270
|
+
body: any[],
|
|
1271
|
+
ctx: Record<string, any>,
|
|
1272
|
+
visit: (node: any, context: any, parent?: any) => any,
|
|
1273
|
+
parentNode: any,
|
|
1274
|
+
): any {
|
|
1275
|
+
for (const stmt of body) {
|
|
1276
|
+
switch (stmt.type) {
|
|
1277
|
+
case 'ReturnStatement':
|
|
1278
|
+
return stmt.argument
|
|
1279
|
+
? visit(stmt.argument, ctx, stmt)
|
|
1280
|
+
: undefined
|
|
1281
|
+
case 'ExpressionStatement':
|
|
1282
|
+
visit(stmt.expression, ctx, stmt)
|
|
1283
|
+
break
|
|
1284
|
+
case 'VariableDeclaration':
|
|
1285
|
+
for (const decl of stmt.declarations) {
|
|
1286
|
+
const value = decl.init
|
|
1287
|
+
? visit(decl.init, ctx, decl)
|
|
1288
|
+
: undefined
|
|
1289
|
+
if (decl.id.type === 'Identifier') {
|
|
1290
|
+
ctx[decl.id.name] = value
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
break
|
|
1294
|
+
case 'IfStatement': {
|
|
1295
|
+
const test = visit(stmt.test, ctx, stmt)
|
|
1296
|
+
if (test) {
|
|
1297
|
+
if (stmt.consequent.type === 'BlockStatement') {
|
|
1298
|
+
const result = executeBlockBody(
|
|
1299
|
+
stmt.consequent.body,
|
|
1300
|
+
ctx,
|
|
1301
|
+
visit,
|
|
1302
|
+
stmt,
|
|
1303
|
+
)
|
|
1304
|
+
if (result !== undefined) return result
|
|
1305
|
+
} else if (
|
|
1306
|
+
stmt.consequent.type === 'ReturnStatement'
|
|
1307
|
+
) {
|
|
1308
|
+
return stmt.consequent.argument
|
|
1309
|
+
? visit(
|
|
1310
|
+
stmt.consequent.argument,
|
|
1311
|
+
ctx,
|
|
1312
|
+
stmt.consequent,
|
|
1313
|
+
)
|
|
1314
|
+
: undefined
|
|
1315
|
+
} else {
|
|
1316
|
+
visit(stmt.consequent, ctx, stmt)
|
|
1317
|
+
}
|
|
1318
|
+
} else if (stmt.alternate) {
|
|
1319
|
+
if (stmt.alternate.type === 'BlockStatement') {
|
|
1320
|
+
const result = executeBlockBody(
|
|
1321
|
+
stmt.alternate.body,
|
|
1322
|
+
ctx,
|
|
1323
|
+
visit,
|
|
1324
|
+
stmt,
|
|
1325
|
+
)
|
|
1326
|
+
if (result !== undefined) return result
|
|
1327
|
+
} else if (
|
|
1328
|
+
stmt.alternate.type === 'ReturnStatement'
|
|
1329
|
+
) {
|
|
1330
|
+
return stmt.alternate.argument
|
|
1331
|
+
? visit(
|
|
1332
|
+
stmt.alternate.argument,
|
|
1333
|
+
ctx,
|
|
1334
|
+
stmt.alternate,
|
|
1335
|
+
)
|
|
1336
|
+
: undefined
|
|
1337
|
+
} else {
|
|
1338
|
+
visit(stmt.alternate, ctx, stmt)
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
break
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return undefined
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Custom visitors for eval-estree-expression that interpret arrow functions
|
|
1350
|
+
* and function expressions by walking the AST recursively, without using
|
|
1351
|
+
* `new Function()` or `eval()`. This makes them safe for Cloudflare Workers
|
|
1352
|
+
* and other edge runtimes that block dynamic code evaluation.
|
|
1353
|
+
*
|
|
1354
|
+
* The visitors are called with `this` bound to the Expression evaluator
|
|
1355
|
+
* instance, giving access to `this.visit()` for recursive evaluation.
|
|
1356
|
+
*/
|
|
1357
|
+
export function createSafeFunctionVisitors() {
|
|
1358
|
+
// Using a regular function (not arrow) so `this` is the Expression instance
|
|
1359
|
+
function functionExpressionVisitor(
|
|
1360
|
+
this: any,
|
|
1361
|
+
node: any,
|
|
1362
|
+
context: any,
|
|
1363
|
+
) {
|
|
1364
|
+
const self = this
|
|
1365
|
+
return function (this: any, ...args: any[]) {
|
|
1366
|
+
const newContext = { ...context }
|
|
1367
|
+
bindParams(node.params, args, newContext, (n, ctx, p) =>
|
|
1368
|
+
self.visit(n, ctx, p),
|
|
1369
|
+
)
|
|
1370
|
+
|
|
1371
|
+
if (
|
|
1372
|
+
node.expression ||
|
|
1373
|
+
node.body.type !== 'BlockStatement'
|
|
1374
|
+
) {
|
|
1375
|
+
// Expression body: x => x.name
|
|
1376
|
+
return self.visit(node.body, newContext, node)
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// Block body: x => { ... return ... }
|
|
1380
|
+
return executeBlockBody(
|
|
1381
|
+
node.body.body,
|
|
1382
|
+
newContext,
|
|
1383
|
+
(n, ctx, p) => self.visit(n, ctx, p),
|
|
1384
|
+
node,
|
|
1385
|
+
)
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
return {
|
|
1390
|
+
ArrowFunctionExpression: functionExpressionVisitor,
|
|
1391
|
+
FunctionExpression: functionExpressionVisitor,
|
|
1392
|
+
}
|
|
1393
|
+
}
|