@mintjamsinc/ichigojs 0.1.54 → 0.1.56

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.
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
2
2
  /**
3
3
  * Represents a reusable component definition.
4
+ * @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
4
5
  */
5
6
  class VComponent {
6
7
  /**
@@ -38,6 +39,7 @@ class VComponent {
38
39
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
39
40
  /**
40
41
  * A registry for managing component definitions.
42
+ * @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
41
43
  */
42
44
  class VComponentRegistry {
43
45
  /**
@@ -6729,11 +6731,18 @@ class ExpressionUtils {
6729
6731
  * Extracts variable and function names used in the expression.
6730
6732
  * @param expression The expression string to analyze.
6731
6733
  * @param functionDependencies A dictionary mapping function names to their dependencies.
6734
+ * @param options Optional parsing options.
6735
+ * - asScript: If true, parse the input as a Script (allows multi-statement source with semicolons,
6736
+ * declarations, control-flow, etc.). If false/omitted, parse as a single expression (the default,
6737
+ * for backward compatibility with interpolation and binding directives).
6732
6738
  * @returns An array of identifier names.
6733
6739
  */
6734
- static extractIdentifiers(expression, functionDependencies) {
6740
+ static extractIdentifiers(expression, functionDependencies, options) {
6735
6741
  const identifiers = new Set();
6736
- const ast = parse(`(${expression})`, { ecmaVersion: "latest" });
6742
+ // In expression mode we wrap in parens so acorn parses the source as a single expression.
6743
+ // In script mode we parse as a Program so that multi-statement bodies (e.g. "a=1; b=2") work.
6744
+ const source = options?.asScript ? expression : `(${expression})`;
6745
+ const ast = parse(source, { ecmaVersion: "latest" });
6737
6746
  // Use walk.full instead of walk.simple to visit ALL nodes including assignment LHS
6738
6747
  full(ast, (node) => {
6739
6748
  if (node.type === 'Identifier') {
@@ -6965,10 +6974,10 @@ class ExpressionUtils {
6965
6974
  * @param identifiers The list of identifiers that are available in bindings.
6966
6975
  * @returns The rewritten expression.
6967
6976
  */
6968
- static rewriteExpression(expression, identifiers) {
6977
+ static rewriteExpression(expression, identifiers, options) {
6969
6978
  // Reserved words and built-in objects that should not be prefixed with 'this.'
6970
6979
  const reserved = new Set([
6971
- 'event', '$ctx', '$newValue',
6980
+ 'event', '$event', '$ctx', '$newValue',
6972
6981
  'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
6973
6982
  'Math', 'Date', 'String', 'Number', 'Boolean', 'Array', 'Object',
6974
6983
  'JSON', 'console', 'window', 'document', 'navigator',
@@ -6980,7 +6989,7 @@ class ExpressionUtils {
6980
6989
  // identifiers that are used (right-hand side), not assigned to (left-hand side)
6981
6990
  let allIdentifiersInExpression;
6982
6991
  try {
6983
- allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {});
6992
+ allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {}, options);
6984
6993
  }
6985
6994
  catch (error) {
6986
6995
  console.warn('[ichigo.js] Failed to extract identifiers from expression:', expression, error);
@@ -7002,7 +7011,26 @@ class ExpressionUtils {
7002
7011
  try {
7003
7012
  // Build a map of positions to replace: { start: number, end: number, name: string }[]
7004
7013
  const replacements = [];
7005
- const parsedAst = parse(`(${expression})`, { ecmaVersion: 'latest' });
7014
+ // In script mode we must not wrap in parens (that would make multi-statement input invalid).
7015
+ // Offsets from the parser therefore refer directly to the original expression, so no shift.
7016
+ const asScript = options?.asScript === true;
7017
+ const source = asScript ? expression : `(${expression})`;
7018
+ const offsetShift = asScript ? 0 : 1;
7019
+ const parsedAst = parse(source, { ecmaVersion: 'latest' });
7020
+ // Track identifiers that are locally declared within the handler body (let/const/var, function
7021
+ // params) so we don't rewrite them to `this.xxx`. Only relevant in script mode, where the user
7022
+ // can write declarations; in expression mode there are no declarations to track.
7023
+ const locallyDeclared = new Set();
7024
+ if (asScript) {
7025
+ full(parsedAst, (node) => {
7026
+ if (node.type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
7027
+ locallyDeclared.add(node.id.name);
7028
+ }
7029
+ else if (node.type === 'FunctionDeclaration' && node.id?.type === 'Identifier') {
7030
+ locallyDeclared.add(node.id.name);
7031
+ }
7032
+ });
7033
+ }
7006
7034
  // Collect all identifier nodes that should be replaced
7007
7035
  // Use walk.fullAncestor to visit ALL nodes (including assignment LHS) while tracking ancestors
7008
7036
  fullAncestor(parsedAst, (node, _state, ancestors) => {
@@ -7013,6 +7041,10 @@ class ExpressionUtils {
7013
7041
  if (!bindingIdentifiers.has(node.name)) {
7014
7042
  return;
7015
7043
  }
7044
+ // Skip identifiers that were declared locally in the handler body
7045
+ if (locallyDeclared.has(node.name)) {
7046
+ return;
7047
+ }
7016
7048
  // Check if this identifier is a property of a MemberExpression
7017
7049
  // (e.g., in 'obj.prop', we should skip 'prop')
7018
7050
  if (ancestors.length >= 1) {
@@ -7024,10 +7056,10 @@ class ExpressionUtils {
7024
7056
  }
7025
7057
  }
7026
7058
  }
7027
- // Add to replacements list (adjust for the wrapping parentheses)
7059
+ // Add to replacements list (adjust for the wrapping parentheses in expression mode)
7028
7060
  replacements.push({
7029
- start: node.start - 1,
7030
- end: node.end - 1,
7061
+ start: node.start - offsetShift,
7062
+ end: node.end - offsetShift,
7031
7063
  name: node.name
7032
7064
  });
7033
7065
  });
@@ -7468,6 +7500,14 @@ class VBindDirective {
7468
7500
  else if (attributeName === 'style') {
7469
7501
  this.#updateStyle(element, value);
7470
7502
  }
7503
+ else if (this.#isCustomElementProperty(element, attributeName, value)) {
7504
+ // Custom elements: set objects/arrays (and declared props) as properties
7505
+ // so complex values are not serialized to strings via setAttribute.
7506
+ // HTML attributes are lowercased by the browser, so resolve to the
7507
+ // actual camelCase property name declared on the custom element.
7508
+ const propName = this.#resolveCustomElementPropertyName(element, attributeName);
7509
+ element[propName] = value;
7510
+ }
7471
7511
  else if (this.#isDOMProperty(attributeName)) {
7472
7512
  this.#updateProperty(element, attributeName, value);
7473
7513
  }
@@ -7551,6 +7591,46 @@ class VBindDirective {
7551
7591
  #camelToKebab(str) {
7552
7592
  return str.replace(/([A-Z])/g, '-$1').toLowerCase();
7553
7593
  }
7594
+ /**
7595
+ * Returns true when the target is a custom element and the binding should be
7596
+ * delivered as a property rather than an HTML attribute.
7597
+ *
7598
+ * Two conditions trigger property delivery:
7599
+ * 1. The value is an object or array — serialising these to a string attribute
7600
+ * would lose type information.
7601
+ * 2. The element exposes a matching property accessor (e.g. a prop declared via
7602
+ * defineComponent), even when the value is a primitive.
7603
+ */
7604
+ #isCustomElementProperty(element, name, value) {
7605
+ if (!element.tagName.includes('-')) {
7606
+ return false;
7607
+ }
7608
+ const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
7609
+ return isObjectOrArray || name in element;
7610
+ }
7611
+ /**
7612
+ * Resolves the actual property name on a custom element for a given
7613
+ * (lowercased) HTML attribute name. HTML attributes are always lowercase,
7614
+ * but custom element props are typically camelCase. This method checks the
7615
+ * element's declared _props (set by defineComponent) for a case-insensitive
7616
+ * match, falling back to scanning the prototype's own property descriptors.
7617
+ */
7618
+ #resolveCustomElementPropertyName(element, name) {
7619
+ // Fast path: exact match already exists
7620
+ if (name in element) {
7621
+ return name;
7622
+ }
7623
+ // Check declared _props from defineComponent / IchigoElement
7624
+ const props = element.constructor._props;
7625
+ if (Array.isArray(props)) {
7626
+ const lowerName = name.toLowerCase();
7627
+ const match = props.find(p => p.toLowerCase() === lowerName);
7628
+ if (match) {
7629
+ return match;
7630
+ }
7631
+ }
7632
+ return name;
7633
+ }
7554
7634
  /**
7555
7635
  * Checks if the attribute should be set as a DOM property.
7556
7636
  */
@@ -8931,6 +9011,18 @@ class VNode {
8931
9011
  });
8932
9012
  }
8933
9013
  }
9014
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
9015
+ // If the node is a DocumentFragment, create child VNodes for its children
9016
+ // DocumentFragment is not an Element so it has no directives itself.
9017
+ this.#childVNodes = [];
9018
+ for (const childNode of Array.from(this.#node.childNodes)) {
9019
+ new VNode({
9020
+ node: childNode,
9021
+ vApplication: this.#vApplication,
9022
+ parentVNode: this
9023
+ });
9024
+ }
9025
+ }
8934
9026
  // Register this node as a dependent of the parent node, if any
8935
9027
  this.#closers = this.#parentVNode?.addDependent(this);
8936
9028
  }
@@ -9185,6 +9277,16 @@ class VNode {
9185
9277
  });
9186
9278
  }
9187
9279
  }
9280
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
9281
+ // For document fragments (e.g. from <template v-if>), propagate updates
9282
+ // to dependent virtual nodes so that child bindings stay reactive.
9283
+ this.#dependents?.forEach(dependentNode => {
9284
+ const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
9285
+ if (changed) {
9286
+ dependentNode.update();
9287
+ }
9288
+ });
9289
+ }
9188
9290
  }
9189
9291
  /**
9190
9292
  * Forces an update of the virtual node and its children, regardless of changed identifiers.
@@ -9234,6 +9336,12 @@ class VNode {
9234
9336
  });
9235
9337
  }
9236
9338
  }
9339
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
9340
+ // For document fragments, recursively force update child VNodes
9341
+ this.#childVNodes?.forEach(childVNode => {
9342
+ childVNode.forceUpdate();
9343
+ });
9344
+ }
9237
9345
  }
9238
9346
  /**
9239
9347
  * Adds a child virtual node to this virtual node.
@@ -9611,9 +9719,11 @@ class VConditionalDirective {
9611
9719
  }
9612
9720
  // Clone the original node and create a new VNode for it
9613
9721
  const clone = this.#cloneNode();
9614
- // Insert the cloned node after the anchor node, or as a child of the parent if no anchor
9615
- this.#vNode.anchorNode?.parentNode?.insertBefore(clone, this.#vNode.anchorNode.nextSibling);
9616
- // Create a new VNode for the cloned element
9722
+ // Create a new VNode for the cloned element BEFORE inserting into DOM.
9723
+ // This prevents custom elements (Web Components) from having their
9724
+ // connectedCallback fire before VNode processes their children, which
9725
+ // would cause the parent VApplication to incorrectly adopt the custom
9726
+ // element's internal template content as its own VNode tree.
9617
9727
  // Pass the current bindings to ensure loop variables from v-for are available
9618
9728
  const vNode = new VNode({
9619
9729
  node: clone,
@@ -9621,6 +9731,29 @@ class VConditionalDirective {
9621
9731
  parentVNode: this.#vNode,
9622
9732
  bindings: this.#vNode.bindings
9623
9733
  });
9734
+ // Insert after the anchor node AFTER VNode creation
9735
+ const anchorParent = this.#vNode.anchorNode?.parentNode;
9736
+ const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
9737
+ if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE && anchorParent) {
9738
+ const startMarker = document.createComment('#vif-fragment-start');
9739
+ const endMarker = document.createComment('#vif-fragment-end');
9740
+ if (nextSibling) {
9741
+ anchorParent.insertBefore(startMarker, nextSibling);
9742
+ }
9743
+ else {
9744
+ anchorParent.appendChild(startMarker);
9745
+ }
9746
+ anchorParent.insertBefore(endMarker, startMarker.nextSibling);
9747
+ anchorParent.insertBefore(clone, endMarker);
9748
+ // Store markers for later removal
9749
+ vNode.userData.set('vif_fragment_start', startMarker);
9750
+ vNode.userData.set('vif_fragment_end', endMarker);
9751
+ this.#renderedVNode = vNode;
9752
+ this.#renderedVNode.forceUpdate();
9753
+ return;
9754
+ }
9755
+ const nodeToInsert = vNode.anchorNode || clone;
9756
+ anchorParent?.insertBefore(nodeToInsert, nextSibling);
9624
9757
  this.#renderedVNode = vNode;
9625
9758
  this.#renderedVNode.forceUpdate();
9626
9759
  }
@@ -9635,17 +9768,40 @@ class VConditionalDirective {
9635
9768
  }
9636
9769
  // Destroy VNode first (calls @unmount hooks while DOM is still accessible)
9637
9770
  this.#renderedVNode.destroy();
9638
- // Then remove from DOM
9639
- this.#renderedVNode.node.parentNode?.removeChild(this.#renderedVNode.node);
9771
+ // Then remove from DOM. Handle fragment markers if present
9772
+ const startMarker = this.#renderedVNode.userData.get?.('vif_fragment_start');
9773
+ const endMarker = this.#renderedVNode.userData.get?.('vif_fragment_end');
9774
+ if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
9775
+ const parentNode = startMarker.parentNode;
9776
+ let node = startMarker;
9777
+ while (node) {
9778
+ const next = node.nextSibling;
9779
+ parentNode.removeChild(node);
9780
+ if (node === endMarker)
9781
+ break;
9782
+ node = next;
9783
+ }
9784
+ this.#renderedVNode = undefined;
9785
+ return;
9786
+ }
9787
+ const parent = this.#renderedVNode.node.parentNode;
9788
+ if (parent) {
9789
+ parent.removeChild(this.#renderedVNode.node);
9790
+ }
9640
9791
  this.#renderedVNode = undefined;
9641
9792
  }
9642
9793
  /**
9643
9794
  * Clones the original node of the directive's virtual node.
9644
- * This is used to create a new instance of the node for rendering.
9645
- * @returns The cloned HTMLElement.
9795
+ * When the source element is a <template>, returns a DocumentFragment
9796
+ * cloned from the template's content.
9797
+ * @returns The cloned Node (HTMLElement or DocumentFragment).
9646
9798
  */
9647
9799
  #cloneNode() {
9648
9800
  const element = this.#vNode.node;
9801
+ if (element instanceof HTMLTemplateElement) {
9802
+ // Return a DocumentFragment cloned from the template content
9803
+ return element.content.cloneNode(true);
9804
+ }
9649
9805
  return element.cloneNode(true);
9650
9806
  }
9651
9807
  /**
@@ -9963,10 +10119,28 @@ class VForDirective {
9963
10119
  vNode.destroy();
9964
10120
  }
9965
10121
  }
9966
- // Then remove from DOM
10122
+ // Then remove from DOM. Handle both Element nodes and fragment-marked ranges.
9967
10123
  for (const vNode of nodesToRemove) {
9968
- if (vNode.node.parentNode) {
9969
- vNode.node.parentNode.removeChild(vNode.node);
10124
+ // If this VNode stored fragment markers, remove the range between them
10125
+ const startMarker = vNode.userData.get?.('vfor_fragment_start');
10126
+ const endMarker = vNode.userData.get?.('vfor_fragment_end');
10127
+ if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
10128
+ const parentNode = startMarker.parentNode;
10129
+ let node = startMarker;
10130
+ // Remove nodes from startMarker up to and including endMarker
10131
+ while (node) {
10132
+ const next = node.nextSibling;
10133
+ parentNode.removeChild(node);
10134
+ if (node === endMarker)
10135
+ break;
10136
+ node = next;
10137
+ }
10138
+ continue;
10139
+ }
10140
+ // Fallback: remove the node itself if it's attached
10141
+ const parentOfNode = vNode.node.parentNode;
10142
+ if (parentOfNode) {
10143
+ parentOfNode.removeChild(vNode.node);
9970
10144
  }
9971
10145
  }
9972
10146
  // Add or reorder items
@@ -10000,6 +10174,28 @@ class VForDirective {
10000
10174
  bindings,
10001
10175
  dependentIdentifiers: depIds,
10002
10176
  });
10177
+ // If clone is a DocumentFragment, insert it between start/end comment markers
10178
+ if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
10179
+ const startMarker = document.createComment('#vfor-fragment-start');
10180
+ const endMarker = document.createComment('#vfor-fragment-end');
10181
+ // Insert start and end markers and the fragment's children between them
10182
+ if (prevNode.nextSibling) {
10183
+ parent.insertBefore(startMarker, prevNode.nextSibling);
10184
+ }
10185
+ else {
10186
+ parent.appendChild(startMarker);
10187
+ }
10188
+ parent.insertBefore(endMarker, startMarker.nextSibling);
10189
+ parent.insertBefore(clone, endMarker);
10190
+ // Store markers on the VNode for later removal/movement
10191
+ vNode.userData.set('vfor_fragment_start', startMarker);
10192
+ vNode.userData.set('vfor_fragment_end', endMarker);
10193
+ newRenderedItems.set(key, vNode);
10194
+ vNode.forceUpdate();
10195
+ // Use endMarker as prevNode for subsequent insertions
10196
+ prevNode = endMarker;
10197
+ continue;
10198
+ }
10003
10199
  // Determine what to insert: anchor node (if exists) or the clone itself
10004
10200
  const nodeToInsert = vNode.anchorNode || clone;
10005
10201
  // Insert after previous node
@@ -10017,8 +10213,9 @@ class VForDirective {
10017
10213
  newRenderedItems.set(key, vNode);
10018
10214
  // Update bindings
10019
10215
  this.#updateItemBindings(vNode, context);
10020
- // Determine the actual node in DOM: anchor node (if exists) or vNode.node
10021
- const actualNode = vNode.anchorNode || vNode.node;
10216
+ // Determine the actual node in DOM: prefer fragment end marker, then anchor node, then vNode.node
10217
+ const fragmentEnd = vNode.userData.get?.('vfor_fragment_end');
10218
+ const actualNode = fragmentEnd || vNode.anchorNode || vNode.node;
10022
10219
  // Move to correct position if needed
10023
10220
  if (prevNode.nextSibling !== actualNode) {
10024
10221
  if (prevNode.nextSibling) {
@@ -10029,8 +10226,9 @@ class VForDirective {
10029
10226
  }
10030
10227
  }
10031
10228
  }
10032
- // Use anchor node as prevNode if it exists, otherwise use vNode.node
10033
- prevNode = vNode.anchorNode || vNode.node;
10229
+ // Use fragment end marker > anchor node > vNode.node as prevNode
10230
+ const fragmentEndForPrev = vNode.userData.get?.('vfor_fragment_end');
10231
+ prevNode = fragmentEndForPrev || vNode.anchorNode || vNode.node;
10034
10232
  }
10035
10233
  // Update rendered items map
10036
10234
  this.#renderedItems = newRenderedItems;
@@ -10209,12 +10407,17 @@ class VForDirective {
10209
10407
  }
10210
10408
  /**
10211
10409
  * Clones the original node of the directive's virtual node.
10212
- * This is used to create a new instance of the node for rendering.
10213
- * @returns The cloned HTMLElement.
10410
+ * When the source element is a <template>, its children (stored in .content)
10411
+ * are cloned into a DocumentFragment so that multiple root nodes can be
10412
+ * managed without adding an extra wrapper element.
10413
+ * @returns The cloned Node (Element or DocumentFragment).
10214
10414
  */
10215
10415
  #cloneNode() {
10216
- // Clone the original element
10217
10416
  const element = this.#vNode.node;
10417
+ if (element instanceof HTMLTemplateElement) {
10418
+ // Return a cloned DocumentFragment containing the template content
10419
+ return element.content.cloneNode(true);
10420
+ }
10218
10421
  return element.cloneNode(true);
10219
10422
  }
10220
10423
  /**
@@ -11055,10 +11258,12 @@ class VOnDirective {
11055
11258
  this.#eventName = parts[0];
11056
11259
  parts.slice(1).forEach(mod => this.#modifiers.add(mod));
11057
11260
  }
11058
- // Parse the expression to extract identifiers and create the handler wrapper
11261
+ // Parse the expression to extract identifiers and create the handler wrapper.
11262
+ // Event handlers are parsed in script mode so that users can write multi-statement bodies
11263
+ // (e.g. "a=1; b=2"), declarations, and control-flow constructs — matching Vue semantics.
11059
11264
  const expression = context.attribute.value;
11060
11265
  if (expression) {
11061
- this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
11266
+ this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies, { asScript: true });
11062
11267
  }
11063
11268
  // Check if this is a lifecycle hook or a regular event
11064
11269
  if (this.#eventName && this.#isLifecycleHook(this.#eventName)) {
@@ -11272,10 +11477,11 @@ class VOnDirective {
11272
11477
  // This allows the method to access the DOM element, VNode, and userData
11273
11478
  return originalMethod($ctx);
11274
11479
  }
11275
- // For inline expressions, rewrite to use 'this' context
11276
- // This allows assignments like "currentTab = 'shop'" to work correctly
11277
- const rewrittenExpr = this.#rewriteExpression(expression, identifiers);
11278
- const funcBody = `return (${rewrittenExpr});`;
11480
+ // For inline bodies, rewrite to use 'this' context
11481
+ // This allows assignments like "currentTab = 'shop'" to work correctly.
11482
+ // Script mode allows multi-statement bodies (e.g. "a=1; init()") and control-flow.
11483
+ const rewrittenExpr = this.#rewriteExpression(expression, identifiers, { asScript: true });
11484
+ const funcBody = rewrittenExpr;
11279
11485
  const func = new Function('$ctx', funcBody);
11280
11486
  return func.call(bindings?.raw, $ctx);
11281
11487
  };
@@ -11305,12 +11511,15 @@ class VOnDirective {
11305
11511
  // Pass event as first argument and $ctx as second argument
11306
11512
  return originalMethod(event, $ctx);
11307
11513
  }
11308
- // For inline expressions, rewrite to use 'this' context
11309
- // This allows assignments like "currentTab = 'shop'" to work correctly
11310
- const rewrittenExpr = this.#rewriteExpression(expression, identifiers);
11311
- const funcBody = `return (${rewrittenExpr});`;
11312
- const func = new Function('event', '$ctx', funcBody);
11313
- return func.call(bindings?.raw, event, $ctx);
11514
+ // For inline bodies, rewrite to use 'this' context
11515
+ // This allows assignments like "currentTab = 'shop'" to work correctly.
11516
+ // Script mode allows multi-statement bodies (e.g. "a=1; b=2") and control-flow,
11517
+ // so we emit the rewritten source directly as the function body (no `return (...)`).
11518
+ const rewrittenExpr = this.#rewriteExpression(expression, identifiers, { asScript: true });
11519
+ const funcBody = rewrittenExpr;
11520
+ // '$event' is an alias for 'event' for Vue compatibility
11521
+ const func = new Function('event', '$event', '$ctx', funcBody);
11522
+ return func.call(bindings?.raw, event, event, $ctx);
11314
11523
  };
11315
11524
  }
11316
11525
  /**
@@ -11321,8 +11530,8 @@ class VOnDirective {
11321
11530
  * @param identifiers The list of identifiers that are available in bindings.
11322
11531
  * @returns The rewritten expression.
11323
11532
  */
11324
- #rewriteExpression(expression, identifiers) {
11325
- return ExpressionUtils.rewriteExpression(expression, identifiers);
11533
+ #rewriteExpression(expression, identifiers, options) {
11534
+ return ExpressionUtils.rewriteExpression(expression, identifiers, options);
11326
11535
  }
11327
11536
  }
11328
11537
 
@@ -13020,6 +13229,7 @@ class VDOM {
13020
13229
  /**
13021
13230
  * Gets the component registry.
13022
13231
  * @return {VComponentRegistry} The component registry.
13232
+ * @deprecated This method is deprecated and will be removed in a future release. Please use the new component registration system instead.
13023
13233
  */
13024
13234
  static get componentRegistry() {
13025
13235
  return this.#componentRegistry;
@@ -13063,5 +13273,260 @@ class VDOM {
13063
13273
  }
13064
13274
  }
13065
13275
 
13066
- export { ExpressionUtils, ReactiveProxy, VComponent, VComponentRegistry, VDOM };
13276
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
13277
+ /**
13278
+ * Base class for ichigo.js-backed Web Components (Light DOM, no Shadow DOM).
13279
+ *
13280
+ * Mount timing:
13281
+ * - If the component declares no props, the VApplication is mounted synchronously
13282
+ * at the end of connectedCallback (the template is known, no props to wait for).
13283
+ * - If the component declares props, the VApplication is mounted the first time
13284
+ * _setProp() is called after connectedCallback has prepared the DOM. This
13285
+ * guarantees that the parent framework (e.g. ichigo.js VBindDirective) has
13286
+ * already delivered at least one prop value before data() is evaluated.
13287
+ *
13288
+ * Subclasses must set the static fields _template, _props and _buildOptions before
13289
+ * calling customElements.define(). defineComponent() handles this automatically.
13290
+ */
13291
+ class IchigoElement extends HTMLElement {
13292
+ /**
13293
+ * The mounted VApplication instance, present only while connected to the DOM.
13294
+ */
13295
+ #app;
13296
+ /**
13297
+ * Stores prop values received at any time (before or after mount).
13298
+ */
13299
+ #propValues = {};
13300
+ /**
13301
+ * The root element cloned from the template, ready for mounting.
13302
+ * Set by connectedCallback; cleared on disconnect.
13303
+ */
13304
+ #mountRoot;
13305
+ /**
13306
+ * Whether a mount microtask is already queued to avoid double-mounting.
13307
+ */
13308
+ #mountScheduled = false;
13309
+ connectedCallback() {
13310
+ // --- 0. Guard: skip if template not yet loaded ---
13311
+ // customElements.define() may run before loadComponent() completes (e.g. when
13312
+ // dynamic imports are inlined by the bundler). The static placeholder element
13313
+ // in the HTML will be removed and replaced by v-for once the template is ready,
13314
+ // so it is safe to do nothing here.
13315
+ const ctor = this.constructor;
13316
+ const templateEl = document.querySelector(ctor._template);
13317
+ if (!templateEl || !(templateEl instanceof HTMLTemplateElement)) {
13318
+ return;
13319
+ }
13320
+ // --- 1. Capture slot content before clearing children ---
13321
+ const defaultSlotNodes = [];
13322
+ const namedSlotNodes = new Map();
13323
+ for (const child of Array.from(this.childNodes)) {
13324
+ if (child.nodeType === Node.ELEMENT_NODE) {
13325
+ const el = child;
13326
+ const slotName = el.getAttribute('slot');
13327
+ if (slotName) {
13328
+ if (!namedSlotNodes.has(slotName)) {
13329
+ namedSlotNodes.set(slotName, []);
13330
+ }
13331
+ namedSlotNodes.get(slotName).push(el);
13332
+ // Remove slot attribute so ichigo.js doesn't try to bind it
13333
+ el.removeAttribute('slot');
13334
+ }
13335
+ else {
13336
+ defaultSlotNodes.push(el);
13337
+ }
13338
+ }
13339
+ else if (child.nodeType === Node.TEXT_NODE) {
13340
+ if ((child.textContent ?? '').trim()) {
13341
+ defaultSlotNodes.push(child);
13342
+ }
13343
+ }
13344
+ }
13345
+ // Clear host element so we can append the cloned template
13346
+ while (this.firstChild) {
13347
+ this.removeChild(this.firstChild);
13348
+ }
13349
+ // --- 2. Clone the component template ---
13350
+ const fragment = templateEl.content.cloneNode(true);
13351
+ const root = this.#findRootElement(fragment);
13352
+ // --- 3. Distribute named slot content ---
13353
+ for (const [name, nodes] of namedSlotNodes) {
13354
+ const slot = root.querySelector(`slot[name="${name}"]`);
13355
+ if (slot) {
13356
+ slot.replaceWith(...nodes);
13357
+ }
13358
+ }
13359
+ // --- 4. Distribute default slot content ---
13360
+ const defaultSlot = root.querySelector('slot:not([name])');
13361
+ if (defaultSlot && defaultSlotNodes.length > 0) {
13362
+ defaultSlot.replaceWith(...defaultSlotNodes);
13363
+ }
13364
+ // Attach the populated template to the host element
13365
+ this.appendChild(root);
13366
+ this.#mountRoot = root;
13367
+ // If props were set before connectedCallback, ensure mount is scheduled
13368
+ this.#scheduleMountIfNeeded();
13369
+ // --- 5. Mount strategy ---
13370
+ // If this component has no declared props, mount immediately.
13371
+ // If it has props, mount will be triggered from _setProp() once the parent
13372
+ // delivers the first prop value (via VBindDirective / forceUpdate).
13373
+ if (ctor._props.length === 0) {
13374
+ this.#doMount();
13375
+ }
13376
+ // else: wait for _setProp() to trigger #scheduleMountIfNeeded()
13377
+ }
13378
+ disconnectedCallback() {
13379
+ this.#mountRoot = undefined;
13380
+ this.#mountScheduled = false;
13381
+ if (this.#app) {
13382
+ this.#app.unmount();
13383
+ this.#app = undefined;
13384
+ }
13385
+ }
13386
+ /**
13387
+ * Called by the property setters generated by defineComponent().
13388
+ * Before mount: stores the value and schedules a mount microtask.
13389
+ * After mount: pushes the value directly into the reactive bindings.
13390
+ */
13391
+ _setProp(name, value) {
13392
+ this.#propValues[name] = value;
13393
+ if (this.#app) {
13394
+ this.#app.bindings?.set(name, value);
13395
+ }
13396
+ else {
13397
+ this.#scheduleMountIfNeeded();
13398
+ }
13399
+ }
13400
+ /**
13401
+ * Called by the property getters generated by defineComponent().
13402
+ */
13403
+ _getProp(name) {
13404
+ return this.#propValues[name];
13405
+ }
13406
+ // --- Static fields set by defineComponent() ---
13407
+ /**
13408
+ * CSS selector for the component's <template> element (e.g. '#my-card').
13409
+ */
13410
+ static _template;
13411
+ /**
13412
+ * List of declared prop names. Used to decide whether to defer mounting.
13413
+ */
13414
+ static _props = [];
13415
+ /**
13416
+ * Factory that builds VApplicationOptions from the current prop values.
13417
+ * Implemented by defineComponent() as a closure that captures the user's options.
13418
+ */
13419
+ static _buildOptions;
13420
+ // --- Private helpers ---
13421
+ /**
13422
+ * Schedules a mount microtask if the DOM root is ready and no mount is pending.
13423
+ * Called from _setProp() so the mount happens after the prop value is stored.
13424
+ */
13425
+ #scheduleMountIfNeeded() {
13426
+ if (this.#mountScheduled || this.#app || !this.#mountRoot) {
13427
+ return;
13428
+ }
13429
+ this.#mountScheduled = true;
13430
+ queueMicrotask(() => {
13431
+ this.#mountScheduled = false;
13432
+ this.#doMount();
13433
+ });
13434
+ }
13435
+ #doMount() {
13436
+ if (this.#app || !this.#mountRoot) {
13437
+ return;
13438
+ }
13439
+ const ctor = this.constructor;
13440
+ const options = ctor._buildOptions(this.#propValues);
13441
+ this.#app = VDOM.createApp(options);
13442
+ this.#app.mount(this.#mountRoot);
13443
+ }
13444
+ #findRootElement(fragment) {
13445
+ for (const node of Array.from(fragment.childNodes)) {
13446
+ if (node.nodeType === Node.ELEMENT_NODE) {
13447
+ return node;
13448
+ }
13449
+ }
13450
+ throw new Error(`IchigoElement: no root element found in template '${this.constructor._template}'`);
13451
+ }
13452
+ }
13453
+
13454
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
13455
+ /**
13456
+ * Defines and registers a custom element backed by ichigo.js reactivity.
13457
+ *
13458
+ * Usage:
13459
+ * ```html
13460
+ * <template id="my-list">
13461
+ * <div>
13462
+ * <ul v-if="items.length > 0">
13463
+ * <li v-for="item of items">{{item.name}}</li>
13464
+ * </ul>
13465
+ * <slot></slot>
13466
+ * </div>
13467
+ * </template>
13468
+ * ```
13469
+ * ```typescript
13470
+ * defineComponent('my-list', {
13471
+ * template: '#my-list',
13472
+ * props: ['items'],
13473
+ * data() {
13474
+ * return { items: this.items ?? [] };
13475
+ * },
13476
+ * });
13477
+ * ```
13478
+ * ```html
13479
+ * <my-list :items="searchResults">
13480
+ * <span slot="empty">No results.</span>
13481
+ * </my-list>
13482
+ * ```
13483
+ *
13484
+ * @param tagName Custom element tag name (must contain a hyphen, e.g. 'my-card').
13485
+ * @param options Component options including template selector and optional props.
13486
+ */
13487
+ function defineComponent(tagName, options) {
13488
+ const { props = [], template, data, computed, methods, watch, logLevel } = options;
13489
+ // Build a subclass of IchigoElement specific to this component
13490
+ class ComponentElement extends IchigoElement {
13491
+ static _template = template;
13492
+ static _props = props;
13493
+ static _buildOptions(propValues) {
13494
+ return {
13495
+ data() {
13496
+ // 'this' is the $ctx object provided by VApplication ({ $markRaw }).
13497
+ // We extend it with prop values so the user's data() can reference them
13498
+ // via 'this.propName' and supply defaults (e.g. `this.items ?? []`).
13499
+ const ctx = { $markRaw: ReactiveProxy.markRaw.bind(ReactiveProxy), ...propValues };
13500
+ const userData = data
13501
+ ? data.call(ctx)
13502
+ : {};
13503
+ // Props are always included in data so they are reactive from the start.
13504
+ // User-returned values take precedence (allow transforming/defaulting props).
13505
+ return { ...propValues, ...userData };
13506
+ },
13507
+ computed,
13508
+ methods,
13509
+ watch,
13510
+ logLevel,
13511
+ };
13512
+ }
13513
+ }
13514
+ // Generate a property getter/setter for each declared prop.
13515
+ // This enables the parent VApplication to push updates via `element.propName = value`.
13516
+ for (const prop of props) {
13517
+ Object.defineProperty(ComponentElement.prototype, prop, {
13518
+ get() {
13519
+ return this._getProp(prop);
13520
+ },
13521
+ set(value) {
13522
+ this._setProp(prop, value);
13523
+ },
13524
+ configurable: true,
13525
+ enumerable: true,
13526
+ });
13527
+ }
13528
+ customElements.define(tagName, ComponentElement);
13529
+ }
13530
+
13531
+ export { ExpressionUtils, IchigoElement, ReactiveProxy, VComponent, VComponentRegistry, VDOM, defineComponent };
13067
13532
  //# sourceMappingURL=ichigo.esm.js.map