@bquery/bquery 1.10.0 → 1.11.1
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 +91 -65
- package/dist/{a11y-DG2i4iZN.js → a11y-DgUQ8-fI.js} +1 -1
- package/dist/{a11y-DG2i4iZN.js.map → a11y-DgUQ8-fI.js.map} +1 -1
- package/dist/a11y.es.mjs +1 -1
- package/dist/{component-DRotf1hl.js → component-D8ydhe58.js} +2 -2
- package/dist/{component-DRotf1hl.js.map → component-D8ydhe58.js.map} +1 -1
- package/dist/component.es.mjs +1 -1
- package/dist/concurrency-BU1wPEsZ.js.map +1 -1
- package/dist/{constraints-CqjhmpZC.js → constraints-Dlbx_m1b.js} +1 -1
- package/dist/{constraints-CqjhmpZC.js.map → constraints-Dlbx_m1b.js.map} +1 -1
- package/dist/{core-EMYSLzaT.js → core-tOP6QOrY.js} +2 -2
- package/dist/{core-EMYSLzaT.js.map → core-tOP6QOrY.js.map} +1 -1
- package/dist/core.es.mjs +1 -1
- package/dist/{custom-directives-BjFzFhuf.js → custom-directives-5DlKqvd2.js} +1 -1
- package/dist/{custom-directives-BjFzFhuf.js.map → custom-directives-5DlKqvd2.js.map} +1 -1
- package/dist/{devtools-C5FExMwv.js → devtools-QosAqo0T.js} +2 -2
- package/dist/{devtools-C5FExMwv.js.map → devtools-QosAqo0T.js.map} +1 -1
- package/dist/devtools.es.mjs +1 -1
- package/dist/{dnd-BAqzPlSo.js → dnd-d2OU4len.js} +1 -1
- package/dist/{dnd-BAqzPlSo.js.map → dnd-d2OU4len.js.map} +1 -1
- package/dist/dnd.es.mjs +1 -1
- package/dist/{forms-Dx1Scvh0.js → forms-BLx4ZzT7.js} +1 -1
- package/dist/{forms-Dx1Scvh0.js.map → forms-BLx4ZzT7.js.map} +1 -1
- package/dist/forms.es.mjs +1 -1
- package/dist/full.d.ts +4 -2
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +258 -219
- package/dist/full.iife.js +41 -37
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +41 -37
- package/dist/full.umd.js.map +1 -1
- package/dist/{i18n-Cazyk9RD.js → i18n--p7PM-9r.js} +1 -1
- package/dist/{i18n-Cazyk9RD.js.map → i18n--p7PM-9r.js.map} +1 -1
- package/dist/i18n.es.mjs +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.mjs +291 -252
- package/dist/match-CrZRVC4z.js +174 -0
- package/dist/match-CrZRVC4z.js.map +1 -0
- package/dist/{media-dAKIGPk3.js → media-gjbWNq50.js} +1 -1
- package/dist/{media-dAKIGPk3.js.map → media-gjbWNq50.js.map} +1 -1
- package/dist/media.es.mjs +1 -1
- package/dist/motion-BBMso9Ir.js.map +1 -1
- package/dist/{mount-C8O2vXkQ.js → mount-0A9qtcRJ.js} +3 -3
- package/dist/{mount-C8O2vXkQ.js.map → mount-0A9qtcRJ.js.map} +1 -1
- package/dist/platform-BPHIXbw8.js.map +1 -1
- package/dist/{plugin-DjTqWg-P.js → plugin-SZEirbwq.js} +2 -2
- package/dist/{plugin-DjTqWg-P.js.map → plugin-SZEirbwq.js.map} +1 -1
- package/dist/plugin.es.mjs +1 -1
- package/dist/reactive-BAd2hfl8.js.map +1 -1
- package/dist/{registry-Cr6VH8CR.js → registry-jpUQHf4E.js} +1 -1
- package/dist/{registry-Cr6VH8CR.js.map → registry-jpUQHf4E.js.map} +1 -1
- package/dist/router-C4weu0QL.js +333 -0
- package/dist/router-C4weu0QL.js.map +1 -0
- package/dist/router.es.mjs +1 -1
- package/dist/{sanitize-B1V4JswB.js → sanitize-DOMkRO9G.js} +12 -7
- package/dist/{sanitize-B1V4JswB.js.map → sanitize-DOMkRO9G.js.map} +1 -1
- package/dist/security.es.mjs +1 -1
- package/dist/server/create-server.d.ts +25 -0
- package/dist/server/create-server.d.ts.map +1 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/types.d.ts +396 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server-QdyKtCS1.js +349 -0
- package/dist/server-QdyKtCS1.js.map +1 -0
- package/dist/server.es.mjs +6 -0
- package/dist/ssr/adapters.d.ts +74 -0
- package/dist/ssr/adapters.d.ts.map +1 -0
- package/dist/ssr/async.d.ts +40 -0
- package/dist/ssr/async.d.ts.map +1 -0
- package/dist/ssr/config.d.ts +60 -0
- package/dist/ssr/config.d.ts.map +1 -0
- package/dist/ssr/context.d.ts +73 -0
- package/dist/ssr/context.d.ts.map +1 -0
- package/dist/ssr/defer-brand.d.ts +5 -0
- package/dist/ssr/defer-brand.d.ts.map +1 -0
- package/dist/ssr/escape.d.ts +17 -0
- package/dist/ssr/escape.d.ts.map +1 -0
- package/dist/ssr/expression.d.ts +44 -0
- package/dist/ssr/expression.d.ts.map +1 -0
- package/dist/ssr/hash.d.ts +39 -0
- package/dist/ssr/hash.d.ts.map +1 -0
- package/dist/ssr/head.d.ts +102 -0
- package/dist/ssr/head.d.ts.map +1 -0
- package/dist/ssr/html-parser.d.ts +58 -0
- package/dist/ssr/html-parser.d.ts.map +1 -0
- package/dist/ssr/index.d.ts +49 -43
- package/dist/ssr/index.d.ts.map +1 -1
- package/dist/ssr/mismatch.d.ts +60 -0
- package/dist/ssr/mismatch.d.ts.map +1 -0
- package/dist/ssr/render-async.d.ts +84 -0
- package/dist/ssr/render-async.d.ts.map +1 -0
- package/dist/ssr/render.d.ts.map +1 -1
- package/dist/ssr/renderer.d.ts +25 -0
- package/dist/ssr/renderer.d.ts.map +1 -0
- package/dist/ssr/resumability.d.ts +65 -0
- package/dist/ssr/resumability.d.ts.map +1 -0
- package/dist/ssr/router-bridge.d.ts +101 -0
- package/dist/ssr/router-bridge.d.ts.map +1 -0
- package/dist/ssr/runtime.d.ts +63 -0
- package/dist/ssr/runtime.d.ts.map +1 -0
- package/dist/ssr/serialize.d.ts.map +1 -1
- package/dist/ssr/store-snapshot.d.ts +87 -0
- package/dist/ssr/store-snapshot.d.ts.map +1 -0
- package/dist/ssr/strategies.d.ts +43 -0
- package/dist/ssr/strategies.d.ts.map +1 -0
- package/dist/ssr/suspense.d.ts +47 -0
- package/dist/ssr/suspense.d.ts.map +1 -0
- package/dist/ssr/types.d.ts +17 -0
- package/dist/ssr/types.d.ts.map +1 -1
- package/dist/ssr-Bt6BQA3J.js +2127 -0
- package/dist/ssr-Bt6BQA3J.js.map +1 -0
- package/dist/ssr.es.mjs +42 -7
- package/dist/{store-CjmEeX9-.js → store-DnXuu6Li.js} +2 -2
- package/dist/{store-CjmEeX9-.js.map → store-DnXuu6Li.js.map} +1 -1
- package/dist/store.es.mjs +2 -2
- package/dist/storybook.es.mjs +1 -1
- package/dist/{testing-TdfaL7VE.js → testing-CeMUwrRD.js} +2 -2
- package/dist/{testing-TdfaL7VE.js.map → testing-CeMUwrRD.js.map} +1 -1
- package/dist/testing.es.mjs +1 -1
- package/dist/view.es.mjs +1 -1
- package/package.json +19 -14
- package/src/full.ts +99 -0
- package/src/index.ts +5 -2
- package/src/server/create-server.ts +754 -0
- package/src/server/index.ts +33 -0
- package/src/server/types.ts +490 -0
- package/src/ssr/adapters.ts +330 -0
- package/src/ssr/async.ts +125 -0
- package/src/ssr/config.ts +86 -0
- package/src/ssr/context.ts +245 -0
- package/src/ssr/defer-brand.ts +3 -0
- package/src/ssr/escape.ts +25 -0
- package/src/ssr/expression.ts +669 -0
- package/src/ssr/hash.ts +71 -0
- package/src/ssr/head.ts +240 -0
- package/src/ssr/html-parser.ts +387 -0
- package/src/ssr/index.ts +136 -43
- package/src/ssr/mismatch.ts +110 -0
- package/src/ssr/render-async.ts +286 -0
- package/src/ssr/render.ts +130 -59
- package/src/ssr/renderer.ts +453 -0
- package/src/ssr/resumability.ts +142 -0
- package/src/ssr/router-bridge.ts +177 -0
- package/src/ssr/runtime.ts +131 -0
- package/src/ssr/serialize.ts +1 -27
- package/src/ssr/store-snapshot.ts +209 -0
- package/src/ssr/strategies.ts +245 -0
- package/src/ssr/suspense.ts +504 -0
- package/src/ssr/types.ts +18 -0
- package/dist/router-CCepRMpC.js +0 -493
- package/dist/router-CCepRMpC.js.map +0 -1
- package/dist/ssr-D-1IPcfw.js +0 -248
- package/dist/ssr-D-1IPcfw.js.map +0 -1
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSP-safe expression evaluator for SSR templates.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates a tightly scoped subset of JavaScript expressions against a
|
|
5
|
+
* binding context without using `eval` or `new Function()`. Runs in any
|
|
6
|
+
* runtime (Bun, Deno, Node, browsers) and is safe under strict CSP without
|
|
7
|
+
* `'unsafe-eval'`.
|
|
8
|
+
*
|
|
9
|
+
* Supported grammar (operator-precedence Pratt parser):
|
|
10
|
+
*
|
|
11
|
+
* - Literals: numbers, single/double-quoted strings, `true`, `false`, `null`,
|
|
12
|
+
* `undefined`.
|
|
13
|
+
* - Identifiers and member access (`a.b`, `a['b']`, `a[0]`).
|
|
14
|
+
* - Optional chaining (`a?.b`, `a?.[b]`).
|
|
15
|
+
* - Unary `!`, `+`, `-`, `typeof`.
|
|
16
|
+
* - Binary `+`, `-`, `*`, `/`, `%`, `==`, `===`, `!=`, `!==`, `<`, `<=`,
|
|
17
|
+
* `>`, `>=`, `&&`, `||`, `??`.
|
|
18
|
+
* - Ternary `cond ? a : b`.
|
|
19
|
+
* - Parentheses for grouping.
|
|
20
|
+
* - Function calls `fn(arg1, arg2, ...)` (only on identifiers / member chains
|
|
21
|
+
* resolved against the context — no arbitrary expression invocation).
|
|
22
|
+
*
|
|
23
|
+
* Anything outside this grammar throws a parse error which the caller
|
|
24
|
+
* converts into the standard SSR fallback (`undefined`).
|
|
25
|
+
*
|
|
26
|
+
* @module bquery/ssr
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { isComputed, isSignal, type Signal } from '../reactive/index';
|
|
31
|
+
import { isPrototypePollutionKey } from '../core/utils/object';
|
|
32
|
+
import type { BindingContext } from '../view/types';
|
|
33
|
+
|
|
34
|
+
const unwrap = (value: unknown): unknown => {
|
|
35
|
+
if (isSignal(value) || isComputed(value)) {
|
|
36
|
+
return (value as Signal<unknown>).value;
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/* ---------------------------------------------------------------------------
|
|
42
|
+
* Tokenizer
|
|
43
|
+
* ------------------------------------------------------------------------- */
|
|
44
|
+
|
|
45
|
+
type TokenKind = 'number' | 'string' | 'ident' | 'punct' | 'eof';
|
|
46
|
+
|
|
47
|
+
interface Token {
|
|
48
|
+
kind: TokenKind;
|
|
49
|
+
value: string;
|
|
50
|
+
start: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const PUNCT_MULTI = ['===', '!==', '==', '!=', '<=', '>=', '&&', '||', '??', '?.'];
|
|
54
|
+
|
|
55
|
+
const PUNCT_SINGLE = '+-*/%<>!?:,.()[]';
|
|
56
|
+
|
|
57
|
+
const isIdentStart = (ch: string): boolean =>
|
|
58
|
+
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$';
|
|
59
|
+
|
|
60
|
+
const isIdentCont = (ch: string): boolean => isIdentStart(ch) || (ch >= '0' && ch <= '9');
|
|
61
|
+
|
|
62
|
+
const isDigit = (ch: string): boolean => ch >= '0' && ch <= '9';
|
|
63
|
+
|
|
64
|
+
const tokenize = (input: string): Token[] => {
|
|
65
|
+
const tokens: Token[] = [];
|
|
66
|
+
const len = input.length;
|
|
67
|
+
let i = 0;
|
|
68
|
+
|
|
69
|
+
while (i < len) {
|
|
70
|
+
const ch = input[i];
|
|
71
|
+
|
|
72
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
73
|
+
i++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Strings
|
|
78
|
+
if (ch === '"' || ch === "'") {
|
|
79
|
+
const quote = ch;
|
|
80
|
+
const start = i;
|
|
81
|
+
i++;
|
|
82
|
+
let value = '';
|
|
83
|
+
while (i < len && input[i] !== quote) {
|
|
84
|
+
if (input[i] === '\\' && i + 1 < len) {
|
|
85
|
+
const next = input[i + 1];
|
|
86
|
+
if (next === 'n') value += '\n';
|
|
87
|
+
else if (next === 't') value += '\t';
|
|
88
|
+
else if (next === 'r') value += '\r';
|
|
89
|
+
else if (next === '\\') value += '\\';
|
|
90
|
+
else if (next === quote) value += quote;
|
|
91
|
+
else value += next;
|
|
92
|
+
i += 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
value += input[i];
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
if (i >= len) {
|
|
99
|
+
throw new Error('Unterminated string literal in SSR expression');
|
|
100
|
+
}
|
|
101
|
+
i++; // closing quote
|
|
102
|
+
tokens.push({ kind: 'string', value, start });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Numbers
|
|
107
|
+
if (isDigit(ch) || (ch === '.' && i + 1 < len && isDigit(input[i + 1]))) {
|
|
108
|
+
const start = i;
|
|
109
|
+
while (i < len && (isDigit(input[i]) || input[i] === '.')) {
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
tokens.push({ kind: 'number', value: input.slice(start, i), start });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Identifiers / keywords
|
|
117
|
+
if (isIdentStart(ch)) {
|
|
118
|
+
const start = i;
|
|
119
|
+
while (i < len && isIdentCont(input[i])) {
|
|
120
|
+
i++;
|
|
121
|
+
}
|
|
122
|
+
tokens.push({ kind: 'ident', value: input.slice(start, i), start });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Multi-char punctuation
|
|
127
|
+
let matched = false;
|
|
128
|
+
for (const p of PUNCT_MULTI) {
|
|
129
|
+
if (input.startsWith(p, i)) {
|
|
130
|
+
tokens.push({ kind: 'punct', value: p, start: i });
|
|
131
|
+
i += p.length;
|
|
132
|
+
matched = true;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (matched) continue;
|
|
137
|
+
|
|
138
|
+
if (PUNCT_SINGLE.includes(ch)) {
|
|
139
|
+
tokens.push({ kind: 'punct', value: ch, start: i });
|
|
140
|
+
i++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw new Error(`Unexpected character "${ch}" in SSR expression`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
tokens.push({ kind: 'eof', value: '', start: len });
|
|
148
|
+
return tokens;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/* ---------------------------------------------------------------------------
|
|
152
|
+
* Parser → directly evaluated AST
|
|
153
|
+
* ------------------------------------------------------------------------- */
|
|
154
|
+
|
|
155
|
+
interface ParserState {
|
|
156
|
+
tokens: Token[];
|
|
157
|
+
pos: number;
|
|
158
|
+
context: BindingContext;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const peek = (s: ParserState): Token => s.tokens[s.pos];
|
|
162
|
+
const advance = (s: ParserState): Token => s.tokens[s.pos++];
|
|
163
|
+
|
|
164
|
+
const expectPunct = (s: ParserState, value: string): void => {
|
|
165
|
+
const t = peek(s);
|
|
166
|
+
if (t.kind !== 'punct' || t.value !== value) {
|
|
167
|
+
throw new Error(`Expected "${value}" in SSR expression, got "${t.value}"`);
|
|
168
|
+
}
|
|
169
|
+
s.pos++;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const matchPunct = (s: ParserState, value: string): boolean => {
|
|
173
|
+
const t = peek(s);
|
|
174
|
+
if (t.kind === 'punct' && t.value === value) {
|
|
175
|
+
s.pos++;
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const skipBalancedGroup = (s: ParserState, open: '(' | '[', close: ')' | ']'): void => {
|
|
182
|
+
expectPunct(s, open);
|
|
183
|
+
const stack: Array<'(' | '['> = [open];
|
|
184
|
+
while (stack.length > 0) {
|
|
185
|
+
const t = advance(s);
|
|
186
|
+
if (!t || t.kind === 'eof') {
|
|
187
|
+
throw new Error(`Unterminated "${open}${close}" group in SSR expression`);
|
|
188
|
+
}
|
|
189
|
+
if (t.kind !== 'punct') continue;
|
|
190
|
+
if (t.value === '(' || t.value === '[') {
|
|
191
|
+
stack.push(t.value);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (t.value === ')' || t.value === ']') {
|
|
195
|
+
const current = stack[stack.length - 1];
|
|
196
|
+
const expected = current === '(' ? ')' : ']';
|
|
197
|
+
if (t.value !== expected) {
|
|
198
|
+
throw new Error(`Mismatched "${current}${expected}" group in SSR expression`);
|
|
199
|
+
}
|
|
200
|
+
stack.pop();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const skipShortCircuitedChainTarget = (s: ParserState): void => {
|
|
206
|
+
const next = peek(s);
|
|
207
|
+
if (next.kind === 'punct' && next.value === '[') {
|
|
208
|
+
skipBalancedGroup(s, '[', ']');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (next.kind === 'punct' && next.value === '(') {
|
|
212
|
+
skipBalancedGroup(s, '(', ')');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (next.kind === 'ident') {
|
|
216
|
+
s.pos++;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
throw new Error('Invalid optional chain in SSR expression');
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const skipShortCircuitedChainRemainder = (s: ParserState): void => {
|
|
223
|
+
while (true) {
|
|
224
|
+
const t = peek(s);
|
|
225
|
+
if (t.kind !== 'punct') break;
|
|
226
|
+
if (t.value === '.') {
|
|
227
|
+
s.pos++;
|
|
228
|
+
const id = advance(s);
|
|
229
|
+
if (id.kind !== 'ident') {
|
|
230
|
+
throw new Error('Expected identifier after "."');
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (t.value === '[') {
|
|
235
|
+
skipBalancedGroup(s, '[', ']');
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (t.value === '(') {
|
|
239
|
+
skipBalancedGroup(s, '(', ')');
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (t.value === '?.') {
|
|
243
|
+
s.pos++;
|
|
244
|
+
skipShortCircuitedChainTarget(s);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Advances through expression tokens without evaluating them.
|
|
253
|
+
*
|
|
254
|
+
* Mirrors the `parsePrimary()` / `parsePostfix()` / `parseUnary()` /
|
|
255
|
+
* `parseExpression()` hierarchy so `parseExpression()` can preserve JavaScript
|
|
256
|
+
* short-circuit semantics for `&&`, `||`, `??`, and ternaries while still
|
|
257
|
+
* leaving the parser in the correct token position.
|
|
258
|
+
*
|
|
259
|
+
* @internal
|
|
260
|
+
*/
|
|
261
|
+
const skipPrimary = (s: ParserState): void => {
|
|
262
|
+
const t = advance(s);
|
|
263
|
+
if (!t || t.kind === 'eof') {
|
|
264
|
+
throw new Error('Unexpected end of SSR expression');
|
|
265
|
+
}
|
|
266
|
+
if (t.kind === 'number' || t.kind === 'string' || t.kind === 'ident') {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (t.kind === 'punct' && t.value === '(') {
|
|
270
|
+
skipExpression(s, 0);
|
|
271
|
+
expectPunct(s, ')');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
throw new Error(`Unexpected token "${t.value}" in SSR expression`);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/** Skips a function-call argument list without evaluating any argument expressions. */
|
|
278
|
+
const skipCall = (s: ParserState): void => {
|
|
279
|
+
expectPunct(s, '(');
|
|
280
|
+
if (!matchPunct(s, ')')) {
|
|
281
|
+
while (true) {
|
|
282
|
+
skipExpression(s, 0);
|
|
283
|
+
if (matchPunct(s, ',')) continue;
|
|
284
|
+
expectPunct(s, ')');
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/** Skips postfix chains such as member access, indexing, optional chaining, and calls. */
|
|
291
|
+
const skipPostfix = (s: ParserState): void => {
|
|
292
|
+
skipPrimary(s);
|
|
293
|
+
|
|
294
|
+
while (true) {
|
|
295
|
+
const t = peek(s);
|
|
296
|
+
if (t.kind !== 'punct') break;
|
|
297
|
+
|
|
298
|
+
if (t.value === '.') {
|
|
299
|
+
s.pos++;
|
|
300
|
+
const id = advance(s);
|
|
301
|
+
if (id.kind !== 'ident') {
|
|
302
|
+
throw new Error('Expected identifier after "."');
|
|
303
|
+
}
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (t.value === '?.') {
|
|
308
|
+
s.pos++;
|
|
309
|
+
skipShortCircuitedChainTarget(s);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (t.value === '[') {
|
|
314
|
+
s.pos++;
|
|
315
|
+
skipExpression(s, 0);
|
|
316
|
+
expectPunct(s, ']');
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (t.value === '(') {
|
|
321
|
+
skipCall(s);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/** Skips unary operators before delegating to the postfix-skipping parser path. */
|
|
330
|
+
const skipUnary = (s: ParserState): void => {
|
|
331
|
+
const t = peek(s);
|
|
332
|
+
if (t.kind === 'punct' && (t.value === '!' || t.value === '-' || t.value === '+')) {
|
|
333
|
+
s.pos++;
|
|
334
|
+
skipUnary(s);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (t.kind === 'ident' && t.value === 'typeof') {
|
|
338
|
+
s.pos++;
|
|
339
|
+
skipUnary(s);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
skipPostfix(s);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/** Skips a full expression subtree while preserving operator precedence and token position. */
|
|
346
|
+
const skipExpression = (s: ParserState, minPrec = 0): void => {
|
|
347
|
+
skipUnary(s);
|
|
348
|
+
|
|
349
|
+
while (true) {
|
|
350
|
+
const t = peek(s);
|
|
351
|
+
if (t.kind !== 'punct') break;
|
|
352
|
+
|
|
353
|
+
if (t.value === '?' && minPrec <= 0) {
|
|
354
|
+
s.pos++;
|
|
355
|
+
skipExpression(s, 0);
|
|
356
|
+
expectPunct(s, ':');
|
|
357
|
+
skipExpression(s, 0);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const prec = BIN_PRECEDENCE[t.value];
|
|
362
|
+
if (prec === undefined || prec < minPrec) break;
|
|
363
|
+
s.pos++;
|
|
364
|
+
skipExpression(s, prec + 1);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const lookupIdent = (s: ParserState, name: string): unknown => {
|
|
369
|
+
if (name === 'true') return true;
|
|
370
|
+
if (name === 'false') return false;
|
|
371
|
+
if (name === 'null') return null;
|
|
372
|
+
if (name === 'undefined') return undefined;
|
|
373
|
+
if (isPrototypePollutionKey(name)) return undefined;
|
|
374
|
+
return unwrap((s.context as Record<string, unknown>)[name]);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const safeMember = (obj: unknown, key: PropertyKey): unknown => {
|
|
378
|
+
if (obj == null) return undefined;
|
|
379
|
+
if (typeof key === 'string' && isPrototypePollutionKey(key)) return undefined;
|
|
380
|
+
return (obj as Record<PropertyKey, unknown>)[key];
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// Pratt-parser precedence table for binary operators.
|
|
384
|
+
const BIN_PRECEDENCE: Record<string, number> = {
|
|
385
|
+
'||': 1,
|
|
386
|
+
'??': 1,
|
|
387
|
+
'&&': 2,
|
|
388
|
+
'==': 3,
|
|
389
|
+
'!=': 3,
|
|
390
|
+
'===': 3,
|
|
391
|
+
'!==': 3,
|
|
392
|
+
'<': 4,
|
|
393
|
+
'<=': 4,
|
|
394
|
+
'>': 4,
|
|
395
|
+
'>=': 4,
|
|
396
|
+
'+': 5,
|
|
397
|
+
'-': 5,
|
|
398
|
+
'*': 6,
|
|
399
|
+
'/': 6,
|
|
400
|
+
'%': 6,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const parseExpression = (s: ParserState, minPrec = 0): unknown => {
|
|
404
|
+
let left = parseUnary(s);
|
|
405
|
+
|
|
406
|
+
while (true) {
|
|
407
|
+
const t = peek(s);
|
|
408
|
+
if (t.kind !== 'punct') break;
|
|
409
|
+
|
|
410
|
+
// Ternary
|
|
411
|
+
if (t.value === '?' && minPrec <= 0) {
|
|
412
|
+
s.pos++;
|
|
413
|
+
if (left) {
|
|
414
|
+
const consequent = parseExpression(s, 0);
|
|
415
|
+
expectPunct(s, ':');
|
|
416
|
+
skipExpression(s, 0);
|
|
417
|
+
left = consequent;
|
|
418
|
+
} else {
|
|
419
|
+
skipExpression(s, 0);
|
|
420
|
+
expectPunct(s, ':');
|
|
421
|
+
left = parseExpression(s, 0);
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const prec = BIN_PRECEDENCE[t.value];
|
|
427
|
+
if (prec === undefined || prec < minPrec) break;
|
|
428
|
+
s.pos++;
|
|
429
|
+
if (t.value === '&&') {
|
|
430
|
+
if (!left) {
|
|
431
|
+
skipExpression(s, prec + 1);
|
|
432
|
+
} else {
|
|
433
|
+
left = parseExpression(s, prec + 1);
|
|
434
|
+
}
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (t.value === '||') {
|
|
438
|
+
if (left) {
|
|
439
|
+
skipExpression(s, prec + 1);
|
|
440
|
+
} else {
|
|
441
|
+
left = parseExpression(s, prec + 1);
|
|
442
|
+
}
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (t.value === '??') {
|
|
446
|
+
if (left !== null && left !== undefined) {
|
|
447
|
+
skipExpression(s, prec + 1);
|
|
448
|
+
} else {
|
|
449
|
+
left = parseExpression(s, prec + 1);
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const right = parseExpression(s, prec + 1);
|
|
454
|
+
left = applyBinary(t.value, left, right);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return left;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const applyBinary = (op: string, l: unknown, r: unknown): unknown => {
|
|
461
|
+
switch (op) {
|
|
462
|
+
case '||':
|
|
463
|
+
return l || r;
|
|
464
|
+
case '&&':
|
|
465
|
+
return l && r;
|
|
466
|
+
case '??':
|
|
467
|
+
return l ?? r;
|
|
468
|
+
case '==':
|
|
469
|
+
// Intentional loose equality: the SSR expression grammar mirrors the
|
|
470
|
+
// JavaScript operators users write in templates. `===` and `!==` are
|
|
471
|
+
// available for strict comparisons.
|
|
472
|
+
|
|
473
|
+
return l == r;
|
|
474
|
+
case '!=':
|
|
475
|
+
// Intentional loose inequality (see `==` note above).
|
|
476
|
+
|
|
477
|
+
return l != r;
|
|
478
|
+
case '===':
|
|
479
|
+
return l === r;
|
|
480
|
+
case '!==':
|
|
481
|
+
return l !== r;
|
|
482
|
+
case '<':
|
|
483
|
+
return (l as number) < (r as number);
|
|
484
|
+
case '<=':
|
|
485
|
+
return (l as number) <= (r as number);
|
|
486
|
+
case '>':
|
|
487
|
+
return (l as number) > (r as number);
|
|
488
|
+
case '>=':
|
|
489
|
+
return (l as number) >= (r as number);
|
|
490
|
+
case '+':
|
|
491
|
+
// String concat for either operand is a string
|
|
492
|
+
if (typeof l === 'string' || typeof r === 'string') {
|
|
493
|
+
return String(l ?? '') + String(r ?? '');
|
|
494
|
+
}
|
|
495
|
+
return (l as number) + (r as number);
|
|
496
|
+
case '-':
|
|
497
|
+
return (l as number) - (r as number);
|
|
498
|
+
case '*':
|
|
499
|
+
return (l as number) * (r as number);
|
|
500
|
+
case '/':
|
|
501
|
+
return (l as number) / (r as number);
|
|
502
|
+
case '%':
|
|
503
|
+
return (l as number) % (r as number);
|
|
504
|
+
default:
|
|
505
|
+
throw new Error(`Unsupported binary operator "${op}"`);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const parseUnary = (s: ParserState): unknown => {
|
|
510
|
+
const t = peek(s);
|
|
511
|
+
if (t.kind === 'punct') {
|
|
512
|
+
if (t.value === '!') {
|
|
513
|
+
s.pos++;
|
|
514
|
+
return !parseUnary(s);
|
|
515
|
+
}
|
|
516
|
+
if (t.value === '-') {
|
|
517
|
+
s.pos++;
|
|
518
|
+
return -(parseUnary(s) as number);
|
|
519
|
+
}
|
|
520
|
+
if (t.value === '+') {
|
|
521
|
+
s.pos++;
|
|
522
|
+
return +(parseUnary(s) as number);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (t.kind === 'ident' && t.value === 'typeof') {
|
|
526
|
+
s.pos++;
|
|
527
|
+
return typeof parseUnary(s);
|
|
528
|
+
}
|
|
529
|
+
return parsePostfix(s);
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const parsePostfix = (s: ParserState): unknown => {
|
|
533
|
+
let value = parsePrimary(s);
|
|
534
|
+
let thisArg: unknown = undefined;
|
|
535
|
+
|
|
536
|
+
while (true) {
|
|
537
|
+
const t = peek(s);
|
|
538
|
+
if (t.kind !== 'punct') break;
|
|
539
|
+
|
|
540
|
+
if (t.value === '.') {
|
|
541
|
+
s.pos++;
|
|
542
|
+
const id = advance(s);
|
|
543
|
+
if (id.kind !== 'ident') {
|
|
544
|
+
throw new Error('Expected identifier after "."');
|
|
545
|
+
}
|
|
546
|
+
thisArg = value;
|
|
547
|
+
value = safeMember(value, id.value);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (t.value === '?.') {
|
|
552
|
+
s.pos++;
|
|
553
|
+
if (value == null) {
|
|
554
|
+
skipShortCircuitedChainTarget(s);
|
|
555
|
+
skipShortCircuitedChainRemainder(s);
|
|
556
|
+
value = undefined;
|
|
557
|
+
thisArg = undefined;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const next = peek(s);
|
|
561
|
+
if (next.kind === 'punct' && next.value === '[') {
|
|
562
|
+
s.pos++;
|
|
563
|
+
const receiver = value;
|
|
564
|
+
const key = parseExpression(s, 0);
|
|
565
|
+
expectPunct(s, ']');
|
|
566
|
+
thisArg = receiver;
|
|
567
|
+
value = safeMember(value, key as PropertyKey);
|
|
568
|
+
} else if (next.kind === 'punct' && next.value === '(') {
|
|
569
|
+
value = parseCall(s, value, thisArg);
|
|
570
|
+
thisArg = undefined;
|
|
571
|
+
} else if (next.kind === 'ident') {
|
|
572
|
+
s.pos++;
|
|
573
|
+
thisArg = value;
|
|
574
|
+
value = safeMember(value, next.value);
|
|
575
|
+
} else {
|
|
576
|
+
throw new Error('Invalid optional chain in SSR expression');
|
|
577
|
+
}
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (t.value === '[') {
|
|
582
|
+
s.pos++;
|
|
583
|
+
const receiver = value;
|
|
584
|
+
const key = parseExpression(s, 0);
|
|
585
|
+
expectPunct(s, ']');
|
|
586
|
+
thisArg = receiver;
|
|
587
|
+
value = safeMember(value, key as PropertyKey);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (t.value === '(') {
|
|
592
|
+
// Function call
|
|
593
|
+
value = parseCall(s, value, thisArg);
|
|
594
|
+
thisArg = undefined;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return value;
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
const parseCall = (s: ParserState, callee: unknown, thisArg: unknown): unknown => {
|
|
605
|
+
expectPunct(s, '(');
|
|
606
|
+
const args: unknown[] = [];
|
|
607
|
+
if (!matchPunct(s, ')')) {
|
|
608
|
+
while (true) {
|
|
609
|
+
args.push(parseExpression(s, 0));
|
|
610
|
+
if (matchPunct(s, ',')) continue;
|
|
611
|
+
expectPunct(s, ')');
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (typeof callee !== 'function') {
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
return (callee as (...a: unknown[]) => unknown).apply(thisArg, args);
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const parsePrimary = (s: ParserState): unknown => {
|
|
622
|
+
const t = advance(s);
|
|
623
|
+
if (t.kind === 'number') {
|
|
624
|
+
return Number(t.value);
|
|
625
|
+
}
|
|
626
|
+
if (t.kind === 'string') {
|
|
627
|
+
return t.value;
|
|
628
|
+
}
|
|
629
|
+
if (t.kind === 'ident') {
|
|
630
|
+
return lookupIdent(s, t.value);
|
|
631
|
+
}
|
|
632
|
+
if (t.kind === 'punct' && t.value === '(') {
|
|
633
|
+
const value = parseExpression(s, 0);
|
|
634
|
+
expectPunct(s, ')');
|
|
635
|
+
return value;
|
|
636
|
+
}
|
|
637
|
+
throw new Error(`Unexpected token "${t.value}" in SSR expression`);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Evaluates a tightly scoped expression against a binding context.
|
|
642
|
+
*
|
|
643
|
+
* Returns `undefined` when the expression cannot be parsed or evaluated.
|
|
644
|
+
* This matches the behaviour of the previous `new Function()`-based fallback
|
|
645
|
+
* but never invokes dynamic code generation.
|
|
646
|
+
*
|
|
647
|
+
* @param expression - Expression source.
|
|
648
|
+
* @param context - Binding context whose top-level signal/computed values are
|
|
649
|
+
* automatically unwrapped.
|
|
650
|
+
*
|
|
651
|
+
* @internal
|
|
652
|
+
*/
|
|
653
|
+
export const evaluateExpression = <T = unknown>(expression: string, context: BindingContext): T => {
|
|
654
|
+
const trimmed = expression.trim();
|
|
655
|
+
if (trimmed === '') return undefined as T;
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
const tokens = tokenize(trimmed);
|
|
659
|
+
const state: ParserState = { tokens, pos: 0, context };
|
|
660
|
+
const value = parseExpression(state, 0);
|
|
661
|
+
if (peek(state).kind !== 'eof') {
|
|
662
|
+
// Unexpected trailing tokens — fall back to undefined.
|
|
663
|
+
return undefined as T;
|
|
664
|
+
}
|
|
665
|
+
return value as T;
|
|
666
|
+
} catch {
|
|
667
|
+
return undefined as T;
|
|
668
|
+
}
|
|
669
|
+
};
|