safe-mdx 1.5.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 +94 -8
- package/dist/dynamic-esm-component.d.ts +1 -1
- package/dist/dynamic-esm-component.d.ts.map +1 -1
- package/dist/dynamic-esm-component.js +9 -1
- package/dist/dynamic-esm-component.js.map +1 -1
- package/dist/esm-parser.d.ts +1 -1
- package/dist/esm-parser.d.ts.map +1 -1
- package/dist/esm-parser.js +3 -3
- package/dist/esm-parser.js.map +1 -1
- package/dist/esm-parser.test.js +2 -2
- package/dist/html/html-and-md.test.js.map +1 -1
- package/dist/html/html-to-mdx-ast.d.ts +1 -1
- package/dist/html/html-to-mdx-ast.js +4 -4
- package/dist/html/html-to-mdx-ast.js.map +1 -1
- package/dist/html/html-to-mdx-ast.test.js +3 -3
- package/dist/html/html-to-mdx-ast.test.js.map +1 -1
- package/dist/parse.d.ts +1 -1
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +5 -1
- package/dist/parse.js.map +1 -1
- package/dist/safe-mdx.bench.js +2 -2
- package/dist/safe-mdx.bench.js.map +1 -1
- package/dist/safe-mdx.d.ts +48 -3
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +219 -26
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +420 -5
- package/dist/safe-mdx.test.js.map +1 -1
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +3 -1
- package/dist/streaming.js.map +1 -1
- package/package.json +30 -7
- package/src/esm-parser.test.ts +3 -3
- package/src/esm-parser.ts +4 -4
- package/src/html/html-and-md.test.ts +2 -2
- package/src/html/html-to-mdx-ast.test.ts +3 -3
- package/src/html/html-to-mdx-ast.ts +4 -4
- package/src/parse.ts +3 -1
- package/src/safe-mdx.bench.tsx +2 -2
- package/src/safe-mdx.test.tsx +519 -11
- package/src/safe-mdx.tsx +315 -28
- package/src/streaming.tsx +2 -1
package/src/safe-mdx.tsx
CHANGED
|
@@ -8,10 +8,10 @@ import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx'
|
|
|
8
8
|
|
|
9
9
|
import { Fragment, ReactNode } from 'react'
|
|
10
10
|
import { DynamicEsmComponent } from 'safe-mdx/client'
|
|
11
|
-
import { extractComponentInfo, parseEsmImports } from './esm-parser.
|
|
12
|
-
import { resolveModulePath, type EagerModules } from './parse.
|
|
13
|
-
import { htmlToMdxAst } from './html/html-to-mdx-ast.
|
|
14
|
-
import { validHtmlElements, nativeTags } from './html/valid-html-elements.
|
|
11
|
+
import { extractComponentInfo, parseEsmImports } from './esm-parser.ts'
|
|
12
|
+
import { resolveModulePath, type EagerModules } from './parse.ts'
|
|
13
|
+
import { htmlToMdxAst } from './html/html-to-mdx-ast.ts'
|
|
14
|
+
import { validHtmlElements, nativeTags } from './html/valid-html-elements.ts'
|
|
15
15
|
|
|
16
16
|
export type MyRootContent = RootContent | Root
|
|
17
17
|
|
|
@@ -46,6 +46,18 @@ export type CreateElementFunction = (
|
|
|
46
46
|
...children: ReactNode[]
|
|
47
47
|
) => ReactNode
|
|
48
48
|
|
|
49
|
+
export interface EvaluateOptions {
|
|
50
|
+
/** Enable function calls in expressions. Automatically enabled when `scope` is provided. */
|
|
51
|
+
functions?: boolean
|
|
52
|
+
/** Pass `escodegen.generate` to support inline function expressions
|
|
53
|
+
* like arrow functions in `.map(x => x.name)`. Requires `functions: true`. */
|
|
54
|
+
generate?: (ast: any) => string
|
|
55
|
+
/** Force logical operators (`&&`, `||`) to return booleans. */
|
|
56
|
+
booleanLogicalOperators?: boolean
|
|
57
|
+
/** Throw when variables referenced in expressions are undefined. */
|
|
58
|
+
strict?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
50
62
|
components,
|
|
51
63
|
markdown = '',
|
|
@@ -58,6 +70,8 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
58
70
|
modules,
|
|
59
71
|
baseUrl,
|
|
60
72
|
onError,
|
|
73
|
+
scope,
|
|
74
|
+
evaluateOptions,
|
|
61
75
|
}: {
|
|
62
76
|
components?: ComponentsMap
|
|
63
77
|
markdown?: string
|
|
@@ -77,6 +91,15 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
77
91
|
/** Called for each error during rendering (missing components, invalid props, failed expressions).
|
|
78
92
|
* Throw inside this callback to stop rendering on first error. */
|
|
79
93
|
onError?: (error: SafeMdxError) => void
|
|
94
|
+
/** Variables and functions available in MDX expressions.
|
|
95
|
+
* When scope contains functions, function calls in expressions are
|
|
96
|
+
* automatically enabled. */
|
|
97
|
+
scope?: Record<string, any>
|
|
98
|
+
/** Options passed to `eval-estree-expression` for expression evaluation.
|
|
99
|
+
* Pass `{ functions: true }` to enable function calls, or
|
|
100
|
+
* `{ functions: true, generate: escodegen.generate }` to also support
|
|
101
|
+
* inline arrow functions and callbacks like `.map(x => x.name)`. */
|
|
102
|
+
evaluateOptions?: EvaluateOptions
|
|
80
103
|
}) {
|
|
81
104
|
const visitor = new MdastToJsx({
|
|
82
105
|
markdown,
|
|
@@ -90,6 +113,8 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
90
113
|
modules,
|
|
91
114
|
baseUrl,
|
|
92
115
|
onError,
|
|
116
|
+
scope,
|
|
117
|
+
evaluateOptions,
|
|
93
118
|
})
|
|
94
119
|
const result = visitor.run()
|
|
95
120
|
return result
|
|
@@ -110,6 +135,8 @@ export class MdastToJsx {
|
|
|
110
135
|
modules?: EagerModules
|
|
111
136
|
baseUrl?: string
|
|
112
137
|
onError?: (error: SafeMdxError) => void
|
|
138
|
+
scope?: Record<string, any>
|
|
139
|
+
evaluateOptions?: EvaluateOptions
|
|
113
140
|
|
|
114
141
|
constructor({
|
|
115
142
|
markdown: code = '',
|
|
@@ -123,6 +150,8 @@ export class MdastToJsx {
|
|
|
123
150
|
modules,
|
|
124
151
|
baseUrl,
|
|
125
152
|
onError,
|
|
153
|
+
scope,
|
|
154
|
+
evaluateOptions,
|
|
126
155
|
}: {
|
|
127
156
|
markdown?: string
|
|
128
157
|
mdast: MyRootContent
|
|
@@ -140,6 +169,15 @@ export class MdastToJsx {
|
|
|
140
169
|
/** Called for each error during rendering (missing components, invalid props, failed expressions).
|
|
141
170
|
* Throw inside this callback to stop rendering on first error. */
|
|
142
171
|
onError?: (error: SafeMdxError) => void
|
|
172
|
+
/** Variables and functions available in MDX expressions.
|
|
173
|
+
* When scope contains functions, function calls in expressions are
|
|
174
|
+
* automatically enabled. */
|
|
175
|
+
scope?: Record<string, any>
|
|
176
|
+
/** Options passed to `eval-estree-expression` for expression evaluation.
|
|
177
|
+
* Pass `{ functions: true }` to enable function calls, or
|
|
178
|
+
* `{ functions: true, generate: escodegen.generate }` to also support
|
|
179
|
+
* inline arrow functions and callbacks like `.map(x => x.name)`. */
|
|
180
|
+
evaluateOptions?: EvaluateOptions
|
|
143
181
|
}) {
|
|
144
182
|
this.str = code
|
|
145
183
|
|
|
@@ -158,6 +196,8 @@ export class MdastToJsx {
|
|
|
158
196
|
this.modules = modules
|
|
159
197
|
this.baseUrl = baseUrl
|
|
160
198
|
this.onError = onError
|
|
199
|
+
this.scope = scope
|
|
200
|
+
this.evaluateOptions = evaluateOptions
|
|
161
201
|
|
|
162
202
|
this.c = {
|
|
163
203
|
...Object.fromEntries(
|
|
@@ -488,6 +528,28 @@ export class MdastToJsx {
|
|
|
488
528
|
return null
|
|
489
529
|
}
|
|
490
530
|
|
|
531
|
+
evaluateExpression(expression: any) {
|
|
532
|
+
const hasScope = this.scope && Object.keys(this.scope).length > 0
|
|
533
|
+
const context = hasScope ? this.scope : undefined
|
|
534
|
+
const options = hasScope || this.evaluateOptions
|
|
535
|
+
? { ...(hasScope ? { functions: true } : {}), ...this.evaluateOptions }
|
|
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
|
+
|
|
550
|
+
return Evaluate.evaluate.sync(expression, context, options)
|
|
551
|
+
}
|
|
552
|
+
|
|
491
553
|
getJsxAttrs(
|
|
492
554
|
node: MdxJsxFlowElement | MdxJsxTextElement,
|
|
493
555
|
onError: (err: SafeMdxError) => void = console.error,
|
|
@@ -500,14 +562,15 @@ export class MdastToJsx {
|
|
|
500
562
|
if (attr.data?.estree) {
|
|
501
563
|
try {
|
|
502
564
|
const program = attr.data.estree
|
|
565
|
+
const firstBody = program.body?.[0]
|
|
503
566
|
if (
|
|
504
|
-
|
|
505
|
-
|
|
567
|
+
firstBody &&
|
|
568
|
+
firstBody.type === 'ExpressionStatement'
|
|
506
569
|
) {
|
|
507
|
-
const expression =
|
|
570
|
+
const expression = firstBody.expression
|
|
508
571
|
try {
|
|
509
572
|
const result =
|
|
510
|
-
|
|
573
|
+
this.evaluateExpression(expression)
|
|
511
574
|
|
|
512
575
|
// Handle spread syntax - merge the evaluated object
|
|
513
576
|
if (
|
|
@@ -597,11 +660,12 @@ export class MdastToJsx {
|
|
|
597
660
|
try {
|
|
598
661
|
// Extract the expression from the Program body
|
|
599
662
|
const program = v.data.estree
|
|
663
|
+
const firstBody = program.body?.[0]
|
|
600
664
|
if (
|
|
601
|
-
|
|
602
|
-
|
|
665
|
+
firstBody &&
|
|
666
|
+
firstBody.type === 'ExpressionStatement'
|
|
603
667
|
) {
|
|
604
|
-
const expression =
|
|
668
|
+
const expression = firstBody.expression
|
|
605
669
|
|
|
606
670
|
// Check if this is a JSX element
|
|
607
671
|
if (expression.type === 'JSXElement') {
|
|
@@ -620,7 +684,7 @@ export class MdastToJsx {
|
|
|
620
684
|
try {
|
|
621
685
|
// Evaluate the expression synchronously
|
|
622
686
|
const result =
|
|
623
|
-
|
|
687
|
+
this.evaluateExpression(expression)
|
|
624
688
|
attrsList.push([attr.name, result])
|
|
625
689
|
continue
|
|
626
690
|
} catch (error) {
|
|
@@ -653,7 +717,7 @@ export class MdastToJsx {
|
|
|
653
717
|
}
|
|
654
718
|
|
|
655
719
|
run() {
|
|
656
|
-
const res = this.mdastTransformer(this.mdast, 'root')
|
|
720
|
+
const res = this.mdastTransformer(this.mdast, 'root')
|
|
657
721
|
if (Array.isArray(res) && res.length === 1) {
|
|
658
722
|
return res[0]
|
|
659
723
|
}
|
|
@@ -669,7 +733,7 @@ export class MdastToJsx {
|
|
|
669
733
|
if (this.renderNode) {
|
|
670
734
|
const customResult = this.renderNode(
|
|
671
735
|
node,
|
|
672
|
-
(n
|
|
736
|
+
(n) => this.mdastTransformer(n, node.type),
|
|
673
737
|
)
|
|
674
738
|
if (customResult !== undefined) {
|
|
675
739
|
return customResult
|
|
@@ -723,15 +787,16 @@ export class MdastToJsx {
|
|
|
723
787
|
try {
|
|
724
788
|
// Extract the expression from the Program body
|
|
725
789
|
const program = node.data.estree
|
|
790
|
+
const firstBody = program.body?.[0]
|
|
726
791
|
if (
|
|
727
|
-
|
|
728
|
-
|
|
792
|
+
firstBody &&
|
|
793
|
+
firstBody.type === 'ExpressionStatement'
|
|
729
794
|
) {
|
|
730
|
-
const expression =
|
|
795
|
+
const expression = firstBody.expression
|
|
731
796
|
try {
|
|
732
797
|
// Evaluate the expression synchronously
|
|
733
798
|
const result =
|
|
734
|
-
|
|
799
|
+
this.evaluateExpression(expression)
|
|
735
800
|
return result
|
|
736
801
|
} catch (error) {
|
|
737
802
|
this.pushError({
|
|
@@ -1060,9 +1125,6 @@ export class MdastToJsx {
|
|
|
1060
1125
|
}
|
|
1061
1126
|
}
|
|
1062
1127
|
|
|
1063
|
-
function isTruthy<T>(val: T | undefined | null | false): val is T {
|
|
1064
|
-
return Boolean(val)
|
|
1065
|
-
}
|
|
1066
1128
|
|
|
1067
1129
|
function accessWithDot(obj, path: string) {
|
|
1068
1130
|
return path
|
|
@@ -1093,14 +1155,239 @@ export function mdastBfs(
|
|
|
1093
1155
|
return result
|
|
1094
1156
|
}
|
|
1095
1157
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1158
|
+
type ComponentsMap = { [k in (typeof nativeTags)[number]]?: any } & {
|
|
1159
|
+
[key: string]: any
|
|
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
|
+
}
|
|
1101
1258
|
}
|
|
1102
1259
|
}
|
|
1103
1260
|
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
+
}
|
|
1106
1393
|
}
|
package/src/streaming.tsx
CHANGED
|
@@ -13,6 +13,7 @@ function matchJsxTag(code: string) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const [fullMatch, tagName, attributes, selfClosing] = match
|
|
16
|
+
if (!tagName) return null
|
|
16
17
|
|
|
17
18
|
const type = selfClosing
|
|
18
19
|
? 'self-closing'
|
|
@@ -24,7 +25,7 @@ function matchJsxTag(code: string) {
|
|
|
24
25
|
tag: fullMatch,
|
|
25
26
|
tagName,
|
|
26
27
|
type,
|
|
27
|
-
attributes: attributes.trim(),
|
|
28
|
+
attributes: (attributes ?? '').trim(),
|
|
28
29
|
startIndex: match.index,
|
|
29
30
|
endIndex: match.index + fullMatch.length,
|
|
30
31
|
}
|