@variojs/core 0.1.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/__tests__/README.md +101 -0
- package/__tests__/errors.test.ts +139 -0
- package/__tests__/expression/cache.test.ts +118 -0
- package/__tests__/expression/compiler.test.ts +111 -0
- package/__tests__/expression/evaluate.test.ts +95 -0
- package/__tests__/expression/parser.test.ts +57 -0
- package/__tests__/expression/whitelist.test.ts +59 -0
- package/__tests__/performance.test.ts +379 -0
- package/__tests__/runtime/create-context.test.ts +78 -0
- package/__tests__/runtime/loop-context-pool.test.ts +74 -0
- package/__tests__/runtime/path.test.ts +128 -0
- package/__tests__/vm/executor-timeout.test.ts +117 -0
- package/__tests__/vm/executor.test.ts +173 -0
- package/__tests__/vm/handlers/array.test.ts +113 -0
- package/__tests__/vm/handlers/call.test.ts +93 -0
- package/dist/errors.d.ts +100 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +132 -0
- package/dist/errors.js.map +1 -0
- package/dist/expression/cache.d.ts +53 -0
- package/dist/expression/cache.d.ts.map +1 -0
- package/dist/expression/cache.js +158 -0
- package/dist/expression/cache.js.map +1 -0
- package/dist/expression/compiler.d.ts +34 -0
- package/dist/expression/compiler.d.ts.map +1 -0
- package/dist/expression/compiler.js +123 -0
- package/dist/expression/compiler.js.map +1 -0
- package/dist/expression/dependencies.d.ts +17 -0
- package/dist/expression/dependencies.d.ts.map +1 -0
- package/dist/expression/dependencies.js +106 -0
- package/dist/expression/dependencies.js.map +1 -0
- package/dist/expression/evaluate.d.ts +22 -0
- package/dist/expression/evaluate.d.ts.map +1 -0
- package/dist/expression/evaluate.js +75 -0
- package/dist/expression/evaluate.js.map +1 -0
- package/dist/expression/evaluator.d.ts +22 -0
- package/dist/expression/evaluator.d.ts.map +1 -0
- package/dist/expression/evaluator.js +506 -0
- package/dist/expression/evaluator.js.map +1 -0
- package/dist/expression/index.d.ts +12 -0
- package/dist/expression/index.d.ts.map +1 -0
- package/dist/expression/index.js +12 -0
- package/dist/expression/index.js.map +1 -0
- package/dist/expression/parser.d.ts +15 -0
- package/dist/expression/parser.d.ts.map +1 -0
- package/dist/expression/parser.js +42 -0
- package/dist/expression/parser.js.map +1 -0
- package/dist/expression/utils.d.ts +46 -0
- package/dist/expression/utils.d.ts.map +1 -0
- package/dist/expression/utils.js +78 -0
- package/dist/expression/utils.js.map +1 -0
- package/dist/expression/whitelist.d.ts +24 -0
- package/dist/expression/whitelist.d.ts.map +1 -0
- package/dist/expression/whitelist.js +198 -0
- package/dist/expression/whitelist.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/context.d.ts +8 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/context.js +7 -0
- package/dist/runtime/context.js.map +1 -0
- package/dist/runtime/create-context.d.ts +50 -0
- package/dist/runtime/create-context.d.ts.map +1 -0
- package/dist/runtime/create-context.js +73 -0
- package/dist/runtime/create-context.js.map +1 -0
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/loop-context-pool.d.ts +58 -0
- package/dist/runtime/loop-context-pool.d.ts.map +1 -0
- package/dist/runtime/loop-context-pool.js +114 -0
- package/dist/runtime/loop-context-pool.js.map +1 -0
- package/dist/runtime/path.d.ts +114 -0
- package/dist/runtime/path.d.ts.map +1 -0
- package/dist/runtime/path.js +302 -0
- package/dist/runtime/path.js.map +1 -0
- package/dist/runtime/proxy.d.ts +18 -0
- package/dist/runtime/proxy.d.ts.map +1 -0
- package/dist/runtime/proxy.js +53 -0
- package/dist/runtime/proxy.js.map +1 -0
- package/dist/runtime/sandbox.d.ts +20 -0
- package/dist/runtime/sandbox.d.ts.map +1 -0
- package/dist/runtime/sandbox.js +32 -0
- package/dist/runtime/sandbox.js.map +1 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- package/dist/vm/action.d.ts +11 -0
- package/dist/vm/action.d.ts.map +1 -0
- package/dist/vm/action.js +10 -0
- package/dist/vm/action.js.map +1 -0
- package/dist/vm/errors.d.ts +5 -0
- package/dist/vm/errors.d.ts.map +1 -0
- package/dist/vm/errors.js +5 -0
- package/dist/vm/errors.js.map +1 -0
- package/dist/vm/executor.d.ts +35 -0
- package/dist/vm/executor.d.ts.map +1 -0
- package/dist/vm/executor.js +137 -0
- package/dist/vm/executor.js.map +1 -0
- package/dist/vm/handlers/array/pop.d.ts +12 -0
- package/dist/vm/handlers/array/pop.d.ts.map +1 -0
- package/dist/vm/handlers/array/pop.js +28 -0
- package/dist/vm/handlers/array/pop.js.map +1 -0
- package/dist/vm/handlers/array/push.d.ts +13 -0
- package/dist/vm/handlers/array/push.d.ts.map +1 -0
- package/dist/vm/handlers/array/push.js +42 -0
- package/dist/vm/handlers/array/push.js.map +1 -0
- package/dist/vm/handlers/array/shift.d.ts +12 -0
- package/dist/vm/handlers/array/shift.d.ts.map +1 -0
- package/dist/vm/handlers/array/shift.js +28 -0
- package/dist/vm/handlers/array/shift.js.map +1 -0
- package/dist/vm/handlers/array/splice.d.ts +12 -0
- package/dist/vm/handlers/array/splice.d.ts.map +1 -0
- package/dist/vm/handlers/array/splice.js +59 -0
- package/dist/vm/handlers/array/splice.js.map +1 -0
- package/dist/vm/handlers/array/unshift.d.ts +13 -0
- package/dist/vm/handlers/array/unshift.d.ts.map +1 -0
- package/dist/vm/handlers/array/unshift.js +42 -0
- package/dist/vm/handlers/array/unshift.js.map +1 -0
- package/dist/vm/handlers/array/utils.d.ts +10 -0
- package/dist/vm/handlers/array/utils.d.ts.map +1 -0
- package/dist/vm/handlers/array/utils.js +33 -0
- package/dist/vm/handlers/array/utils.js.map +1 -0
- package/dist/vm/handlers/batch.d.ts +12 -0
- package/dist/vm/handlers/batch.d.ts.map +1 -0
- package/dist/vm/handlers/batch.js +40 -0
- package/dist/vm/handlers/batch.js.map +1 -0
- package/dist/vm/handlers/call.d.ts +14 -0
- package/dist/vm/handlers/call.d.ts.map +1 -0
- package/dist/vm/handlers/call.js +65 -0
- package/dist/vm/handlers/call.js.map +1 -0
- package/dist/vm/handlers/emit.d.ts +12 -0
- package/dist/vm/handlers/emit.d.ts.map +1 -0
- package/dist/vm/handlers/emit.js +26 -0
- package/dist/vm/handlers/emit.js.map +1 -0
- package/dist/vm/handlers/if.d.ts +13 -0
- package/dist/vm/handlers/if.d.ts.map +1 -0
- package/dist/vm/handlers/if.js +35 -0
- package/dist/vm/handlers/if.js.map +1 -0
- package/dist/vm/handlers/index.d.ts +11 -0
- package/dist/vm/handlers/index.d.ts.map +1 -0
- package/dist/vm/handlers/index.js +46 -0
- package/dist/vm/handlers/index.js.map +1 -0
- package/dist/vm/handlers/log.d.ts +12 -0
- package/dist/vm/handlers/log.d.ts.map +1 -0
- package/dist/vm/handlers/log.js +41 -0
- package/dist/vm/handlers/log.js.map +1 -0
- package/dist/vm/handlers/loop.d.ts +12 -0
- package/dist/vm/handlers/loop.d.ts.map +1 -0
- package/dist/vm/handlers/loop.js +71 -0
- package/dist/vm/handlers/loop.js.map +1 -0
- package/dist/vm/handlers/navigate.d.ts +12 -0
- package/dist/vm/handlers/navigate.d.ts.map +1 -0
- package/dist/vm/handlers/navigate.js +43 -0
- package/dist/vm/handlers/navigate.js.map +1 -0
- package/dist/vm/handlers/set.d.ts +15 -0
- package/dist/vm/handlers/set.d.ts.map +1 -0
- package/dist/vm/handlers/set.js +30 -0
- package/dist/vm/handlers/set.js.map +1 -0
- package/dist/vm/index.d.ts +8 -0
- package/dist/vm/index.d.ts.map +1 -0
- package/dist/vm/index.js +7 -0
- package/dist/vm/index.js.map +1 -0
- package/package.json +34 -0
- package/src/errors.ts +194 -0
- package/src/expression/README.md +192 -0
- package/src/expression/cache.ts +199 -0
- package/src/expression/compiler.ts +144 -0
- package/src/expression/dependencies.ts +116 -0
- package/src/expression/evaluate.ts +95 -0
- package/src/expression/evaluator.ts +640 -0
- package/src/expression/index.ts +27 -0
- package/src/expression/parser.ts +54 -0
- package/src/expression/utils.ts +89 -0
- package/src/expression/whitelist.ts +224 -0
- package/src/globals.d.ts +10 -0
- package/src/index.ts +72 -0
- package/src/runtime/context.ts +8 -0
- package/src/runtime/create-context.ts +133 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/loop-context-pool.ts +134 -0
- package/src/runtime/path.ts +372 -0
- package/src/runtime/proxy.ts +66 -0
- package/src/runtime/sandbox.ts +43 -0
- package/src/types.ts +177 -0
- package/src/vm/errors.ts +10 -0
- package/src/vm/executor.ts +210 -0
- package/src/vm/handlers/array/pop.ts +47 -0
- package/src/vm/handlers/array/push.ts +68 -0
- package/src/vm/handlers/array/shift.ts +47 -0
- package/src/vm/handlers/array/splice.ts +78 -0
- package/src/vm/handlers/array/unshift.ts +68 -0
- package/src/vm/handlers/array/utils.ts +41 -0
- package/src/vm/handlers/batch.ts +57 -0
- package/src/vm/handlers/call.ts +92 -0
- package/src/vm/handlers/emit.ts +39 -0
- package/src/vm/handlers/if.ts +48 -0
- package/src/vm/handlers/index.ts +52 -0
- package/src/vm/handlers/log.ts +54 -0
- package/src/vm/handlers/loop.ts +102 -0
- package/src/vm/handlers/navigate.ts +64 -0
- package/src/vm/handlers/set.ts +43 -0
- package/src/vm/index.ts +8 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +30 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表达式解析器
|
|
3
|
+
*
|
|
4
|
+
* 使用 @babel/parser 解析 JavaScript 表达式为 AST
|
|
5
|
+
* 移除 models. 前缀支持
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parse } from '@babel/parser'
|
|
9
|
+
import type * as ESTree from '@babel/types'
|
|
10
|
+
import { ExpressionError, ErrorCodes } from '../errors.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 解析表达式为 AST
|
|
14
|
+
*
|
|
15
|
+
* @param expr 表达式字符串(如 "user.name + 1")
|
|
16
|
+
* @returns ESTree.Node AST 节点
|
|
17
|
+
*/
|
|
18
|
+
export function parseExpression(expr: string): ESTree.Node {
|
|
19
|
+
try {
|
|
20
|
+
// 解析为表达式(ExpressionStatement)
|
|
21
|
+
const ast = parse(`(${expr})`, {
|
|
22
|
+
plugins: ['typescript'],
|
|
23
|
+
sourceType: 'module',
|
|
24
|
+
allowReturnOutsideFunction: true,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// 提取表达式部分
|
|
28
|
+
const statement = ast.program.body[0]
|
|
29
|
+
if (statement?.type === 'ExpressionStatement') {
|
|
30
|
+
return statement.expression
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new ExpressionError(
|
|
34
|
+
expr,
|
|
35
|
+
`Failed to parse expression: ${expr}`,
|
|
36
|
+
ErrorCodes.EXPRESSION_PARSE_ERROR
|
|
37
|
+
)
|
|
38
|
+
} catch (error: unknown) {
|
|
39
|
+
if (error instanceof ExpressionError) {
|
|
40
|
+
throw error
|
|
41
|
+
}
|
|
42
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
43
|
+
throw new ExpressionError(
|
|
44
|
+
expr,
|
|
45
|
+
`Expression parse error: ${errorMessage}`,
|
|
46
|
+
ErrorCodes.EXPRESSION_PARSE_ERROR,
|
|
47
|
+
{
|
|
48
|
+
metadata: {
|
|
49
|
+
originalError: error instanceof Error ? error.name : 'Unknown'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表达式工具函数
|
|
3
|
+
*
|
|
4
|
+
* 提供表达式解析、格式化的通用工具
|
|
5
|
+
* 支持 {{ }} 格式的表达式提取和规范化
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 提取表达式字符串
|
|
10
|
+
* 支持 {{ expression }} 格式,自动去掉包装
|
|
11
|
+
*
|
|
12
|
+
* @param expr 表达式字符串,可能是 "{{ showContent }}" 或 "showContent"
|
|
13
|
+
* @returns 去掉 {{ }} 包装后的表达式字符串
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* extractExpression("{{ showContent }}") // => "showContent"
|
|
18
|
+
* extractExpression("showContent") // => "showContent"
|
|
19
|
+
* extractExpression("{{ userRole === 'admin' }}") // => "userRole === 'admin'"
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function extractExpression(expr: string): string {
|
|
23
|
+
if (typeof expr !== 'string') {
|
|
24
|
+
return String(expr)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 如果表达式是 {{ }} 格式,去掉包装
|
|
28
|
+
if (expr.startsWith('{{') && expr.endsWith('}}')) {
|
|
29
|
+
return expr.slice(2, -2).trim()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return expr.trim()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 规范化表达式字符串
|
|
37
|
+
* 确保表达式格式统一,便于后续处理
|
|
38
|
+
*
|
|
39
|
+
* @param expr 表达式字符串
|
|
40
|
+
* @returns 规范化后的表达式字符串(去掉 {{ }} 包装)
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeExpression(expr: string): string {
|
|
43
|
+
return extractExpression(expr)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 检查是否为表达式格式
|
|
48
|
+
*
|
|
49
|
+
* @param value 要检查的值
|
|
50
|
+
* @returns 如果是 {{ }} 格式返回 true
|
|
51
|
+
*/
|
|
52
|
+
export function isExpressionFormat(value: any): boolean {
|
|
53
|
+
return typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 批量提取表达式
|
|
58
|
+
* 从对象或数组中提取所有表达式字符串
|
|
59
|
+
*
|
|
60
|
+
* @param value 要处理的值(对象、数组或字符串)
|
|
61
|
+
* @param extractor 提取函数,默认使用 extractExpression
|
|
62
|
+
* @returns 处理后的值
|
|
63
|
+
*/
|
|
64
|
+
export function extractExpressionsRecursively(
|
|
65
|
+
value: any,
|
|
66
|
+
extractor: (expr: string) => string = extractExpression
|
|
67
|
+
): any {
|
|
68
|
+
// 字符串:检查是否为表达式
|
|
69
|
+
if (typeof value === 'string') {
|
|
70
|
+
return extractor(value)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 数组:递归处理每个元素
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
return value.map(item => extractExpressionsRecursively(item, extractor))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 对象:递归处理每个属性值
|
|
79
|
+
if (value && typeof value === 'object') {
|
|
80
|
+
const result: Record<string, any> = {}
|
|
81
|
+
for (const [key, val] of Object.entries(value)) {
|
|
82
|
+
result[key] = extractExpressionsRecursively(val, extractor)
|
|
83
|
+
}
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 其他类型:直接返回
|
|
88
|
+
return value
|
|
89
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST 白名单验证器
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 深度遍历 AST,检查每个节点
|
|
6
|
+
* - 只允许安全的语法节点
|
|
7
|
+
* - 禁止危险的语法(赋值、函数、this 等)
|
|
8
|
+
* - 检查函数调用中的函数名是否在白名单中
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type * as ESTree from '@babel/types'
|
|
12
|
+
import { ExpressionError, ErrorCodes } from '../errors.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 白名单全局函数
|
|
16
|
+
*/
|
|
17
|
+
const WHITELISTED_GLOBALS = new Set([
|
|
18
|
+
'String', 'Number', 'Boolean', 'BigInt', 'Symbol',
|
|
19
|
+
'Array', 'Object', 'Math', 'Date',
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 白名单函数(带命名空间)
|
|
24
|
+
*/
|
|
25
|
+
const WHITELISTED_FUNCTIONS = new Set([
|
|
26
|
+
'Array.isArray',
|
|
27
|
+
'Object.is',
|
|
28
|
+
'Number.isFinite',
|
|
29
|
+
'Number.isInteger',
|
|
30
|
+
'Number.isNaN',
|
|
31
|
+
'Number.isSafeInteger',
|
|
32
|
+
'Math.abs',
|
|
33
|
+
'Math.round',
|
|
34
|
+
'Math.floor',
|
|
35
|
+
'Math.ceil',
|
|
36
|
+
'Math.random',
|
|
37
|
+
'Math.max',
|
|
38
|
+
'Math.min',
|
|
39
|
+
'Date.now',
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 检查名称是否为全局对象名称
|
|
44
|
+
*/
|
|
45
|
+
function isGlobalObjectName(name: string): boolean {
|
|
46
|
+
return ['window', 'document', 'global', 'globalThis', 'self'].includes(name)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 允许的 AST 节点类型
|
|
51
|
+
*
|
|
52
|
+
* 注意:@babel/types 可能使用更具体的字面量类型(如 NumericLiteral, StringLiteral)
|
|
53
|
+
* 这些都应该被允许,因为它们都是 Literal 的子类型
|
|
54
|
+
*/
|
|
55
|
+
const ALLOWED_NODE_TYPES = new Set([
|
|
56
|
+
'MemberExpression', // 成员访问:user.name
|
|
57
|
+
'OptionalMemberExpression', // 可选链:user?.name
|
|
58
|
+
'ArrayExpression', // 数组字面量:[1, 2, 3]
|
|
59
|
+
'ObjectExpression', // 对象字面量:{ a: 1 }
|
|
60
|
+
'ObjectProperty', // 对象属性:{ a: 1 } 中的 a: 1
|
|
61
|
+
'Literal', // 字面量:'string', 123, true
|
|
62
|
+
'NumericLiteral', // 数字字面量:123(@babel/types 的具体类型)
|
|
63
|
+
'StringLiteral', // 字符串字面量:'string'
|
|
64
|
+
'BooleanLiteral', // 布尔字面量:true, false
|
|
65
|
+
'NullLiteral', // null 字面量
|
|
66
|
+
'Identifier', // 标识符:user, name
|
|
67
|
+
'BinaryExpression', // 二元运算:a + b
|
|
68
|
+
'LogicalExpression', // 逻辑运算:a && b
|
|
69
|
+
'UnaryExpression', // 一元运算:!a, -b
|
|
70
|
+
'ConditionalExpression', // 三元表达式:a ? b : c
|
|
71
|
+
'CallExpression', // 函数调用:Math.max()
|
|
72
|
+
'TemplateLiteral', // 模板字符串:`${name}`
|
|
73
|
+
'SequenceExpression', // 序列表达式:(a, b)
|
|
74
|
+
'NullishCoalescingExpression', // 空值合并:a ?? b
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 禁止的 AST 节点类型
|
|
79
|
+
*/
|
|
80
|
+
const FORBIDDEN_NODE_TYPES = new Set([
|
|
81
|
+
'AssignmentExpression', // 赋值:a = b
|
|
82
|
+
'UpdateExpression', // 自增/自减:a++, --b
|
|
83
|
+
'FunctionExpression', // 函数表达式:function() {}
|
|
84
|
+
'ArrowFunctionExpression', // 箭头函数:() => {}
|
|
85
|
+
'ThisExpression', // this
|
|
86
|
+
'NewExpression', // new 运算符
|
|
87
|
+
'YieldExpression', // yield
|
|
88
|
+
'AwaitExpression', // await
|
|
89
|
+
'ImportExpression', // import()
|
|
90
|
+
'MetaProperty', // import.meta
|
|
91
|
+
'SpreadElement', // 展开运算符:...array(在函数调用中不安全)
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 默认最大嵌套深度(防止 DoS 攻击)
|
|
96
|
+
*/
|
|
97
|
+
const DEFAULT_MAX_NESTING_DEPTH = 50
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 验证 AST 是否通过白名单检查
|
|
101
|
+
*
|
|
102
|
+
* @param ast AST 节点
|
|
103
|
+
* @param options 验证选项
|
|
104
|
+
* @param options.allowGlobals 是否允许全局函数调用(跳过白名单检查)
|
|
105
|
+
* @param options.maxNestingDepth 最大嵌套深度(默认 50)
|
|
106
|
+
* @throws ExpressionError 如果发现禁止的节点
|
|
107
|
+
*/
|
|
108
|
+
export function validateAST(ast: ESTree.Node, options?: { allowGlobals?: boolean; maxNestingDepth?: number }): void {
|
|
109
|
+
const allowGlobals = options?.allowGlobals === true
|
|
110
|
+
const maxNestingDepth = options?.maxNestingDepth ?? DEFAULT_MAX_NESTING_DEPTH
|
|
111
|
+
const errors: string[] = []
|
|
112
|
+
|
|
113
|
+
function getFunctionName(callee: ESTree.Node): string | null {
|
|
114
|
+
if (callee.type === 'Identifier') {
|
|
115
|
+
return callee.name
|
|
116
|
+
}
|
|
117
|
+
if (callee.type === 'MemberExpression') {
|
|
118
|
+
const member = callee as ESTree.MemberExpression
|
|
119
|
+
if (member.object.type === 'Identifier' && member.property.type === 'Identifier') {
|
|
120
|
+
return `${member.object.name}.${member.property.name}`
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function traverse(node: ESTree.Node, path: string = 'root', depth: number = 0): void {
|
|
127
|
+
// 检查嵌套深度(防止 DoS 攻击)
|
|
128
|
+
if (depth > maxNestingDepth) {
|
|
129
|
+
errors.push(`Maximum nesting depth (${maxNestingDepth}) exceeded at ${path}`)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 检查节点类型
|
|
134
|
+
if (FORBIDDEN_NODE_TYPES.has(node.type)) {
|
|
135
|
+
errors.push(`Forbidden node type "${node.type}" at ${path}`)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 检查函数调用中的函数名
|
|
140
|
+
if (node.type === 'CallExpression') {
|
|
141
|
+
const call = node as ESTree.CallExpression
|
|
142
|
+
const funcName = getFunctionName(call.callee)
|
|
143
|
+
if (funcName) {
|
|
144
|
+
// 检查是否为危险函数(eval, Function, etc.)- 即使 allowGlobals 也禁止
|
|
145
|
+
const dangerousFunctions = ['eval', 'Function', 'setTimeout', 'setInterval', 'execScript']
|
|
146
|
+
if (dangerousFunctions.includes(funcName) || funcName.startsWith('eval') || funcName.startsWith('Function')) {
|
|
147
|
+
errors.push(`Dangerous function "${funcName}" is not allowed at ${path}`)
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 如果 allowGlobals,跳过白名单检查
|
|
152
|
+
if (!allowGlobals) {
|
|
153
|
+
// 检查是否为全局函数调用(如 Array.isArray, Math.max)
|
|
154
|
+
const isGlobalFunction =
|
|
155
|
+
WHITELISTED_FUNCTIONS.has(funcName) ||
|
|
156
|
+
WHITELISTED_GLOBALS.has(funcName.split('.')[0])
|
|
157
|
+
|
|
158
|
+
// 检查是否为对象方法调用(如 array.slice, user.getName)
|
|
159
|
+
// 如果 callee 是 MemberExpression 且 object 不是全局对象,则允许
|
|
160
|
+
let isObjectMethod = false
|
|
161
|
+
if (call.callee.type === 'MemberExpression') {
|
|
162
|
+
const member = call.callee as ESTree.MemberExpression
|
|
163
|
+
// 如果 object 是 Identifier(变量名),且不在全局对象白名单中,则认为是对象方法
|
|
164
|
+
if (member.object.type === 'Identifier') {
|
|
165
|
+
const objName = member.object.name
|
|
166
|
+
// 不是全局对象名称,则认为是用户数据的对象方法
|
|
167
|
+
if (!WHITELISTED_GLOBALS.has(objName) && !isGlobalObjectName(objName)) {
|
|
168
|
+
isObjectMethod = true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!isGlobalFunction && !isObjectMethod && !funcName.startsWith('$')) {
|
|
174
|
+
// 不在白名单中的函数,禁止调用
|
|
175
|
+
errors.push(`Function "${funcName}" is not in whitelist at ${path}`)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!ALLOWED_NODE_TYPES.has(node.type) && node.type !== 'Program') {
|
|
183
|
+
// 未知节点类型,保守策略:禁止
|
|
184
|
+
errors.push(`Unknown node type "${node.type}" at ${path}`)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 递归遍历子节点
|
|
189
|
+
for (const key in node) {
|
|
190
|
+
const value = (node as unknown as Record<string, unknown>)[key]
|
|
191
|
+
|
|
192
|
+
if (value == null) continue
|
|
193
|
+
|
|
194
|
+
// 处理数组
|
|
195
|
+
if (Array.isArray(value)) {
|
|
196
|
+
value.forEach((child, index) => {
|
|
197
|
+
if (child && typeof child === 'object' && 'type' in child) {
|
|
198
|
+
traverse(child as ESTree.Node, `${path}[${index}]`, depth + 1)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
// 处理对象节点
|
|
203
|
+
else if (value && typeof value === 'object' && 'type' in value) {
|
|
204
|
+
traverse(value as ESTree.Node, `${path}.${key}`, depth + 1)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
traverse(ast, 'root', 0)
|
|
210
|
+
|
|
211
|
+
if (errors.length > 0) {
|
|
212
|
+
throw new ExpressionError(
|
|
213
|
+
JSON.stringify(ast),
|
|
214
|
+
`AST validation failed:\n${errors.join('\n')}`,
|
|
215
|
+
ErrorCodes.EXPRESSION_VALIDATION_ERROR,
|
|
216
|
+
{
|
|
217
|
+
metadata: {
|
|
218
|
+
errors,
|
|
219
|
+
nodeType: ast.type
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
}
|
package/src/globals.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vario/core - Vario Core Runtime
|
|
3
|
+
*
|
|
4
|
+
* 核心运行时模块,包含:
|
|
5
|
+
* - RuntimeContext: 扁平化状态 + $ 前缀系统 API
|
|
6
|
+
* - Expression System: 安全表达式求值
|
|
7
|
+
* - Action VM: 动作虚拟机
|
|
8
|
+
* - Path Utilities: 统一的路径解析工具
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Runtime
|
|
12
|
+
export {
|
|
13
|
+
createRuntimeContext,
|
|
14
|
+
createProxy,
|
|
15
|
+
createExpressionSandbox,
|
|
16
|
+
isSafePropertyAccess,
|
|
17
|
+
// 循环上下文(供框架集成层使用)
|
|
18
|
+
createLoopContext,
|
|
19
|
+
releaseLoopContext,
|
|
20
|
+
// 路径工具(供框架集成层使用)
|
|
21
|
+
parsePath,
|
|
22
|
+
parsePathCached,
|
|
23
|
+
clearPathCache,
|
|
24
|
+
stringifyPath,
|
|
25
|
+
getPathValue,
|
|
26
|
+
setPathValue,
|
|
27
|
+
matchPath,
|
|
28
|
+
getParentPath,
|
|
29
|
+
getLastSegment
|
|
30
|
+
} from './runtime/index.js'
|
|
31
|
+
export type { RuntimeContext } from './types.js'
|
|
32
|
+
export type { PathSegment } from './runtime/path.js'
|
|
33
|
+
export type { CreateContextOptions } from './runtime/create-context.js'
|
|
34
|
+
|
|
35
|
+
// Expression
|
|
36
|
+
export {
|
|
37
|
+
parseExpression,
|
|
38
|
+
validateAST,
|
|
39
|
+
evaluateExpression,
|
|
40
|
+
evaluate,
|
|
41
|
+
extractExpression,
|
|
42
|
+
extractDependencies,
|
|
43
|
+
getCachedExpression,
|
|
44
|
+
setCachedExpression,
|
|
45
|
+
invalidateCache,
|
|
46
|
+
clearCache
|
|
47
|
+
} from './expression/index.js'
|
|
48
|
+
|
|
49
|
+
// VM
|
|
50
|
+
export {
|
|
51
|
+
execute,
|
|
52
|
+
registerBuiltinMethods
|
|
53
|
+
} from './vm/index.js'
|
|
54
|
+
export type { ExecuteOptions } from './vm/index.js'
|
|
55
|
+
export {
|
|
56
|
+
ActionError,
|
|
57
|
+
ExpressionError,
|
|
58
|
+
ServiceError,
|
|
59
|
+
BatchError,
|
|
60
|
+
VarioError,
|
|
61
|
+
ErrorCodes,
|
|
62
|
+
type ErrorContext,
|
|
63
|
+
type ErrorCode
|
|
64
|
+
} from './errors.js'
|
|
65
|
+
export type {
|
|
66
|
+
Action,
|
|
67
|
+
ActionMap,
|
|
68
|
+
ActionHandler,
|
|
69
|
+
ExpressionCache,
|
|
70
|
+
MethodsRegistry,
|
|
71
|
+
ExpressionOptions
|
|
72
|
+
} from './types.js'
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuntimeContext 创建工厂
|
|
3
|
+
*
|
|
4
|
+
* 实现要点:
|
|
5
|
+
* - 扁平化状态存储
|
|
6
|
+
* - 系统 API 保护($ 和 _ 前缀)
|
|
7
|
+
* - 命名冲突检测
|
|
8
|
+
* - Proxy 拦截防止覆盖系统 API
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { RuntimeContext, MethodsRegistry, GetPathValue, SetPathValue, ExpressionOptions } from '../types.js'
|
|
12
|
+
import { createProxy } from './proxy.js'
|
|
13
|
+
import { registerBuiltinMethods } from '../vm/handlers/index.js'
|
|
14
|
+
import { getPathValue as getPath, setPathValue as setPath } from './path.js'
|
|
15
|
+
import { invalidateCache } from '../expression/cache.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* RuntimeContext 创建选项
|
|
19
|
+
*/
|
|
20
|
+
export interface CreateContextOptions {
|
|
21
|
+
/**
|
|
22
|
+
* 事件发射回调
|
|
23
|
+
*/
|
|
24
|
+
onEmit?: (event: string, data?: unknown) => void
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 预注册的方法
|
|
28
|
+
*/
|
|
29
|
+
methods?: MethodsRegistry
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 状态变更钩子(用于框架集成)
|
|
33
|
+
* 在 _set 调用后触发,可用于缓存失效等
|
|
34
|
+
*/
|
|
35
|
+
onStateChange?: (path: string, value: unknown, ctx: RuntimeContext) => void
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 创建对象的工厂函数(用于框架集成创建响应式对象)
|
|
39
|
+
*/
|
|
40
|
+
createObject?: () => Record<string, unknown>
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 创建数组的工厂函数(用于框架集成创建响应式数组)
|
|
44
|
+
*/
|
|
45
|
+
createArray?: () => unknown[]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 表达式求值配置
|
|
49
|
+
*/
|
|
50
|
+
exprOptions?: ExpressionOptions
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 创建运行时上下文
|
|
55
|
+
*
|
|
56
|
+
* @param initialState 初始状态(扁平化)
|
|
57
|
+
* @param options 配置选项
|
|
58
|
+
*
|
|
59
|
+
* @template TState 状态类型,从 initialState 推导
|
|
60
|
+
*/
|
|
61
|
+
export function createRuntimeContext<
|
|
62
|
+
TState extends Record<string, unknown> = Record<string, unknown>
|
|
63
|
+
>(
|
|
64
|
+
initialState: Partial<TState> = {} as Partial<TState>,
|
|
65
|
+
options: CreateContextOptions = {}
|
|
66
|
+
): RuntimeContext<TState> {
|
|
67
|
+
// 1. 验证命名冲突
|
|
68
|
+
validateStateKeys(initialState)
|
|
69
|
+
|
|
70
|
+
const {
|
|
71
|
+
onEmit,
|
|
72
|
+
methods = {},
|
|
73
|
+
onStateChange,
|
|
74
|
+
createObject = () => ({}),
|
|
75
|
+
createArray = () => [],
|
|
76
|
+
exprOptions
|
|
77
|
+
} = options
|
|
78
|
+
|
|
79
|
+
// 用于存储 proxied 引用,以便 _set 中的 onStateChange 能使用正确的引用
|
|
80
|
+
let proxiedRef: RuntimeContext<TState> | null = null
|
|
81
|
+
|
|
82
|
+
// 2. 创建基础上下文对象
|
|
83
|
+
const ctx = {
|
|
84
|
+
...initialState,
|
|
85
|
+
$emit: (event: string, data?: unknown) => {
|
|
86
|
+
if (onEmit) {
|
|
87
|
+
onEmit(event, data)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
$methods: methods as MethodsRegistry,
|
|
91
|
+
$exprOptions: exprOptions,
|
|
92
|
+
_get: <TPath extends string>(path: TPath): GetPathValue<TState, TPath> => {
|
|
93
|
+
return getPath(ctx as Record<string, unknown>, path) as GetPathValue<TState, TPath>
|
|
94
|
+
},
|
|
95
|
+
_set: <TPath extends string>(path: TPath, value: SetPathValue<TState, TPath>, options?: { skipCallback?: boolean }): void => {
|
|
96
|
+
setPath(ctx as Record<string, unknown>, path, value, {
|
|
97
|
+
createObject,
|
|
98
|
+
createArray
|
|
99
|
+
})
|
|
100
|
+
// 使缓存失效(使用 proxied 引用以确保缓存键一致)
|
|
101
|
+
invalidateCache(path, (proxiedRef || ctx) as RuntimeContext)
|
|
102
|
+
// 触发状态变更钩子
|
|
103
|
+
if (onStateChange && !options?.skipCallback) {
|
|
104
|
+
onStateChange(path, value, (proxiedRef || ctx) as RuntimeContext)
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
} as RuntimeContext<TState>
|
|
108
|
+
|
|
109
|
+
// 3. 自动注册内置指令到 $methods
|
|
110
|
+
registerBuiltinMethods(ctx as RuntimeContext)
|
|
111
|
+
|
|
112
|
+
// 4. 使用 Proxy 保护系统 API
|
|
113
|
+
const proxied = createProxy(ctx as unknown as RuntimeContext) as RuntimeContext<TState>
|
|
114
|
+
|
|
115
|
+
// 5. 保存 proxied 引用供 _set 使用
|
|
116
|
+
proxiedRef = proxied
|
|
117
|
+
|
|
118
|
+
return proxied
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 验证状态键名,防止与系统 API 冲突
|
|
123
|
+
*/
|
|
124
|
+
function validateStateKeys(state: Record<string, unknown>): void {
|
|
125
|
+
for (const key in state) {
|
|
126
|
+
if (key.startsWith('$') || key.startsWith('_')) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Property name "${key}" conflicts with system API. ` +
|
|
129
|
+
`Properties starting with "$" or "_" are reserved. Use a different name.`
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime 模块导出
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { createRuntimeContext } from './create-context.js'
|
|
6
|
+
export { createProxy } from './proxy.js'
|
|
7
|
+
export { createExpressionSandbox, isSafePropertyAccess } from './sandbox.js'
|
|
8
|
+
export {
|
|
9
|
+
createLoopContext,
|
|
10
|
+
releaseLoopContext,
|
|
11
|
+
getLoopContextPool,
|
|
12
|
+
clearLoopContextPool
|
|
13
|
+
} from './loop-context-pool.js'
|
|
14
|
+
export type { RuntimeContext } from '../types.js'
|
|
15
|
+
|
|
16
|
+
// 路径工具(供框架集成层使用)
|
|
17
|
+
export {
|
|
18
|
+
parsePath,
|
|
19
|
+
parsePathCached,
|
|
20
|
+
clearPathCache,
|
|
21
|
+
stringifyPath,
|
|
22
|
+
getPathValue,
|
|
23
|
+
setPathValue,
|
|
24
|
+
matchPath,
|
|
25
|
+
getParentPath,
|
|
26
|
+
getLastSegment,
|
|
27
|
+
type PathSegment
|
|
28
|
+
} from './path.js'
|