@mintjamsinc/ichigojs 0.1.55 → 0.1.57

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/dist/ichigo.cjs CHANGED
@@ -6737,11 +6737,18 @@
6737
6737
  * Extracts variable and function names used in the expression.
6738
6738
  * @param expression The expression string to analyze.
6739
6739
  * @param functionDependencies A dictionary mapping function names to their dependencies.
6740
+ * @param options Optional parsing options.
6741
+ * - asScript: If true, parse the input as a Script (allows multi-statement source with semicolons,
6742
+ * declarations, control-flow, etc.). If false/omitted, parse as a single expression (the default,
6743
+ * for backward compatibility with interpolation and binding directives).
6740
6744
  * @returns An array of identifier names.
6741
6745
  */
6742
- static extractIdentifiers(expression, functionDependencies) {
6746
+ static extractIdentifiers(expression, functionDependencies, options) {
6743
6747
  const identifiers = new Set();
6744
- const ast = parse(`(${expression})`, { ecmaVersion: "latest" });
6748
+ // In expression mode we wrap in parens so acorn parses the source as a single expression.
6749
+ // In script mode we parse as a Program so that multi-statement bodies (e.g. "a=1; b=2") work.
6750
+ const source = options?.asScript ? expression : `(${expression})`;
6751
+ const ast = parse(source, { ecmaVersion: "latest" });
6745
6752
  // Use walk.full instead of walk.simple to visit ALL nodes including assignment LHS
6746
6753
  full(ast, (node) => {
6747
6754
  if (node.type === 'Identifier') {
@@ -6973,10 +6980,10 @@
6973
6980
  * @param identifiers The list of identifiers that are available in bindings.
6974
6981
  * @returns The rewritten expression.
6975
6982
  */
6976
- static rewriteExpression(expression, identifiers) {
6983
+ static rewriteExpression(expression, identifiers, options) {
6977
6984
  // Reserved words and built-in objects that should not be prefixed with 'this.'
6978
6985
  const reserved = new Set([
6979
- 'event', '$ctx', '$newValue',
6986
+ 'event', '$event', '$ctx', '$newValue',
6980
6987
  'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
6981
6988
  'Math', 'Date', 'String', 'Number', 'Boolean', 'Array', 'Object',
6982
6989
  'JSON', 'console', 'window', 'document', 'navigator',
@@ -6988,7 +6995,7 @@
6988
6995
  // identifiers that are used (right-hand side), not assigned to (left-hand side)
6989
6996
  let allIdentifiersInExpression;
6990
6997
  try {
6991
- allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {});
6998
+ allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {}, options);
6992
6999
  }
6993
7000
  catch (error) {
6994
7001
  console.warn('[ichigo.js] Failed to extract identifiers from expression:', expression, error);
@@ -7010,7 +7017,26 @@
7010
7017
  try {
7011
7018
  // Build a map of positions to replace: { start: number, end: number, name: string }[]
7012
7019
  const replacements = [];
7013
- const parsedAst = parse(`(${expression})`, { ecmaVersion: 'latest' });
7020
+ // In script mode we must not wrap in parens (that would make multi-statement input invalid).
7021
+ // Offsets from the parser therefore refer directly to the original expression, so no shift.
7022
+ const asScript = options?.asScript === true;
7023
+ const source = asScript ? expression : `(${expression})`;
7024
+ const offsetShift = asScript ? 0 : 1;
7025
+ const parsedAst = parse(source, { ecmaVersion: 'latest' });
7026
+ // Track identifiers that are locally declared within the handler body (let/const/var, function
7027
+ // params) so we don't rewrite them to `this.xxx`. Only relevant in script mode, where the user
7028
+ // can write declarations; in expression mode there are no declarations to track.
7029
+ const locallyDeclared = new Set();
7030
+ if (asScript) {
7031
+ full(parsedAst, (node) => {
7032
+ if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
7033
+ locallyDeclared.add(node.id.name);
7034
+ }
7035
+ else if (node.type === 'FunctionDeclaration' && node.id?.type === 'Identifier') {
7036
+ locallyDeclared.add(node.id.name);
7037
+ }
7038
+ });
7039
+ }
7014
7040
  // Collect all identifier nodes that should be replaced
7015
7041
  // Use walk.fullAncestor to visit ALL nodes (including assignment LHS) while tracking ancestors
7016
7042
  fullAncestor(parsedAst, (node, _state, ancestors) => {
@@ -7021,6 +7047,10 @@
7021
7047
  if (!bindingIdentifiers.has(node.name)) {
7022
7048
  return;
7023
7049
  }
7050
+ // Skip identifiers that were declared locally in the handler body
7051
+ if (locallyDeclared.has(node.name)) {
7052
+ return;
7053
+ }
7024
7054
  // Check if this identifier is a property of a MemberExpression
7025
7055
  // (e.g., in 'obj.prop', we should skip 'prop')
7026
7056
  if (ancestors.length >= 1) {
@@ -7032,10 +7062,10 @@
7032
7062
  }
7033
7063
  }
7034
7064
  }
7035
- // Add to replacements list (adjust for the wrapping parentheses)
7065
+ // Add to replacements list (adjust for the wrapping parentheses in expression mode)
7036
7066
  replacements.push({
7037
- start: node.start - 1,
7038
- end: node.end - 1,
7067
+ start: node.start - offsetShift,
7068
+ end: node.end - offsetShift,
7039
7069
  name: node.name
7040
7070
  });
7041
7071
  });
@@ -11234,10 +11264,12 @@
11234
11264
  this.#eventName = parts[0];
11235
11265
  parts.slice(1).forEach(mod => this.#modifiers.add(mod));
11236
11266
  }
11237
- // Parse the expression to extract identifiers and create the handler wrapper
11267
+ // Parse the expression to extract identifiers and create the handler wrapper.
11268
+ // Event handlers are parsed in script mode so that users can write multi-statement bodies
11269
+ // (e.g. "a=1; b=2"), declarations, and control-flow constructs — matching Vue semantics.
11238
11270
  const expression = context.attribute.value;
11239
11271
  if (expression) {
11240
- this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
11272
+ this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies, { asScript: true });
11241
11273
  }
11242
11274
  // Check if this is a lifecycle hook or a regular event
11243
11275
  if (this.#eventName && this.#isLifecycleHook(this.#eventName)) {
@@ -11367,23 +11399,27 @@
11367
11399
  this.#listener = (event) => {
11368
11400
  // Check key modifiers for keyboard events
11369
11401
  if (event instanceof KeyboardEvent) {
11370
- const keyModifiers = ['enter', 'tab', 'delete', 'esc', 'space', 'up', 'down', 'left', 'right'];
11371
- const hasKeyModifier = keyModifiers.some(key => this.#modifiers.has(key));
11402
+ // Map of modifier alias -> KeyboardEvent.key values it matches.
11403
+ // Multiple values allow a single modifier to match several physical keys
11404
+ // (e.g. `.delete` matches both Delete and Backspace, matching Vue's behavior).
11405
+ // Multiple aliases pointing to the same key are allowed (e.g. `.esc` / `.escape`).
11406
+ const keyMap = {
11407
+ 'enter': ['Enter'],
11408
+ 'tab': ['Tab'],
11409
+ 'delete': ['Delete', 'Backspace'],
11410
+ 'esc': ['Escape'],
11411
+ 'escape': ['Escape'],
11412
+ 'space': [' '],
11413
+ 'up': ['ArrowUp'],
11414
+ 'down': ['ArrowDown'],
11415
+ 'left': ['ArrowLeft'],
11416
+ 'right': ['ArrowRight']
11417
+ };
11418
+ const hasKeyModifier = Object.keys(keyMap).some(key => this.#modifiers.has(key));
11372
11419
  if (hasKeyModifier) {
11373
- const keyMap = {
11374
- 'enter': 'Enter',
11375
- 'tab': 'Tab',
11376
- 'delete': 'Delete',
11377
- 'esc': 'Escape',
11378
- 'space': ' ',
11379
- 'up': 'ArrowUp',
11380
- 'down': 'ArrowDown',
11381
- 'left': 'ArrowLeft',
11382
- 'right': 'ArrowRight'
11383
- };
11384
11420
  let keyMatched = false;
11385
- for (const [modifier, keyValue] of Object.entries(keyMap)) {
11386
- if (this.#modifiers.has(modifier) && event.key === keyValue) {
11421
+ for (const [modifier, keyValues] of Object.entries(keyMap)) {
11422
+ if (this.#modifiers.has(modifier) && keyValues.includes(event.key)) {
11387
11423
  keyMatched = true;
11388
11424
  break;
11389
11425
  }
@@ -11451,10 +11487,11 @@
11451
11487
  // This allows the method to access the DOM element, VNode, and userData
11452
11488
  return originalMethod($ctx);
11453
11489
  }
11454
- // For inline expressions, rewrite to use 'this' context
11455
- // This allows assignments like "currentTab = 'shop'" to work correctly
11456
- const rewrittenExpr = this.#rewriteExpression(expression, identifiers);
11457
- const funcBody = `return (${rewrittenExpr});`;
11490
+ // For inline bodies, rewrite to use 'this' context
11491
+ // This allows assignments like "currentTab = 'shop'" to work correctly.
11492
+ // Script mode allows multi-statement bodies (e.g. "a=1; init()") and control-flow.
11493
+ const rewrittenExpr = this.#rewriteExpression(expression, identifiers, { asScript: true });
11494
+ const funcBody = rewrittenExpr;
11458
11495
  const func = new Function('$ctx', funcBody);
11459
11496
  return func.call(bindings?.raw, $ctx);
11460
11497
  };
@@ -11484,12 +11521,15 @@
11484
11521
  // Pass event as first argument and $ctx as second argument
11485
11522
  return originalMethod(event, $ctx);
11486
11523
  }
11487
- // For inline expressions, rewrite to use 'this' context
11488
- // This allows assignments like "currentTab = 'shop'" to work correctly
11489
- const rewrittenExpr = this.#rewriteExpression(expression, identifiers);
11490
- const funcBody = `return (${rewrittenExpr});`;
11491
- const func = new Function('event', '$ctx', funcBody);
11492
- return func.call(bindings?.raw, event, $ctx);
11524
+ // For inline bodies, rewrite to use 'this' context
11525
+ // This allows assignments like "currentTab = 'shop'" to work correctly.
11526
+ // Script mode allows multi-statement bodies (e.g. "a=1; b=2") and control-flow,
11527
+ // so we emit the rewritten source directly as the function body (no `return (...)`).
11528
+ const rewrittenExpr = this.#rewriteExpression(expression, identifiers, { asScript: true });
11529
+ const funcBody = rewrittenExpr;
11530
+ // '$event' is an alias for 'event' for Vue compatibility
11531
+ const func = new Function('event', '$event', '$ctx', funcBody);
11532
+ return func.call(bindings?.raw, event, event, $ctx);
11493
11533
  };
11494
11534
  }
11495
11535
  /**
@@ -11500,8 +11540,8 @@
11500
11540
  * @param identifiers The list of identifiers that are available in bindings.
11501
11541
  * @returns The rewritten expression.
11502
11542
  */
11503
- #rewriteExpression(expression, identifiers) {
11504
- return ExpressionUtils.rewriteExpression(expression, identifiers);
11543
+ #rewriteExpression(expression, identifiers, options) {
11544
+ return ExpressionUtils.rewriteExpression(expression, identifiers, options);
11505
11545
  }
11506
11546
  }
11507
11547