@ape-egg/vibe 1.7.2 → 1.9.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/runtime/utils.js CHANGED
@@ -2,6 +2,35 @@
2
2
  let hashCounter = 0;
3
3
  export const hash = () => `_${hashCounter++}`;
4
4
 
5
+ // Function compilation cache: avoids creating new Function() for repeated expressions
6
+ // Key: normalized expression + '\0' + state keys joined by '\0'
7
+ const fnCache = new Map();
8
+
9
+ // Built-ins and reserved words that must NEVER be pre-declared as `var`
10
+ // inside a compiled expression — they're either real globals we want to
11
+ // reach, or JS keywords that would be a SyntaxError to shadow.
12
+ const EVAL_IDENT_EXCLUDE = new Set([
13
+ // Primitive literals / special identifiers
14
+ 'true', 'false', 'null', 'undefined', 'NaN', 'Infinity', 'this',
15
+ // JS reserved words (shadowing any of these is a SyntaxError)
16
+ 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
17
+ 'default', 'delete', 'do', 'else', 'export', 'extends', 'finally',
18
+ 'for', 'function', 'if', 'import', 'in', 'instanceof', 'let', 'new',
19
+ 'of', 'return', 'super', 'switch', 'throw', 'try', 'typeof', 'var',
20
+ 'void', 'while', 'with', 'yield', 'async', 'await',
21
+ // Common runtime globals authors reach for
22
+ 'Math', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Date',
23
+ 'JSON', 'RegExp', 'Error', 'Map', 'Set', 'WeakMap', 'WeakSet',
24
+ 'Promise', 'Symbol', 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
25
+ 'window', 'document', 'console', 'globalThis',
26
+ ]);
27
+
28
+ // Match free identifiers (not property accesses like `obj.foo` or keys
29
+ // inside strings). The `(?<![.\w$'"])` lookbehind skips tokens preceded
30
+ // by a dot (property access), an identifier char (mid-identifier), or
31
+ // a quote (inside a string literal).
32
+ const FREE_IDENT_REGEX = /(?<![.\w$'"])[a-zA-Z_$][\w$]*/g;
33
+
5
34
  // Evaluate expression in the context of state
6
35
  // Handles @[this.property] for component state and @[property] for global state
7
36
  export const evalInScope = (expr, state, element = null) => {
@@ -18,13 +47,76 @@ export const evalInScope = (expr, state, element = null) => {
18
47
  }
19
48
  }
20
49
 
21
- // Add $ to scope for this.property references
22
- const effectiveState = { ...state, $: state };
50
+ // Get state keys once (triggers ownKeys trap only once for Proxies)
51
+ const stateKeys = Object.keys(state);
52
+ const keyCount = stateKeys.length;
53
+
54
+ // Cache lookup: expression + key signature → compiled function
55
+ // All iteration instances share the same state shape, so this hits cache 999/1000 times
56
+ const cacheKey = normalized + '\0' + stateKeys.join('\0');
57
+ let fn = fnCache.get(cacheKey);
58
+ if (!fn) {
59
+ const allKeys = new Array(keyCount + 1);
60
+ for (let i = 0; i < keyCount; i++) allKeys[i] = stateKeys[i];
61
+ allKeys[keyCount] = '$';
62
+
63
+ // Rewrite free identifiers that are neither state keys nor known
64
+ // globals/reserved words so they resolve to `undefined` instead of
65
+ // throwing ReferenceError. `typeof x` is the only operator that can
66
+ // probe a name without reading it, so we guard each such reference:
67
+ //
68
+ // missingProp → (typeof missingProp==='undefined'?void 0:missingProp)
69
+ //
70
+ // This keeps author intuition working for absent state (`!undef` is
71
+ // `true`, `undef?.x` is `undefined`) while leaving real globals —
72
+ // functions the app exposes on `window` — reachable via normal
73
+ // identifier lookup. The previous approach (`var undef;` hoist) also
74
+ // shadowed those globals to `undefined`, so a template call like
75
+ // `getLevelByExperience(exp)` threw TypeError and silently rendered
76
+ // "undefined".
77
+ const known = new Set(allKeys);
78
+ for (const ex of EVAL_IDENT_EXCLUDE) known.add(ex);
79
+
80
+ // Arrow function parameters are bound locally — rewriting them breaks
81
+ // the arrow's parameter list syntax (e.g. `f =>` must stay a bare ident).
82
+ // Collect both `(a, b) =>` and `x =>` forms, strip defaults, and treat
83
+ // them as known so the main pass leaves them untouched.
84
+ normalized.replace(/\(([^()]*)\)\s*=>/g, (_, paramList) => {
85
+ for (const p of paramList.split(',')) {
86
+ const name = p.trim().split('=')[0].trim();
87
+ if (/^[a-zA-Z_$][\w$]*$/.test(name)) known.add(name);
88
+ }
89
+ return '';
90
+ });
91
+ const singleParamRe = /(?:^|[^\w$.])([a-zA-Z_$][\w$]*)\s*=>/g;
92
+ let paramMatch;
93
+ while ((paramMatch = singleParamRe.exec(normalized)) !== null) {
94
+ known.add(paramMatch[1]);
95
+ }
96
+
97
+ // Match string literals first so hyphens inside them (e.g. 'the-arena')
98
+ // don't cause the trailing word to be rewritten as a free identifier.
99
+ // Also capture object-literal keys (`{foo: ...}` / `, foo: ...`) as a
100
+ // second alternative — they're property names, not references, so
101
+ // rewriting them produces invalid JS (computed key without brackets).
102
+ const source = normalized.replace(
103
+ /('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")|((?<=[{,]\s*)[a-zA-Z_$][\w$]*(?=\s*:))|(?<![.\w$'"])[a-zA-Z_$][\w$]*/g,
104
+ (match, strLit, objKey) => {
105
+ if (strLit !== undefined) return match;
106
+ if (objKey !== undefined) return match;
107
+ return known.has(match) ? match : `(typeof ${match}==='undefined'?void 0:${match})`;
108
+ },
109
+ );
110
+ fn = new Function(...allKeys, `'use strict'; return (${source})`);
111
+ fnCache.set(cacheKey, fn);
112
+ }
23
113
 
24
- const keys = Object.keys(effectiveState);
25
- const values = Object.values(effectiveState);
114
+ // Build values array matching the cached function's parameter order
115
+ const values = new Array(keyCount + 1);
116
+ for (let i = 0; i < keyCount; i++) values[i] = state[stateKeys[i]];
117
+ values[keyCount] = state;
26
118
 
27
- const result = new Function(...keys, `'use strict'; return (${normalized})`)(...values);
119
+ const result = fn(...values);
28
120
 
29
121
  // If result is undefined and we're accessing a component property, try case-insensitive match
30
122
  // This handles HTML lowercasing attribute names like @[this.iconName] -> @[this.iconname]
package/vibe.css CHANGED
@@ -27,6 +27,8 @@
27
27
  * to make the wrapper invisible in the layout. Children render as if the wrapper doesn't exist.
28
28
  */
29
29
  component,
30
- div.component {
30
+ div.component,
31
+ slot,
32
+ div.slot {
31
33
  display: contents;
32
34
  }