@mintjamsinc/ichigojs 0.1.64 → 0.1.66

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
@@ -6865,11 +6865,16 @@
6865
6865
  const ast = parse(source, { ecmaVersion: "latest" });
6866
6866
  const dependencies = new Set();
6867
6867
  const declaredVariables = new Set();
6868
+ const thisAliases = new Set();
6868
6869
  // First, collect all declared variables (const, let, var, function params, etc.)
6869
6870
  ancestor(ast, {
6870
6871
  VariableDeclarator(node) {
6871
6872
  if (node.id.type === 'Identifier') {
6872
6873
  declaredVariables.add(node.id.name);
6874
+ // Detect aliases assigned from `this`, e.g. `let vm = this;`
6875
+ if (node.init && node.init.type === 'ThisExpression') {
6876
+ thisAliases.add(node.id.name);
6877
+ }
6873
6878
  }
6874
6879
  },
6875
6880
  FunctionDeclaration(node) {
@@ -6921,9 +6926,14 @@
6921
6926
  }
6922
6927
  },
6923
6928
  MemberExpression(node) {
6924
- // Handle 'this.propertyName' patterns
6925
- if (node.object.type === 'ThisExpression' && node.property.type === 'Identifier') {
6926
- dependencies.add(node.property.name);
6929
+ // Handle 'this.propertyName' patterns and aliases like 'vm.x' when 'vm = this'
6930
+ if (node.property.type === 'Identifier') {
6931
+ if (node.object.type === 'ThisExpression') {
6932
+ dependencies.add(node.property.name);
6933
+ }
6934
+ else if (node.object.type === 'Identifier' && thisAliases.has(node.object.name)) {
6935
+ dependencies.add(node.property.name);
6936
+ }
6927
6937
  }
6928
6938
  }
6929
6939
  });
@@ -7017,7 +7027,7 @@
7017
7027
  return expression;
7018
7028
  }
7019
7029
  try {
7020
- // Build a map of positions to replace: { start: number, end: number, name: string }[]
7030
+ // Build a map of positions to replace: { start: number, end: number, name: string, asThisAlias?: boolean }[]
7021
7031
  const replacements = [];
7022
7032
  // In script mode we must not wrap in parens (that would make multi-statement input invalid).
7023
7033
  // Offsets from the parser therefore refer directly to the original expression, so no shift.
@@ -7026,13 +7036,17 @@
7026
7036
  const offsetShift = asScript ? 0 : 1;
7027
7037
  const parsedAst = parse(source, { ecmaVersion: 'latest' });
7028
7038
  // Track identifiers that are locally declared within the handler body (let/const/var, function
7029
- // params) so we don't rewrite them to `this.xxx`. Only relevant in script mode, where the user
7030
- // can write declarations; in expression mode there are no declarations to track.
7039
+ // params) so we don't rewrite them to `this.xxx`. Additionally detect `this` aliases such as
7040
+ // `let vm = this;` so that `vm.x` can be rewritten to `this.x` even though `vm` is locally declared.
7031
7041
  const locallyDeclared = new Set();
7042
+ const thisAliases = new Set();
7032
7043
  if (asScript) {
7033
7044
  full(parsedAst, (node) => {
7034
7045
  if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
7035
7046
  locallyDeclared.add(node.id.name);
7047
+ if (node.init && node.init.type === 'ThisExpression') {
7048
+ thisAliases.add(node.id.name);
7049
+ }
7036
7050
  }
7037
7051
  else if (node.type === 'FunctionDeclaration' && node.id?.type === 'Identifier') {
7038
7052
  locallyDeclared.add(node.id.name);
@@ -7049,20 +7063,34 @@
7049
7063
  if (!bindingIdentifiers.has(node.name)) {
7050
7064
  return;
7051
7065
  }
7052
- // Skip identifiers that were declared locally in the handler body
7066
+ // Locally declared identifiers must not be rewritten to `this.xxx`. The one
7067
+ // exception is a `this`-alias (e.g. `let vm = this;`): when such an alias is
7068
+ // used as the OBJECT of a MemberExpression (`vm.x`), we replace just the
7069
+ // object with `this` so that `vm.x` becomes `this.x`. In every other position
7070
+ // (the declaration LHS, a bare reference such as `console.log(vm)`, an
7071
+ // argument, etc.) the alias must be left untouched — rewriting it to
7072
+ // `this.vm` would either produce a syntax error (`let this.vm = this;`) or
7073
+ // change the meaning at runtime.
7074
+ // walk.fullAncestor includes the visited node itself as the last entry of
7075
+ // `ancestors`, so the actual parent node is at length - 2.
7076
+ const parentNode = ancestors.length >= 2 ? ancestors[ancestors.length - 2] : undefined;
7077
+ const isAliasAsMemberObject = parentNode?.type === 'MemberExpression' && parentNode.object === node;
7053
7078
  if (locallyDeclared.has(node.name)) {
7079
+ if (!thisAliases.has(node.name) || !isAliasAsMemberObject) {
7080
+ return;
7081
+ }
7082
+ replacements.push({
7083
+ start: node.start - offsetShift,
7084
+ end: node.end - offsetShift,
7085
+ name: node.name,
7086
+ asThisAlias: true
7087
+ });
7054
7088
  return;
7055
7089
  }
7056
- // Check if this identifier is a property of a MemberExpression
7057
- // (e.g., in 'obj.prop', we should skip 'prop')
7058
- if (ancestors.length >= 1) {
7059
- const parent = ancestors[ancestors.length - 1];
7060
- if (parent.type === 'MemberExpression') {
7061
- // Skip if this identifier is the property (not the object) of a non-computed member access
7062
- if (!parent.computed && parent.property === node) {
7063
- return;
7064
- }
7065
- }
7090
+ // Skip identifiers that are the property (not the object) of a non-computed
7091
+ // member access (e.g., the `prop` in `obj.prop`).
7092
+ if (parentNode?.type === 'MemberExpression' && !parentNode.computed && parentNode.property === node) {
7093
+ return;
7066
7094
  }
7067
7095
  // Add to replacements list (adjust for the wrapping parentheses in expression mode)
7068
7096
  replacements.push({
@@ -7076,9 +7104,8 @@
7076
7104
  // Apply replacements
7077
7105
  let result = expression;
7078
7106
  for (const replacement of replacements) {
7079
- result = result.substring(0, replacement.start) +
7080
- `this.${replacement.name}` +
7081
- result.substring(replacement.end);
7107
+ const insertion = replacement.asThisAlias ? 'this' : `this.${replacement.name}`;
7108
+ result = result.substring(0, replacement.start) + insertion + result.substring(replacement.end);
7082
7109
  }
7083
7110
  return result;
7084
7111
  }
@@ -7443,6 +7470,13 @@
7443
7470
  get expression() {
7444
7471
  return this.#expression;
7445
7472
  }
7473
+ /**
7474
+ * Evaluates the bound expression and returns the current value.
7475
+ * Returns undefined when no evaluator is available (e.g., empty expression).
7476
+ */
7477
+ evaluate() {
7478
+ return this.#evaluator?.evaluate();
7479
+ }
7446
7480
  /**
7447
7481
  * @inheritdoc
7448
7482
  */
@@ -8655,6 +8689,29 @@
8655
8689
  get keyDirective() {
8656
8690
  return this.#keyDirective;
8657
8691
  }
8692
+ /**
8693
+ * Finds a VBindDirective that binds the given attribute name (e.g. "value",
8694
+ * "true-value", "false-value"). Returns undefined if no matching v-bind
8695
+ * directive is registered on this node.
8696
+ *
8697
+ * Used by directives such as v-model that need to read the typed value of a
8698
+ * sibling v-bind without depending on the attribute order in the source HTML.
8699
+ */
8700
+ findBindDirective(attrName) {
8701
+ if (!this.#directives || this.#directives.length === 0) {
8702
+ return undefined;
8703
+ }
8704
+ for (const directive of this.#directives) {
8705
+ if (directive.name !== StandardDirectiveName.V_BIND) {
8706
+ continue;
8707
+ }
8708
+ const bindDirective = directive;
8709
+ if (bindDirective.attributeName === attrName) {
8710
+ return bindDirective;
8711
+ }
8712
+ }
8713
+ return undefined;
8714
+ }
8658
8715
  /**
8659
8716
  * Gets the VBindDirective for options specific to the given directive name.
8660
8717
  * Searches in order: `:options.{directive}` -> `:options`
@@ -11108,14 +11165,13 @@
11108
11165
  * @inheritdoc
11109
11166
  */
11110
11167
  get domUpdater() {
11111
- const identifiers = this.#evaluator?.dependentIdentifiers ?? [];
11112
- // Create and return the DOM updater
11168
+ const self = this;
11113
11169
  const updater = {
11114
11170
  get dependentIdentifiers() {
11115
- return identifiers;
11171
+ return self.#collectDependentIdentifiers();
11116
11172
  },
11117
11173
  applyToDOM: () => {
11118
- this.#render();
11174
+ self.#render();
11119
11175
  }
11120
11176
  };
11121
11177
  return updater;
@@ -11130,7 +11186,38 @@
11130
11186
  * @inheritdoc
11131
11187
  */
11132
11188
  get dependentIdentifiers() {
11133
- return this.#evaluator?.dependentIdentifiers ?? [];
11189
+ return this.#collectDependentIdentifiers();
11190
+ }
11191
+ /**
11192
+ * Collects identifiers this directive's render depends on. For checkboxes
11193
+ * this includes the v-model expression itself plus the expressions bound to
11194
+ * `:value`, `:true-value`, and `:false-value`, since the rendered checked
11195
+ * state changes when any of these change.
11196
+ */
11197
+ #collectDependentIdentifiers() {
11198
+ const ids = new Set(this.#evaluator?.dependentIdentifiers ?? []);
11199
+ const element = this.#vNode.node;
11200
+ if (element instanceof HTMLInputElement) {
11201
+ const manager = this.#vNode.directiveManager;
11202
+ // For checkboxes and radios, v-model's rendered state depends on
11203
+ // the element's typed `:value` binding (if present). Checkboxes
11204
+ // additionally depend on `:true-value` / `:false-value` bindings.
11205
+ if (element.type === 'checkbox' || element.type === 'radio') {
11206
+ const valueBind = manager?.findBindDirective('value');
11207
+ if (valueBind) {
11208
+ valueBind.dependentIdentifiers.forEach(id => ids.add(id));
11209
+ }
11210
+ }
11211
+ if (element.type === 'checkbox') {
11212
+ for (const attrName of ['true-value', 'false-value']) {
11213
+ const bindDirective = manager?.findBindDirective(attrName);
11214
+ if (bindDirective) {
11215
+ bindDirective.dependentIdentifiers.forEach(id => ids.add(id));
11216
+ }
11217
+ }
11218
+ }
11219
+ }
11220
+ return Array.from(ids);
11134
11221
  }
11135
11222
  /**
11136
11223
  * @inheritdoc
@@ -11210,14 +11297,16 @@
11210
11297
  // Update the element based on its type
11211
11298
  if (element instanceof HTMLInputElement) {
11212
11299
  if (element.type === 'checkbox') {
11213
- element.checked = !!value;
11300
+ this.#renderCheckbox(element, value);
11214
11301
  }
11215
11302
  else if (element.type === 'radio') {
11216
- // Prefer the original typed value stored by VBindDirective (:value binding)
11217
- // to avoid type coercion issues (e.g., boolean false vs string "false").
11218
- const radioValue = element._value !== undefined
11219
- ? element._value
11220
- : element.value;
11303
+ // Prefer the typed value from a sibling :value binding when present,
11304
+ // falling back to any stored `_value` or the raw string `value`.
11305
+ const manager = this.#vNode.directiveManager;
11306
+ const bindDirective = manager?.findBindDirective('value');
11307
+ const radioValue = bindDirective !== undefined
11308
+ ? bindDirective.evaluate()
11309
+ : (element._value !== undefined ? element._value : element.value);
11221
11310
  element.checked = radioValue === value;
11222
11311
  }
11223
11312
  else {
@@ -11243,14 +11332,16 @@
11243
11332
  // Get the new value based on element type
11244
11333
  if (target instanceof HTMLInputElement) {
11245
11334
  if (target.type === 'checkbox') {
11246
- newValue = target.checked;
11335
+ newValue = this.#computeCheckboxNewValue(target);
11247
11336
  }
11248
11337
  else if (target.type === 'radio') {
11249
- // Prefer the original typed value stored by VBindDirective (:value binding)
11250
- // to preserve the type on write-back (e.g., boolean false, number 0).
11251
- newValue = target._value !== undefined
11252
- ? target._value
11253
- : target.value;
11338
+ // Prefer the typed value from a sibling :value binding when present,
11339
+ // falling back to any stored `_value` or the raw string `value`.
11340
+ const manager = this.#vNode.directiveManager;
11341
+ const bindDirective = manager?.findBindDirective('value');
11342
+ newValue = bindDirective !== undefined
11343
+ ? bindDirective.evaluate()
11344
+ : (target._value !== undefined ? target._value : target.value);
11254
11345
  }
11255
11346
  else {
11256
11347
  newValue = target.value;
@@ -11259,13 +11350,109 @@
11259
11350
  else if (target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
11260
11351
  newValue = target.value;
11261
11352
  }
11262
- // Apply modifiers to the value
11263
- newValue = this.#applyModifiers(newValue);
11353
+ // Apply modifiers to the value (skip for checkboxes: their value
11354
+ // is either boolean, a custom true/false value, or an array, none
11355
+ // of which should be coerced by .trim or .number).
11356
+ const isCheckbox = target instanceof HTMLInputElement && target.type === 'checkbox';
11357
+ if (!isCheckbox) {
11358
+ newValue = this.#applyModifiers(newValue);
11359
+ }
11264
11360
  // Update the binding
11265
11361
  this.#updateBinding(newValue);
11266
11362
  };
11267
11363
  element.addEventListener(eventName, this.#listener);
11268
11364
  }
11365
+ /**
11366
+ * Renders a checkbox in one of three modes (Vue-compatible):
11367
+ * 1. Array binding: the bound value is an array; the checkbox is checked
11368
+ * when its element-value is a member of that array.
11369
+ * 2. true-value/false-value binding: when `:true-value` (and optionally
11370
+ * `:false-value`) is provided via v-bind, the checkbox is checked
11371
+ * when the bound value strictly equals the resolved true-value.
11372
+ * 3. Boolean binding (default): the bound value is coerced to boolean.
11373
+ */
11374
+ #renderCheckbox(element, value) {
11375
+ if (Array.isArray(value)) {
11376
+ const elementValue = this.#resolveCheckboxElementValue(element);
11377
+ element.checked = value.indexOf(elementValue) !== -1;
11378
+ return;
11379
+ }
11380
+ const trueValueDescriptor = this.#resolveCheckboxTrueFalseValues(element);
11381
+ if (trueValueDescriptor) {
11382
+ element.checked = value === trueValueDescriptor.trueValue;
11383
+ return;
11384
+ }
11385
+ element.checked = !!value;
11386
+ }
11387
+ /**
11388
+ * Computes the value to write back to the bound expression when a checkbox
11389
+ * change event fires. Mirrors the three-mode logic of #renderCheckbox.
11390
+ *
11391
+ * For array binding, the current value of the bound expression is read so
11392
+ * that a fresh array can be returned (the existing array is not mutated,
11393
+ * which preserves reactivity semantics).
11394
+ */
11395
+ #computeCheckboxNewValue(target) {
11396
+ const currentValue = this.#evaluator?.evaluate();
11397
+ if (Array.isArray(currentValue)) {
11398
+ const elementValue = this.#resolveCheckboxElementValue(target);
11399
+ const next = currentValue.slice();
11400
+ const index = next.indexOf(elementValue);
11401
+ if (target.checked) {
11402
+ if (index === -1) {
11403
+ next.push(elementValue);
11404
+ }
11405
+ }
11406
+ else {
11407
+ if (index !== -1) {
11408
+ next.splice(index, 1);
11409
+ }
11410
+ }
11411
+ return next;
11412
+ }
11413
+ const trueValueDescriptor = this.#resolveCheckboxTrueFalseValues(target);
11414
+ if (trueValueDescriptor) {
11415
+ return target.checked ? trueValueDescriptor.trueValue : trueValueDescriptor.falseValue;
11416
+ }
11417
+ return target.checked;
11418
+ }
11419
+ /**
11420
+ * Resolves the typed element value for a checkbox. Prefers the value bound
11421
+ * via `:value` (evaluated through the sibling VBindDirective so type is
11422
+ * preserved), then the typed value previously stored on the element by
11423
+ * VBindDirective, and finally the raw string `value` attribute.
11424
+ */
11425
+ #resolveCheckboxElementValue(element) {
11426
+ const bindDirective = this.#vNode.directiveManager?.findBindDirective('value');
11427
+ if (bindDirective) {
11428
+ return bindDirective.evaluate();
11429
+ }
11430
+ if (element._value !== undefined) {
11431
+ return element._value;
11432
+ }
11433
+ return element.value;
11434
+ }
11435
+ /**
11436
+ * Resolves the (true-value, false-value) pair for a checkbox if either is
11437
+ * bound via `:true-value` or `:false-value`. Returns undefined when no
11438
+ * true/false value binding is present, signalling that the default boolean
11439
+ * mode should be used.
11440
+ *
11441
+ * If only one of the two is bound, the other defaults match Vue: an unbound
11442
+ * true-value defaults to literal `true`, an unbound false-value to `false`.
11443
+ */
11444
+ #resolveCheckboxTrueFalseValues(element) {
11445
+ const manager = this.#vNode.directiveManager;
11446
+ const trueBind = manager?.findBindDirective('true-value');
11447
+ const falseBind = manager?.findBindDirective('false-value');
11448
+ if (!trueBind && !falseBind) {
11449
+ return undefined;
11450
+ }
11451
+ return {
11452
+ trueValue: trueBind ? trueBind.evaluate() : true,
11453
+ falseValue: falseBind ? falseBind.evaluate() : false,
11454
+ };
11455
+ }
11269
11456
  /**
11270
11457
  * Applies modifiers to the input value.
11271
11458
  * @param value The value to process.