bonsai-js 0.3.0 → 0.5.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/CHANGELOG.md +41 -5
- package/README.md +84 -13
- package/dist/autocomplete/index.d.mts +1 -1
- package/dist/autocomplete/index.d.mts.map +1 -1
- package/dist/autocomplete/index.mjs +22 -61
- package/dist/autocomplete/index.mjs.map +1 -1
- package/dist/{errors-t5i9AEaf.mjs → errors-CCZDd1gK.mjs} +2 -3
- package/dist/errors-CCZDd1gK.mjs.map +1 -0
- package/dist/{execution-context-DqYC3L92.mjs → execution-context-b6FBfEdv.mjs} +147 -16
- package/dist/execution-context-b6FBfEdv.mjs.map +1 -0
- package/dist/index.d.mts +16 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +228 -111
- package/dist/index.mjs.map +1 -1
- package/dist/stdlib/index.d.mts +1 -1
- package/dist/stdlib/index.d.mts.map +1 -1
- package/dist/stdlib/index.mjs +43 -11
- package/dist/stdlib/index.mjs.map +1 -1
- package/dist/{types-BhsZZsLY.d.mts → types-DkXfy5IJ.d.mts} +59 -17
- package/dist/types-DkXfy5IJ.d.mts.map +1 -0
- package/package.json +37 -11
- package/dist/errors-t5i9AEaf.mjs.map +0 -1
- package/dist/execution-context-DqYC3L92.mjs.map +0 -1
- package/dist/types-BhsZZsLY.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as attachLocation, c as offsetToPosition, i as ExpressionError, l as suggest, n as BonsaiSecurityError, o as formatBonsaiError, r as BonsaiTypeError, s as formatError, t as BonsaiReferenceError } from "./errors-
|
|
2
|
-
import { i as
|
|
1
|
+
import { a as attachLocation, c as offsetToPosition, i as ExpressionError, l as suggest, n as BonsaiSecurityError, o as formatBonsaiError, r as BonsaiTypeError, s as formatError, t as BonsaiReferenceError } from "./errors-CCZDd1gK.mjs";
|
|
2
|
+
import { i as isMethodAllowedOn, n as ExecutionContext, o as tokenize, r as SecurityPolicy } from "./execution-context-b6FBfEdv.mjs";
|
|
3
3
|
//#region src/parser.ts
|
|
4
4
|
const PIPE_PRECEDENCE = 5;
|
|
5
5
|
const PRECEDENCE = {
|
|
@@ -23,6 +23,7 @@ const PRECEDENCE = {
|
|
|
23
23
|
"not": void 0
|
|
24
24
|
};
|
|
25
25
|
const MAX_PARSE_DEPTH = 32;
|
|
26
|
+
const MAX_GRAMMAR_DEPTH = 1e3;
|
|
26
27
|
function parse(source, _depth = 0) {
|
|
27
28
|
if (_depth > MAX_PARSE_DEPTH) throw new ExpressionError("Maximum template nesting depth exceeded", {
|
|
28
29
|
source,
|
|
@@ -31,6 +32,7 @@ function parse(source, _depth = 0) {
|
|
|
31
32
|
});
|
|
32
33
|
const tokens = tokenize(source);
|
|
33
34
|
let pos = 0;
|
|
35
|
+
let grammarDepth = 0;
|
|
34
36
|
function current() {
|
|
35
37
|
return tokens[pos];
|
|
36
38
|
}
|
|
@@ -46,7 +48,26 @@ function parse(source, _depth = 0) {
|
|
|
46
48
|
});
|
|
47
49
|
return advance();
|
|
48
50
|
}
|
|
51
|
+
function enterNesting() {
|
|
52
|
+
if (++grammarDepth > MAX_GRAMMAR_DEPTH) {
|
|
53
|
+
grammarDepth--;
|
|
54
|
+
const tok = current();
|
|
55
|
+
throw new ExpressionError("Maximum expression nesting depth exceeded", {
|
|
56
|
+
source,
|
|
57
|
+
start: tok.start,
|
|
58
|
+
end: tok.end
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
49
62
|
function parseExpression(minPrec = 0) {
|
|
63
|
+
enterNesting();
|
|
64
|
+
try {
|
|
65
|
+
return parseExpressionInner(minPrec);
|
|
66
|
+
} finally {
|
|
67
|
+
grammarDepth--;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function parseExpressionInner(minPrec = 0) {
|
|
50
71
|
let left = parseUnary();
|
|
51
72
|
if (current().type === "Punctuation" && current().value === "?" && minPrec === 0) {
|
|
52
73
|
advance();
|
|
@@ -151,6 +172,14 @@ function parse(source, _depth = 0) {
|
|
|
151
172
|
return node;
|
|
152
173
|
}
|
|
153
174
|
function parseUnary() {
|
|
175
|
+
enterNesting();
|
|
176
|
+
try {
|
|
177
|
+
return parseUnaryInner();
|
|
178
|
+
} finally {
|
|
179
|
+
grammarDepth--;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function parseUnaryInner() {
|
|
154
183
|
const tok = current();
|
|
155
184
|
if (tok.type === "Operator" && (tok.value === "!" || tok.value === "-" || tok.value === "+")) {
|
|
156
185
|
advance();
|
|
@@ -259,7 +288,7 @@ function parse(source, _depth = 0) {
|
|
|
259
288
|
const tok = current();
|
|
260
289
|
if (tok.type === "Number") {
|
|
261
290
|
advance();
|
|
262
|
-
const raw = tok.value.replace(/_/
|
|
291
|
+
const raw = tok.value.replace(/_/gu, "");
|
|
263
292
|
return {
|
|
264
293
|
type: "NumberLiteral",
|
|
265
294
|
value: Number(raw),
|
|
@@ -610,8 +639,18 @@ function parse(source, _depth = 0) {
|
|
|
610
639
|
let depth = 1;
|
|
611
640
|
const exprStart = i;
|
|
612
641
|
while (i < raw.length && depth > 0) {
|
|
613
|
-
|
|
614
|
-
|
|
642
|
+
const c = raw[i];
|
|
643
|
+
if (c === "\"" || c === "'" || c === "`") {
|
|
644
|
+
i++;
|
|
645
|
+
while (i < raw.length && raw[i] !== c) {
|
|
646
|
+
if (raw[i] === "\\") i++;
|
|
647
|
+
i++;
|
|
648
|
+
}
|
|
649
|
+
i++;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (c === "{") depth++;
|
|
653
|
+
else if (c === "}") depth--;
|
|
615
654
|
if (depth > 0) i++;
|
|
616
655
|
}
|
|
617
656
|
const exprSource = raw.slice(exprStart, i);
|
|
@@ -791,50 +830,6 @@ function makeConstant(value, start, end) {
|
|
|
791
830
|
}
|
|
792
831
|
//#endregion
|
|
793
832
|
//#region src/eval-ops.ts
|
|
794
|
-
/** Receiver type check for safe methods. */
|
|
795
|
-
function isAllowedReceiver(obj, methodName) {
|
|
796
|
-
const t = typeof obj;
|
|
797
|
-
switch (methodName) {
|
|
798
|
-
case "startsWith":
|
|
799
|
-
case "endsWith":
|
|
800
|
-
case "substring":
|
|
801
|
-
case "charAt":
|
|
802
|
-
case "charCodeAt":
|
|
803
|
-
case "repeat":
|
|
804
|
-
case "trim":
|
|
805
|
-
case "trimStart":
|
|
806
|
-
case "trimEnd":
|
|
807
|
-
case "toLowerCase":
|
|
808
|
-
case "toUpperCase":
|
|
809
|
-
case "replace":
|
|
810
|
-
case "replaceAll":
|
|
811
|
-
case "padStart":
|
|
812
|
-
case "padEnd":
|
|
813
|
-
case "split": return t === "string";
|
|
814
|
-
case "includes":
|
|
815
|
-
case "indexOf":
|
|
816
|
-
case "lastIndexOf":
|
|
817
|
-
case "slice":
|
|
818
|
-
case "at":
|
|
819
|
-
case "concat": return t === "string" || Array.isArray(obj);
|
|
820
|
-
case "filter":
|
|
821
|
-
case "map":
|
|
822
|
-
case "find":
|
|
823
|
-
case "findIndex":
|
|
824
|
-
case "some":
|
|
825
|
-
case "every":
|
|
826
|
-
case "flatMap": return Array.isArray(obj);
|
|
827
|
-
case "join":
|
|
828
|
-
case "flat":
|
|
829
|
-
case "toReversed":
|
|
830
|
-
case "toSorted":
|
|
831
|
-
case "toSpliced":
|
|
832
|
-
case "with": return Array.isArray(obj);
|
|
833
|
-
case "toFixed": return t === "number";
|
|
834
|
-
case "toString": return t === "string" || t === "number";
|
|
835
|
-
default: return false;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
833
|
function toPropertyKey(value) {
|
|
839
834
|
return String(value);
|
|
840
835
|
}
|
|
@@ -874,7 +869,7 @@ function applyUnaryOp(operator, operand) {
|
|
|
874
869
|
function validateMethodCall(obj, methodName, guard) {
|
|
875
870
|
guard.checkNameAccess(methodName, "method");
|
|
876
871
|
if (obj == null) throw new BonsaiTypeError(methodName, "a non-null value", obj);
|
|
877
|
-
if (!
|
|
872
|
+
if (!isMethodAllowedOn(obj, methodName)) throw new BonsaiSecurityError("METHOD_NOT_ALLOWED", `Method "${methodName}" is not allowed on ${typeof obj}`);
|
|
878
873
|
const method = obj[methodName];
|
|
879
874
|
if (typeof method !== "function") throw new BonsaiTypeError(methodName, "a method", method);
|
|
880
875
|
return method;
|
|
@@ -883,9 +878,16 @@ function resolveTransform(name, transforms) {
|
|
|
883
878
|
if (!Object.hasOwn(transforms, name)) throw new BonsaiReferenceError("transform", name, suggest(name, Object.keys(transforms)));
|
|
884
879
|
return transforms[name];
|
|
885
880
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
881
|
+
/**
|
|
882
|
+
* Resolve a callable by name in the shared function namespace. Pure and context
|
|
883
|
+
* functions occupy one registry keyed by name, so a single lookup returns the
|
|
884
|
+
* tagged entry; the call site uses `kind` to decide how to invoke it (pure
|
|
885
|
+
* functions take only call args; context functions receive the evaluation
|
|
886
|
+
* context first, then call args).
|
|
887
|
+
*/
|
|
888
|
+
function resolveCallable(name, functions) {
|
|
889
|
+
if (Object.hasOwn(functions, name)) return functions[name];
|
|
890
|
+
throw new BonsaiReferenceError("function", name, suggest(name, Object.keys(functions)));
|
|
889
891
|
}
|
|
890
892
|
function getIdentifierName(node, message = "Expected identifier") {
|
|
891
893
|
if (node.type !== "Identifier") throw new Error(message);
|
|
@@ -914,7 +916,17 @@ function expandSpreadValue(value, maxLength) {
|
|
|
914
916
|
}
|
|
915
917
|
const REPLACE_METHODS = new Set(["replace", "replaceAll"]);
|
|
916
918
|
const MAX_REPEAT_COUNT = 1e5;
|
|
917
|
-
|
|
919
|
+
const HIGHER_ORDER_METHODS = new Set([
|
|
920
|
+
"map",
|
|
921
|
+
"filter",
|
|
922
|
+
"find",
|
|
923
|
+
"findIndex",
|
|
924
|
+
"some",
|
|
925
|
+
"every",
|
|
926
|
+
"flatMap"
|
|
927
|
+
]);
|
|
928
|
+
const LAMBDA_CALLBACK_EXPECTED = "a lambda or function callback (e.g. .field or . > value)";
|
|
929
|
+
function validateMethodArgs(receiver, methodName, args, guard) {
|
|
918
930
|
if (REPLACE_METHODS.has(methodName)) for (const arg of args) {
|
|
919
931
|
if (typeof arg === "function") throw new BonsaiTypeError(methodName, "string arguments (callbacks are not allowed)", arg);
|
|
920
932
|
if (arg instanceof RegExp) throw new BonsaiTypeError(methodName, "string arguments (RegExp is not allowed)", arg);
|
|
@@ -923,7 +935,32 @@ function validateMethodArgs(methodName, args) {
|
|
|
923
935
|
if (methodName === "repeat") {
|
|
924
936
|
const count = Number(args[0]);
|
|
925
937
|
if (!Number.isFinite(count) || count < 0 || count > MAX_REPEAT_COUNT) throw new BonsaiTypeError("repeat", `a count between 0 and ${MAX_REPEAT_COUNT}`, args[0]);
|
|
938
|
+
if (typeof receiver === "string") guard.checkStringLength(receiver.length * count);
|
|
926
939
|
}
|
|
940
|
+
if (methodName === "padStart" || methodName === "padEnd") guard.checkStringLength(Number(args[0]));
|
|
941
|
+
if (HIGHER_ORDER_METHODS.has(methodName) && typeof args[0] !== "function") throw new BonsaiTypeError(methodName, LAMBDA_CALLBACK_EXPECTED, args[0]);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Enforce the array-size limit on a value produced by a method call. Array
|
|
945
|
+
* literals and spread are checked at construction; array-returning methods
|
|
946
|
+
* (split, map, flat, concat, toSorted, ...) are checked here so maxArrayLength
|
|
947
|
+
* is a real ceiling on every array that flows through evaluation.
|
|
948
|
+
*/
|
|
949
|
+
function checkResultArrayLength(result, guard) {
|
|
950
|
+
if (Array.isArray(result)) guard.checkArrayLength(result.length);
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Enforce the string-size limit on a value produced by a method call. The
|
|
954
|
+
* argument-time checks on padStart/padEnd/repeat stop the worst single-call
|
|
955
|
+
* amplifiers before allocation, but string-returning methods such as join,
|
|
956
|
+
* concat, slice, and toUpperCase can still produce a string past the ceiling
|
|
957
|
+
* (notably `arr.join(sep)`, whose output is array length times separator length
|
|
958
|
+
* in a single native call). Checking the produced length here makes
|
|
959
|
+
* maxStringLength a real ceiling on every string that flows through evaluation,
|
|
960
|
+
* mirroring checkResultArrayLength.
|
|
961
|
+
*/
|
|
962
|
+
function checkResultStringLength(result, guard) {
|
|
963
|
+
if (typeof result === "string") guard.checkStringLength(result.length);
|
|
927
964
|
}
|
|
928
965
|
function accessMember(object, propertyNode, computed, computedValue, guard) {
|
|
929
966
|
const key = computed ? toPropertyKey(computedValue) : getIdentifierName(propertyNode, "Expected identifier property");
|
|
@@ -936,11 +973,11 @@ function rejectPromise(value, kind, name) {
|
|
|
936
973
|
if (value instanceof Promise) throw new BonsaiTypeError(name, `a synchronous ${kind} result — use evaluate() instead of evaluateSync() for async`, value);
|
|
937
974
|
return value;
|
|
938
975
|
}
|
|
939
|
-
function evaluate(node, context,
|
|
976
|
+
function evaluate(node, context, bindings, guard, source) {
|
|
940
977
|
return evalNode(node, {
|
|
941
978
|
ctx: context,
|
|
942
|
-
tr: transforms,
|
|
943
|
-
fn: functions,
|
|
979
|
+
tr: bindings.transforms,
|
|
980
|
+
fn: bindings.functions,
|
|
944
981
|
g: guard,
|
|
945
982
|
s: source
|
|
946
983
|
});
|
|
@@ -1054,18 +1091,21 @@ function evalCallExpression(node, env) {
|
|
|
1054
1091
|
const method = validateMethodCall(obj, methodName, g);
|
|
1055
1092
|
const args = [];
|
|
1056
1093
|
for (const arg of node.args) pushCallArgument(args, arg, env);
|
|
1057
|
-
validateMethodArgs(methodName, args);
|
|
1058
|
-
|
|
1094
|
+
validateMethodArgs(obj, methodName, args, g);
|
|
1095
|
+
const result = rejectPromise(method.call(obj, ...args), "method", methodName);
|
|
1096
|
+
checkResultArrayLength(result, g);
|
|
1097
|
+
checkResultStringLength(result, g);
|
|
1098
|
+
return result;
|
|
1059
1099
|
} catch (e) {
|
|
1060
1100
|
if (s) attachLocation(e, s, node.start, node.end);
|
|
1061
1101
|
throw e;
|
|
1062
1102
|
}
|
|
1063
1103
|
}
|
|
1064
1104
|
if (node.callee.type === "Identifier") try {
|
|
1065
|
-
const
|
|
1105
|
+
const resolved = resolveCallable(node.callee.name, fn);
|
|
1066
1106
|
const args = [];
|
|
1067
1107
|
for (const arg of node.args) pushCallArgument(args, arg, env);
|
|
1068
|
-
return rejectPromise(
|
|
1108
|
+
return rejectPromise(resolved.kind === "context" ? resolved.fn(env.ctx, ...args) : resolved.fn(...args), "function", node.callee.name);
|
|
1069
1109
|
} catch (e) {
|
|
1070
1110
|
if (s) attachLocation(e, s, node.start, node.end);
|
|
1071
1111
|
throw e;
|
|
@@ -1128,8 +1168,11 @@ function evalLambdaBody(node, item, env) {
|
|
|
1128
1168
|
const args = [];
|
|
1129
1169
|
for (const arg of node.args) if (arg.type === "SpreadElement") args.push(...expandSpreadValue(evalNode(arg.argument, env), g.policy.maxArrayLength));
|
|
1130
1170
|
else args.push(evalArg(arg, env));
|
|
1131
|
-
validateMethodArgs(methodName, args);
|
|
1132
|
-
|
|
1171
|
+
validateMethodArgs(obj, methodName, args, g);
|
|
1172
|
+
const result = rejectPromise(method.call(obj, ...args), "method", methodName);
|
|
1173
|
+
checkResultArrayLength(result, g);
|
|
1174
|
+
checkResultStringLength(result, g);
|
|
1175
|
+
return result;
|
|
1133
1176
|
}
|
|
1134
1177
|
ownDepth = false;
|
|
1135
1178
|
g.exitDepth();
|
|
@@ -1175,11 +1218,11 @@ function makeLambdaAccessor$1(property, guard) {
|
|
|
1175
1218
|
}
|
|
1176
1219
|
//#endregion
|
|
1177
1220
|
//#region src/evaluator-async.ts
|
|
1178
|
-
async function evaluateAsync(node, context,
|
|
1221
|
+
async function evaluateAsync(node, context, bindings, guard, source) {
|
|
1179
1222
|
const result = await evalNodeAsync(node, {
|
|
1180
1223
|
ctx: context,
|
|
1181
|
-
tr: transforms,
|
|
1182
|
-
fn: functions,
|
|
1224
|
+
tr: bindings.transforms,
|
|
1225
|
+
fn: bindings.functions,
|
|
1183
1226
|
g: guard,
|
|
1184
1227
|
s: source
|
|
1185
1228
|
});
|
|
@@ -1295,18 +1338,21 @@ async function evalCallExpressionAsync(node, env) {
|
|
|
1295
1338
|
const method = validateMethodCall(obj, methodName, g);
|
|
1296
1339
|
const args = [];
|
|
1297
1340
|
for (const arg of node.args) await pushCallArgumentAsync(args, arg, env);
|
|
1298
|
-
validateMethodArgs(methodName, args);
|
|
1341
|
+
validateMethodArgs(obj, methodName, args, g);
|
|
1299
1342
|
if (Array.isArray(obj) && args.length === 1 && typeof args[0] === "function") {
|
|
1300
1343
|
const arr = obj;
|
|
1301
1344
|
const predicate = args[0];
|
|
1302
1345
|
const asyncResult = await evalAsyncArrayMethod(methodName, arr, predicate);
|
|
1303
1346
|
if (asyncResult !== void 0) {
|
|
1304
1347
|
g.checkTimeout();
|
|
1348
|
+
checkResultArrayLength(asyncResult.value, g);
|
|
1305
1349
|
return asyncResult.value;
|
|
1306
1350
|
}
|
|
1307
1351
|
}
|
|
1308
1352
|
const result = await method.call(obj, ...args);
|
|
1309
1353
|
g.checkTimeout();
|
|
1354
|
+
checkResultArrayLength(result, g);
|
|
1355
|
+
checkResultStringLength(result, g);
|
|
1310
1356
|
return result;
|
|
1311
1357
|
} catch (e) {
|
|
1312
1358
|
if (s) attachLocation(e, s, node.start, node.end);
|
|
@@ -1314,10 +1360,10 @@ async function evalCallExpressionAsync(node, env) {
|
|
|
1314
1360
|
}
|
|
1315
1361
|
}
|
|
1316
1362
|
if (node.callee.type === "Identifier") try {
|
|
1317
|
-
const
|
|
1363
|
+
const resolved = resolveCallable(node.callee.name, fn);
|
|
1318
1364
|
const args = [];
|
|
1319
1365
|
for (const arg of node.args) await pushCallArgumentAsync(args, arg, env);
|
|
1320
|
-
const result = await
|
|
1366
|
+
const result = resolved.kind === "context" ? await resolved.fn(env.ctx, ...args) : await resolved.fn(...args);
|
|
1321
1367
|
g.checkTimeout();
|
|
1322
1368
|
return result;
|
|
1323
1369
|
} catch (e) {
|
|
@@ -1336,35 +1382,32 @@ async function pushCallArgumentAsync(args, node, env) {
|
|
|
1336
1382
|
async function evalAsyncArrayMethod(methodName, arr, predicate) {
|
|
1337
1383
|
switch (methodName) {
|
|
1338
1384
|
case "filter": {
|
|
1339
|
-
const
|
|
1340
|
-
const
|
|
1341
|
-
return { value:
|
|
1385
|
+
const out = [];
|
|
1386
|
+
for (const item of arr) if (await predicate(item)) out.push(item);
|
|
1387
|
+
return { value: out };
|
|
1342
1388
|
}
|
|
1343
1389
|
case "map": {
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
case "find": {
|
|
1348
|
-
const results = arr.map(predicate);
|
|
1349
|
-
const idx = (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).findIndex(Boolean);
|
|
1350
|
-
return { value: idx >= 0 ? arr[idx] : void 0 };
|
|
1351
|
-
}
|
|
1352
|
-
case "some": {
|
|
1353
|
-
const results = arr.map(predicate);
|
|
1354
|
-
return { value: (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).some(Boolean) };
|
|
1355
|
-
}
|
|
1356
|
-
case "every": {
|
|
1357
|
-
const results = arr.map(predicate);
|
|
1358
|
-
return { value: (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).every(Boolean) };
|
|
1359
|
-
}
|
|
1360
|
-
case "findIndex": {
|
|
1361
|
-
const results = arr.map(predicate);
|
|
1362
|
-
return { value: (results.some((r) => r instanceof Promise) ? await Promise.all(results) : results).findIndex(Boolean) };
|
|
1390
|
+
const out = [];
|
|
1391
|
+
for (const item of arr) out.push(await predicate(item));
|
|
1392
|
+
return { value: out };
|
|
1363
1393
|
}
|
|
1364
1394
|
case "flatMap": {
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1395
|
+
const out = [];
|
|
1396
|
+
for (const item of arr) out.push(await predicate(item));
|
|
1397
|
+
return { value: out.flat() };
|
|
1367
1398
|
}
|
|
1399
|
+
case "find":
|
|
1400
|
+
for (const item of arr) if (await predicate(item)) return { value: item };
|
|
1401
|
+
return { value: void 0 };
|
|
1402
|
+
case "findIndex":
|
|
1403
|
+
for (let i = 0; i < arr.length; i++) if (await predicate(arr[i])) return { value: i };
|
|
1404
|
+
return { value: -1 };
|
|
1405
|
+
case "some":
|
|
1406
|
+
for (const item of arr) if (await predicate(item)) return { value: true };
|
|
1407
|
+
return { value: false };
|
|
1408
|
+
case "every":
|
|
1409
|
+
for (const item of arr) if (!await predicate(item)) return { value: false };
|
|
1410
|
+
return { value: true };
|
|
1368
1411
|
default: return;
|
|
1369
1412
|
}
|
|
1370
1413
|
}
|
|
@@ -1422,15 +1465,19 @@ async function evalLambdaBodyAsync(node, item, env) {
|
|
|
1422
1465
|
const args = [];
|
|
1423
1466
|
for (const arg of node.args) if (arg.type === "SpreadElement") args.push(...expandSpreadValue(await evalNodeAsync(arg.argument, env), g.policy.maxArrayLength));
|
|
1424
1467
|
else args.push(await evalArgAsync(arg, env));
|
|
1425
|
-
validateMethodArgs(methodName, args);
|
|
1468
|
+
validateMethodArgs(obj, methodName, args, g);
|
|
1426
1469
|
if (Array.isArray(obj) && args.length === 1 && typeof args[0] === "function") {
|
|
1427
1470
|
const asyncResult = await evalAsyncArrayMethod(methodName, obj, args[0]);
|
|
1428
1471
|
if (asyncResult !== void 0) {
|
|
1429
1472
|
g.checkTimeout();
|
|
1473
|
+
checkResultArrayLength(asyncResult.value, g);
|
|
1430
1474
|
return asyncResult.value;
|
|
1431
1475
|
}
|
|
1432
1476
|
}
|
|
1433
|
-
|
|
1477
|
+
const result = await method.call(obj, ...args);
|
|
1478
|
+
checkResultArrayLength(result, g);
|
|
1479
|
+
checkResultStringLength(result, g);
|
|
1480
|
+
return result;
|
|
1434
1481
|
}
|
|
1435
1482
|
ownDepth = false;
|
|
1436
1483
|
g.exitDepth();
|
|
@@ -1527,13 +1574,24 @@ function createPluginRegistry() {
|
|
|
1527
1574
|
let functionsCache = {};
|
|
1528
1575
|
let transformsDirty = false;
|
|
1529
1576
|
let functionsDirty = false;
|
|
1577
|
+
let bindingsCache;
|
|
1530
1578
|
const registry = {
|
|
1531
1579
|
addTransform(name, fn) {
|
|
1532
1580
|
transformMap.set(name, fn);
|
|
1533
1581
|
transformsDirty = true;
|
|
1534
1582
|
},
|
|
1535
1583
|
addFunction(name, fn) {
|
|
1536
|
-
functionMap.set(name,
|
|
1584
|
+
functionMap.set(name, {
|
|
1585
|
+
kind: "pure",
|
|
1586
|
+
fn
|
|
1587
|
+
});
|
|
1588
|
+
functionsDirty = true;
|
|
1589
|
+
},
|
|
1590
|
+
addContextFunction(name, fn) {
|
|
1591
|
+
functionMap.set(name, {
|
|
1592
|
+
kind: "context",
|
|
1593
|
+
fn
|
|
1594
|
+
});
|
|
1537
1595
|
functionsDirty = true;
|
|
1538
1596
|
},
|
|
1539
1597
|
removeTransform(name) {
|
|
@@ -1552,6 +1610,12 @@ function createPluginRegistry() {
|
|
|
1552
1610
|
getFunction(name) {
|
|
1553
1611
|
return functionMap.get(name);
|
|
1554
1612
|
},
|
|
1613
|
+
isContextFunction(name) {
|
|
1614
|
+
return functionMap.get(name)?.kind === "context";
|
|
1615
|
+
},
|
|
1616
|
+
hasFunction(name) {
|
|
1617
|
+
return functionMap.has(name);
|
|
1618
|
+
},
|
|
1555
1619
|
getTransformNames() {
|
|
1556
1620
|
return [...transformMap.keys()];
|
|
1557
1621
|
},
|
|
@@ -1574,6 +1638,15 @@ function createPluginRegistry() {
|
|
|
1574
1638
|
functionsDirty = false;
|
|
1575
1639
|
}
|
|
1576
1640
|
return functionsCache;
|
|
1641
|
+
},
|
|
1642
|
+
get bindings() {
|
|
1643
|
+
const t = registry.transforms;
|
|
1644
|
+
const f = registry.functions;
|
|
1645
|
+
if (!bindingsCache || bindingsCache.transforms !== t || bindingsCache.functions !== f) bindingsCache = {
|
|
1646
|
+
transforms: t,
|
|
1647
|
+
functions: f
|
|
1648
|
+
};
|
|
1649
|
+
return bindingsCache;
|
|
1577
1650
|
}
|
|
1578
1651
|
};
|
|
1579
1652
|
return registry;
|
|
@@ -1581,27 +1654,62 @@ function createPluginRegistry() {
|
|
|
1581
1654
|
//#endregion
|
|
1582
1655
|
//#region src/index.ts
|
|
1583
1656
|
const DEFAULT_CACHE_SIZE = 256;
|
|
1584
|
-
|
|
1657
|
+
/**
|
|
1658
|
+
* Validate options at construction so misconfiguration fails fast with a clear
|
|
1659
|
+
* error rather than silently producing surprising behavior (e.g. a negative
|
|
1660
|
+
* cacheSize quietly disabling the cache).
|
|
1661
|
+
*/
|
|
1662
|
+
function assertValidOptions(options) {
|
|
1663
|
+
const requireNonNegativeInt = (name, value) => {
|
|
1664
|
+
if (value !== void 0 && (!Number.isInteger(value) || value < 0)) throw new RangeError(`bonsai: "${name}" must be a non-negative integer, received ${String(value)}`);
|
|
1665
|
+
};
|
|
1666
|
+
requireNonNegativeInt("cacheSize", options.cacheSize);
|
|
1667
|
+
requireNonNegativeInt("maxArrayLength", options.maxArrayLength);
|
|
1668
|
+
requireNonNegativeInt("maxStringLength", options.maxStringLength);
|
|
1669
|
+
if (options.maxDepth !== void 0 && (!Number.isInteger(options.maxDepth) || options.maxDepth < 1)) throw new RangeError(`bonsai: "maxDepth" must be a positive integer, received ${String(options.maxDepth)}`);
|
|
1670
|
+
if (options.timeout !== void 0 && (typeof options.timeout !== "number" || !Number.isFinite(options.timeout) || options.timeout < 0)) throw new RangeError(`bonsai: "timeout" must be a non-negative, finite number of milliseconds, received ${String(options.timeout)}`);
|
|
1671
|
+
const requireStringArray = (name, value) => {
|
|
1672
|
+
if (value === void 0) return;
|
|
1673
|
+
if (!Array.isArray(value)) throw new TypeError(`bonsai: "${name}" must be an array of strings`);
|
|
1674
|
+
if (!value.every((item) => typeof item === "string")) throw new TypeError(`bonsai: "${name}" must contain only strings`);
|
|
1675
|
+
};
|
|
1676
|
+
requireStringArray("allowedProperties", options.allowedProperties);
|
|
1677
|
+
requireStringArray("deniedProperties", options.deniedProperties);
|
|
1678
|
+
}
|
|
1679
|
+
let sharedInstance;
|
|
1585
1680
|
/**
|
|
1586
1681
|
* Evaluate a single expression with default options. Uses a shared instance internally.
|
|
1587
1682
|
* For repeated evaluation or custom configuration, use `bonsai()` to create a dedicated instance.
|
|
1588
1683
|
*/
|
|
1589
1684
|
function evaluateExpression(expression, context) {
|
|
1590
|
-
if (!
|
|
1591
|
-
return
|
|
1685
|
+
if (!sharedInstance) sharedInstance = bonsai();
|
|
1686
|
+
return sharedInstance.evaluateSync(expression, context);
|
|
1592
1687
|
}
|
|
1593
1688
|
/**
|
|
1594
1689
|
* Create a new Bonsai instance with optional safety and caching configuration.
|
|
1595
|
-
* Register transforms and functions via `.use()`, `.addTransform()`,
|
|
1690
|
+
* Register transforms and functions via `.use()`, `.addTransform()`, `.addFunction()`,
|
|
1691
|
+
* and `.addContextFunction()`.
|
|
1692
|
+
*
|
|
1693
|
+
* Pass a context type argument to get end-to-end type safety on the evaluation
|
|
1694
|
+
* context: `bonsai<MyContext>()` will type-check `evaluate(expr, ctx)` calls,
|
|
1695
|
+
* require a context argument when `MyContext` has required fields, and
|
|
1696
|
+
* propagate `MyContext` into context-aware function signatures.
|
|
1596
1697
|
*
|
|
1597
1698
|
* @example
|
|
1598
1699
|
* ```ts
|
|
1599
1700
|
* const expr = bonsai({ timeout: 50 })
|
|
1600
1701
|
* expr.use(strings)
|
|
1601
1702
|
* expr.evaluateSync('name |> trim |> upper', { name: ' hello ' }) // "HELLO"
|
|
1703
|
+
*
|
|
1704
|
+
* type AppCtx = { userId: string; perms: string[] }
|
|
1705
|
+
* const app = bonsai<AppCtx>()
|
|
1706
|
+
* app.addContextFunction('hasPermission', (ctx, action) =>
|
|
1707
|
+
* ctx.perms.includes(String(action)))
|
|
1708
|
+
* app.evaluateSync('hasPermission("write")', { userId: 'u_1', perms: ['write'] })
|
|
1602
1709
|
* ```
|
|
1603
1710
|
*/
|
|
1604
1711
|
function bonsai(options = {}) {
|
|
1712
|
+
assertValidOptions(options);
|
|
1605
1713
|
const registry = createPluginRegistry();
|
|
1606
1714
|
const cache = new LRUCache(options.cacheSize ?? DEFAULT_CACHE_SIZE);
|
|
1607
1715
|
const astCache = new LRUCache(options.cacheSize ?? DEFAULT_CACHE_SIZE);
|
|
@@ -1625,15 +1733,16 @@ function bonsai(options = {}) {
|
|
|
1625
1733
|
const compiled = {
|
|
1626
1734
|
ast: optimized,
|
|
1627
1735
|
source,
|
|
1628
|
-
async evaluate(
|
|
1629
|
-
return evaluateAsync(optimized,
|
|
1736
|
+
async evaluate(...args) {
|
|
1737
|
+
return evaluateAsync(optimized, args[0] ?? {}, registry.bindings, createExecutionContext(), source);
|
|
1630
1738
|
},
|
|
1631
|
-
evaluateSync(
|
|
1632
|
-
|
|
1739
|
+
evaluateSync(...args) {
|
|
1740
|
+
const ctx = args[0] ?? {};
|
|
1741
|
+
if (syncCtxInUse) return evaluate(optimized, ctx, registry.bindings, createExecutionContext(), source);
|
|
1633
1742
|
syncCtxInUse = true;
|
|
1634
1743
|
try {
|
|
1635
1744
|
syncCtx.reset();
|
|
1636
|
-
return evaluate(optimized,
|
|
1745
|
+
return evaluate(optimized, ctx, registry.bindings, syncCtx, source);
|
|
1637
1746
|
} finally {
|
|
1638
1747
|
syncCtxInUse = false;
|
|
1639
1748
|
}
|
|
@@ -1655,6 +1764,10 @@ function bonsai(options = {}) {
|
|
|
1655
1764
|
registry.addFunction(name, fn);
|
|
1656
1765
|
return instance;
|
|
1657
1766
|
},
|
|
1767
|
+
addContextFunction(name, fn) {
|
|
1768
|
+
registry.addContextFunction(name, fn);
|
|
1769
|
+
return instance;
|
|
1770
|
+
},
|
|
1658
1771
|
removeTransform(name) {
|
|
1659
1772
|
return registry.removeTransform(name);
|
|
1660
1773
|
},
|
|
@@ -1665,7 +1778,10 @@ function bonsai(options = {}) {
|
|
|
1665
1778
|
return registry.getTransform(name) !== void 0;
|
|
1666
1779
|
},
|
|
1667
1780
|
hasFunction(name) {
|
|
1668
|
-
return registry.
|
|
1781
|
+
return registry.hasFunction(name);
|
|
1782
|
+
},
|
|
1783
|
+
isContextFunction(name) {
|
|
1784
|
+
return registry.isContextFunction(name);
|
|
1669
1785
|
},
|
|
1670
1786
|
listTransforms() {
|
|
1671
1787
|
return registry.getTransformNames();
|
|
@@ -1686,16 +1802,17 @@ function bonsai(options = {}) {
|
|
|
1686
1802
|
compile(expression) {
|
|
1687
1803
|
return compileExpr(expression);
|
|
1688
1804
|
},
|
|
1689
|
-
async evaluate(expression,
|
|
1690
|
-
return evaluateAsync(getAst(expression),
|
|
1805
|
+
async evaluate(expression, ...args) {
|
|
1806
|
+
return evaluateAsync(getAst(expression), args[0] ?? {}, registry.bindings, createExecutionContext(), expression);
|
|
1691
1807
|
},
|
|
1692
|
-
evaluateSync(expression,
|
|
1808
|
+
evaluateSync(expression, ...args) {
|
|
1693
1809
|
const ast = getAst(expression);
|
|
1694
|
-
|
|
1810
|
+
const ctx = args[0] ?? {};
|
|
1811
|
+
if (syncCtxInUse) return evaluate(ast, ctx, registry.bindings, createExecutionContext(), expression);
|
|
1695
1812
|
syncCtxInUse = true;
|
|
1696
1813
|
try {
|
|
1697
1814
|
syncCtx.reset();
|
|
1698
|
-
return evaluate(ast,
|
|
1815
|
+
return evaluate(ast, ctx, registry.bindings, syncCtx, expression);
|
|
1699
1816
|
} finally {
|
|
1700
1817
|
syncCtxInUse = false;
|
|
1701
1818
|
}
|