@ape-egg/vibe 1.8.0 → 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
@@ -6,6 +6,31 @@ export const hash = () => `_${hashCounter++}`;
6
6
  // Key: normalized expression + '\0' + state keys joined by '\0'
7
7
  const fnCache = new Map();
8
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
+
9
34
  // Evaluate expression in the context of state
10
35
  // Handles @[this.property] for component state and @[property] for global state
11
36
  export const evalInScope = (expr, state, element = null) => {
@@ -34,7 +59,55 @@ export const evalInScope = (expr, state, element = null) => {
34
59
  const allKeys = new Array(keyCount + 1);
35
60
  for (let i = 0; i < keyCount; i++) allKeys[i] = stateKeys[i];
36
61
  allKeys[keyCount] = '$';
37
- fn = new Function(...allKeys, `'use strict'; return (${normalized})`);
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})`);
38
111
  fnCache.set(cacheKey, fn);
39
112
  }
40
113
 
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
  }