@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.
package/dist/ichigo.cjs CHANGED
@@ -7,6 +7,7 @@
7
7
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
8
8
  /**
9
9
  * Represents a reusable component definition.
10
+ * @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
10
11
  */
11
12
  class VComponent {
12
13
  /**
@@ -44,6 +45,7 @@
44
45
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
45
46
  /**
46
47
  * A registry for managing component definitions.
48
+ * @deprecated This class is deprecated and will be removed in a future release. Please use the new component registration system instead.
47
49
  */
48
50
  class VComponentRegistry {
49
51
  /**
@@ -6735,11 +6737,18 @@
6735
6737
  * Extracts variable and function names used in the expression.
6736
6738
  * @param expression The expression string to analyze.
6737
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).
6738
6744
  * @returns An array of identifier names.
6739
6745
  */
6740
- static extractIdentifiers(expression, functionDependencies) {
6746
+ static extractIdentifiers(expression, functionDependencies, options) {
6741
6747
  const identifiers = new Set();
6742
- 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" });
6743
6752
  // Use walk.full instead of walk.simple to visit ALL nodes including assignment LHS
6744
6753
  full(ast, (node) => {
6745
6754
  if (node.type === 'Identifier') {
@@ -6971,10 +6980,10 @@
6971
6980
  * @param identifiers The list of identifiers that are available in bindings.
6972
6981
  * @returns The rewritten expression.
6973
6982
  */
6974
- static rewriteExpression(expression, identifiers) {
6983
+ static rewriteExpression(expression, identifiers, options) {
6975
6984
  // Reserved words and built-in objects that should not be prefixed with 'this.'
6976
6985
  const reserved = new Set([
6977
- 'event', '$ctx', '$newValue',
6986
+ 'event', '$event', '$ctx', '$newValue',
6978
6987
  'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
6979
6988
  'Math', 'Date', 'String', 'Number', 'Boolean', 'Array', 'Object',
6980
6989
  'JSON', 'console', 'window', 'document', 'navigator',
@@ -6986,7 +6995,7 @@
6986
6995
  // identifiers that are used (right-hand side), not assigned to (left-hand side)
6987
6996
  let allIdentifiersInExpression;
6988
6997
  try {
6989
- allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {});
6998
+ allIdentifiersInExpression = ExpressionUtils.extractIdentifiers(expression, {}, options);
6990
6999
  }
6991
7000
  catch (error) {
6992
7001
  console.warn('[ichigo.js] Failed to extract identifiers from expression:', expression, error);
@@ -7008,7 +7017,26 @@
7008
7017
  try {
7009
7018
  // Build a map of positions to replace: { start: number, end: number, name: string }[]
7010
7019
  const replacements = [];
7011
- 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
+ }
7012
7040
  // Collect all identifier nodes that should be replaced
7013
7041
  // Use walk.fullAncestor to visit ALL nodes (including assignment LHS) while tracking ancestors
7014
7042
  fullAncestor(parsedAst, (node, _state, ancestors) => {
@@ -7019,6 +7047,10 @@
7019
7047
  if (!bindingIdentifiers.has(node.name)) {
7020
7048
  return;
7021
7049
  }
7050
+ // Skip identifiers that were declared locally in the handler body
7051
+ if (locallyDeclared.has(node.name)) {
7052
+ return;
7053
+ }
7022
7054
  // Check if this identifier is a property of a MemberExpression
7023
7055
  // (e.g., in 'obj.prop', we should skip 'prop')
7024
7056
  if (ancestors.length >= 1) {
@@ -7030,10 +7062,10 @@
7030
7062
  }
7031
7063
  }
7032
7064
  }
7033
- // Add to replacements list (adjust for the wrapping parentheses)
7065
+ // Add to replacements list (adjust for the wrapping parentheses in expression mode)
7034
7066
  replacements.push({
7035
- start: node.start - 1,
7036
- end: node.end - 1,
7067
+ start: node.start - offsetShift,
7068
+ end: node.end - offsetShift,
7037
7069
  name: node.name
7038
7070
  });
7039
7071
  });
@@ -7474,6 +7506,14 @@
7474
7506
  else if (attributeName === 'style') {
7475
7507
  this.#updateStyle(element, value);
7476
7508
  }
7509
+ else if (this.#isCustomElementProperty(element, attributeName, value)) {
7510
+ // Custom elements: set objects/arrays (and declared props) as properties
7511
+ // so complex values are not serialized to strings via setAttribute.
7512
+ // HTML attributes are lowercased by the browser, so resolve to the
7513
+ // actual camelCase property name declared on the custom element.
7514
+ const propName = this.#resolveCustomElementPropertyName(element, attributeName);
7515
+ element[propName] = value;
7516
+ }
7477
7517
  else if (this.#isDOMProperty(attributeName)) {
7478
7518
  this.#updateProperty(element, attributeName, value);
7479
7519
  }
@@ -7557,6 +7597,46 @@
7557
7597
  #camelToKebab(str) {
7558
7598
  return str.replace(/([A-Z])/g, '-$1').toLowerCase();
7559
7599
  }
7600
+ /**
7601
+ * Returns true when the target is a custom element and the binding should be
7602
+ * delivered as a property rather than an HTML attribute.
7603
+ *
7604
+ * Two conditions trigger property delivery:
7605
+ * 1. The value is an object or array — serialising these to a string attribute
7606
+ * would lose type information.
7607
+ * 2. The element exposes a matching property accessor (e.g. a prop declared via
7608
+ * defineComponent), even when the value is a primitive.
7609
+ */
7610
+ #isCustomElementProperty(element, name, value) {
7611
+ if (!element.tagName.includes('-')) {
7612
+ return false;
7613
+ }
7614
+ const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
7615
+ return isObjectOrArray || name in element;
7616
+ }
7617
+ /**
7618
+ * Resolves the actual property name on a custom element for a given
7619
+ * (lowercased) HTML attribute name. HTML attributes are always lowercase,
7620
+ * but custom element props are typically camelCase. This method checks the
7621
+ * element's declared _props (set by defineComponent) for a case-insensitive
7622
+ * match, falling back to scanning the prototype's own property descriptors.
7623
+ */
7624
+ #resolveCustomElementPropertyName(element, name) {
7625
+ // Fast path: exact match already exists
7626
+ if (name in element) {
7627
+ return name;
7628
+ }
7629
+ // Check declared _props from defineComponent / IchigoElement
7630
+ const props = element.constructor._props;
7631
+ if (Array.isArray(props)) {
7632
+ const lowerName = name.toLowerCase();
7633
+ const match = props.find(p => p.toLowerCase() === lowerName);
7634
+ if (match) {
7635
+ return match;
7636
+ }
7637
+ }
7638
+ return name;
7639
+ }
7560
7640
  /**
7561
7641
  * Checks if the attribute should be set as a DOM property.
7562
7642
  */
@@ -8937,6 +9017,18 @@
8937
9017
  });
8938
9018
  }
8939
9019
  }
9020
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
9021
+ // If the node is a DocumentFragment, create child VNodes for its children
9022
+ // DocumentFragment is not an Element so it has no directives itself.
9023
+ this.#childVNodes = [];
9024
+ for (const childNode of Array.from(this.#node.childNodes)) {
9025
+ new VNode({
9026
+ node: childNode,
9027
+ vApplication: this.#vApplication,
9028
+ parentVNode: this
9029
+ });
9030
+ }
9031
+ }
8940
9032
  // Register this node as a dependent of the parent node, if any
8941
9033
  this.#closers = this.#parentVNode?.addDependent(this);
8942
9034
  }
@@ -9191,6 +9283,16 @@
9191
9283
  });
9192
9284
  }
9193
9285
  }
9286
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
9287
+ // For document fragments (e.g. from <template v-if>), propagate updates
9288
+ // to dependent virtual nodes so that child bindings stay reactive.
9289
+ this.#dependents?.forEach(dependentNode => {
9290
+ const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
9291
+ if (changed) {
9292
+ dependentNode.update();
9293
+ }
9294
+ });
9295
+ }
9194
9296
  }
9195
9297
  /**
9196
9298
  * Forces an update of the virtual node and its children, regardless of changed identifiers.
@@ -9240,6 +9342,12 @@
9240
9342
  });
9241
9343
  }
9242
9344
  }
9345
+ else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
9346
+ // For document fragments, recursively force update child VNodes
9347
+ this.#childVNodes?.forEach(childVNode => {
9348
+ childVNode.forceUpdate();
9349
+ });
9350
+ }
9243
9351
  }
9244
9352
  /**
9245
9353
  * Adds a child virtual node to this virtual node.
@@ -9617,9 +9725,11 @@
9617
9725
  }
9618
9726
  // Clone the original node and create a new VNode for it
9619
9727
  const clone = this.#cloneNode();
9620
- // Insert the cloned node after the anchor node, or as a child of the parent if no anchor
9621
- this.#vNode.anchorNode?.parentNode?.insertBefore(clone, this.#vNode.anchorNode.nextSibling);
9622
- // Create a new VNode for the cloned element
9728
+ // Create a new VNode for the cloned element BEFORE inserting into DOM.
9729
+ // This prevents custom elements (Web Components) from having their
9730
+ // connectedCallback fire before VNode processes their children, which
9731
+ // would cause the parent VApplication to incorrectly adopt the custom
9732
+ // element's internal template content as its own VNode tree.
9623
9733
  // Pass the current bindings to ensure loop variables from v-for are available
9624
9734
  const vNode = new VNode({
9625
9735
  node: clone,
@@ -9627,6 +9737,29 @@
9627
9737
  parentVNode: this.#vNode,
9628
9738
  bindings: this.#vNode.bindings
9629
9739
  });
9740
+ // Insert after the anchor node AFTER VNode creation
9741
+ const anchorParent = this.#vNode.anchorNode?.parentNode;
9742
+ const nextSibling = this.#vNode.anchorNode?.nextSibling ?? null;
9743
+ if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE && anchorParent) {
9744
+ const startMarker = document.createComment('#vif-fragment-start');
9745
+ const endMarker = document.createComment('#vif-fragment-end');
9746
+ if (nextSibling) {
9747
+ anchorParent.insertBefore(startMarker, nextSibling);
9748
+ }
9749
+ else {
9750
+ anchorParent.appendChild(startMarker);
9751
+ }
9752
+ anchorParent.insertBefore(endMarker, startMarker.nextSibling);
9753
+ anchorParent.insertBefore(clone, endMarker);
9754
+ // Store markers for later removal
9755
+ vNode.userData.set('vif_fragment_start', startMarker);
9756
+ vNode.userData.set('vif_fragment_end', endMarker);
9757
+ this.#renderedVNode = vNode;
9758
+ this.#renderedVNode.forceUpdate();
9759
+ return;
9760
+ }
9761
+ const nodeToInsert = vNode.anchorNode || clone;
9762
+ anchorParent?.insertBefore(nodeToInsert, nextSibling);
9630
9763
  this.#renderedVNode = vNode;
9631
9764
  this.#renderedVNode.forceUpdate();
9632
9765
  }
@@ -9641,17 +9774,40 @@
9641
9774
  }
9642
9775
  // Destroy VNode first (calls @unmount hooks while DOM is still accessible)
9643
9776
  this.#renderedVNode.destroy();
9644
- // Then remove from DOM
9645
- this.#renderedVNode.node.parentNode?.removeChild(this.#renderedVNode.node);
9777
+ // Then remove from DOM. Handle fragment markers if present
9778
+ const startMarker = this.#renderedVNode.userData.get?.('vif_fragment_start');
9779
+ const endMarker = this.#renderedVNode.userData.get?.('vif_fragment_end');
9780
+ if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
9781
+ const parentNode = startMarker.parentNode;
9782
+ let node = startMarker;
9783
+ while (node) {
9784
+ const next = node.nextSibling;
9785
+ parentNode.removeChild(node);
9786
+ if (node === endMarker)
9787
+ break;
9788
+ node = next;
9789
+ }
9790
+ this.#renderedVNode = undefined;
9791
+ return;
9792
+ }
9793
+ const parent = this.#renderedVNode.node.parentNode;
9794
+ if (parent) {
9795
+ parent.removeChild(this.#renderedVNode.node);
9796
+ }
9646
9797
  this.#renderedVNode = undefined;
9647
9798
  }
9648
9799
  /**
9649
9800
  * Clones the original node of the directive's virtual node.
9650
- * This is used to create a new instance of the node for rendering.
9651
- * @returns The cloned HTMLElement.
9801
+ * When the source element is a <template>, returns a DocumentFragment
9802
+ * cloned from the template's content.
9803
+ * @returns The cloned Node (HTMLElement or DocumentFragment).
9652
9804
  */
9653
9805
  #cloneNode() {
9654
9806
  const element = this.#vNode.node;
9807
+ if (element instanceof HTMLTemplateElement) {
9808
+ // Return a DocumentFragment cloned from the template content
9809
+ return element.content.cloneNode(true);
9810
+ }
9655
9811
  return element.cloneNode(true);
9656
9812
  }
9657
9813
  /**
@@ -9969,10 +10125,28 @@
9969
10125
  vNode.destroy();
9970
10126
  }
9971
10127
  }
9972
- // Then remove from DOM
10128
+ // Then remove from DOM. Handle both Element nodes and fragment-marked ranges.
9973
10129
  for (const vNode of nodesToRemove) {
9974
- if (vNode.node.parentNode) {
9975
- vNode.node.parentNode.removeChild(vNode.node);
10130
+ // If this VNode stored fragment markers, remove the range between them
10131
+ const startMarker = vNode.userData.get?.('vfor_fragment_start');
10132
+ const endMarker = vNode.userData.get?.('vfor_fragment_end');
10133
+ if (startMarker && endMarker && startMarker.parentNode === endMarker.parentNode && startMarker.parentNode) {
10134
+ const parentNode = startMarker.parentNode;
10135
+ let node = startMarker;
10136
+ // Remove nodes from startMarker up to and including endMarker
10137
+ while (node) {
10138
+ const next = node.nextSibling;
10139
+ parentNode.removeChild(node);
10140
+ if (node === endMarker)
10141
+ break;
10142
+ node = next;
10143
+ }
10144
+ continue;
10145
+ }
10146
+ // Fallback: remove the node itself if it's attached
10147
+ const parentOfNode = vNode.node.parentNode;
10148
+ if (parentOfNode) {
10149
+ parentOfNode.removeChild(vNode.node);
9976
10150
  }
9977
10151
  }
9978
10152
  // Add or reorder items
@@ -10006,6 +10180,28 @@
10006
10180
  bindings,
10007
10181
  dependentIdentifiers: depIds,
10008
10182
  });
10183
+ // If clone is a DocumentFragment, insert it between start/end comment markers
10184
+ if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
10185
+ const startMarker = document.createComment('#vfor-fragment-start');
10186
+ const endMarker = document.createComment('#vfor-fragment-end');
10187
+ // Insert start and end markers and the fragment's children between them
10188
+ if (prevNode.nextSibling) {
10189
+ parent.insertBefore(startMarker, prevNode.nextSibling);
10190
+ }
10191
+ else {
10192
+ parent.appendChild(startMarker);
10193
+ }
10194
+ parent.insertBefore(endMarker, startMarker.nextSibling);
10195
+ parent.insertBefore(clone, endMarker);
10196
+ // Store markers on the VNode for later removal/movement
10197
+ vNode.userData.set('vfor_fragment_start', startMarker);
10198
+ vNode.userData.set('vfor_fragment_end', endMarker);
10199
+ newRenderedItems.set(key, vNode);
10200
+ vNode.forceUpdate();
10201
+ // Use endMarker as prevNode for subsequent insertions
10202
+ prevNode = endMarker;
10203
+ continue;
10204
+ }
10009
10205
  // Determine what to insert: anchor node (if exists) or the clone itself
10010
10206
  const nodeToInsert = vNode.anchorNode || clone;
10011
10207
  // Insert after previous node
@@ -10023,8 +10219,9 @@
10023
10219
  newRenderedItems.set(key, vNode);
10024
10220
  // Update bindings
10025
10221
  this.#updateItemBindings(vNode, context);
10026
- // Determine the actual node in DOM: anchor node (if exists) or vNode.node
10027
- const actualNode = vNode.anchorNode || vNode.node;
10222
+ // Determine the actual node in DOM: prefer fragment end marker, then anchor node, then vNode.node
10223
+ const fragmentEnd = vNode.userData.get?.('vfor_fragment_end');
10224
+ const actualNode = fragmentEnd || vNode.anchorNode || vNode.node;
10028
10225
  // Move to correct position if needed
10029
10226
  if (prevNode.nextSibling !== actualNode) {
10030
10227
  if (prevNode.nextSibling) {
@@ -10035,8 +10232,9 @@
10035
10232
  }
10036
10233
  }
10037
10234
  }
10038
- // Use anchor node as prevNode if it exists, otherwise use vNode.node
10039
- prevNode = vNode.anchorNode || vNode.node;
10235
+ // Use fragment end marker > anchor node > vNode.node as prevNode
10236
+ const fragmentEndForPrev = vNode.userData.get?.('vfor_fragment_end');
10237
+ prevNode = fragmentEndForPrev || vNode.anchorNode || vNode.node;
10040
10238
  }
10041
10239
  // Update rendered items map
10042
10240
  this.#renderedItems = newRenderedItems;
@@ -10215,12 +10413,17 @@
10215
10413
  }
10216
10414
  /**
10217
10415
  * Clones the original node of the directive's virtual node.
10218
- * This is used to create a new instance of the node for rendering.
10219
- * @returns The cloned HTMLElement.
10416
+ * When the source element is a <template>, its children (stored in .content)
10417
+ * are cloned into a DocumentFragment so that multiple root nodes can be
10418
+ * managed without adding an extra wrapper element.
10419
+ * @returns The cloned Node (Element or DocumentFragment).
10220
10420
  */
10221
10421
  #cloneNode() {
10222
- // Clone the original element
10223
10422
  const element = this.#vNode.node;
10423
+ if (element instanceof HTMLTemplateElement) {
10424
+ // Return a cloned DocumentFragment containing the template content
10425
+ return element.content.cloneNode(true);
10426
+ }
10224
10427
  return element.cloneNode(true);
10225
10428
  }
10226
10429
  /**
@@ -11061,10 +11264,12 @@
11061
11264
  this.#eventName = parts[0];
11062
11265
  parts.slice(1).forEach(mod => this.#modifiers.add(mod));
11063
11266
  }
11064
- // 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.
11065
11270
  const expression = context.attribute.value;
11066
11271
  if (expression) {
11067
- this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
11272
+ this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies, { asScript: true });
11068
11273
  }
11069
11274
  // Check if this is a lifecycle hook or a regular event
11070
11275
  if (this.#eventName && this.#isLifecycleHook(this.#eventName)) {
@@ -11278,10 +11483,11 @@
11278
11483
  // This allows the method to access the DOM element, VNode, and userData
11279
11484
  return originalMethod($ctx);
11280
11485
  }
11281
- // For inline expressions, rewrite to use 'this' context
11282
- // This allows assignments like "currentTab = 'shop'" to work correctly
11283
- const rewrittenExpr = this.#rewriteExpression(expression, identifiers);
11284
- const funcBody = `return (${rewrittenExpr});`;
11486
+ // For inline bodies, rewrite to use 'this' context
11487
+ // This allows assignments like "currentTab = 'shop'" to work correctly.
11488
+ // Script mode allows multi-statement bodies (e.g. "a=1; init()") and control-flow.
11489
+ const rewrittenExpr = this.#rewriteExpression(expression, identifiers, { asScript: true });
11490
+ const funcBody = rewrittenExpr;
11285
11491
  const func = new Function('$ctx', funcBody);
11286
11492
  return func.call(bindings?.raw, $ctx);
11287
11493
  };
@@ -11311,12 +11517,15 @@
11311
11517
  // Pass event as first argument and $ctx as second argument
11312
11518
  return originalMethod(event, $ctx);
11313
11519
  }
11314
- // For inline expressions, rewrite to use 'this' context
11315
- // This allows assignments like "currentTab = 'shop'" to work correctly
11316
- const rewrittenExpr = this.#rewriteExpression(expression, identifiers);
11317
- const funcBody = `return (${rewrittenExpr});`;
11318
- const func = new Function('event', '$ctx', funcBody);
11319
- return func.call(bindings?.raw, event, $ctx);
11520
+ // For inline bodies, rewrite to use 'this' context
11521
+ // This allows assignments like "currentTab = 'shop'" to work correctly.
11522
+ // Script mode allows multi-statement bodies (e.g. "a=1; b=2") and control-flow,
11523
+ // so we emit the rewritten source directly as the function body (no `return (...)`).
11524
+ const rewrittenExpr = this.#rewriteExpression(expression, identifiers, { asScript: true });
11525
+ const funcBody = rewrittenExpr;
11526
+ // '$event' is an alias for 'event' for Vue compatibility
11527
+ const func = new Function('event', '$event', '$ctx', funcBody);
11528
+ return func.call(bindings?.raw, event, event, $ctx);
11320
11529
  };
11321
11530
  }
11322
11531
  /**
@@ -11327,8 +11536,8 @@
11327
11536
  * @param identifiers The list of identifiers that are available in bindings.
11328
11537
  * @returns The rewritten expression.
11329
11538
  */
11330
- #rewriteExpression(expression, identifiers) {
11331
- return ExpressionUtils.rewriteExpression(expression, identifiers);
11539
+ #rewriteExpression(expression, identifiers, options) {
11540
+ return ExpressionUtils.rewriteExpression(expression, identifiers, options);
11332
11541
  }
11333
11542
  }
11334
11543
 
@@ -13026,6 +13235,7 @@
13026
13235
  /**
13027
13236
  * Gets the component registry.
13028
13237
  * @return {VComponentRegistry} The component registry.
13238
+ * @deprecated This method is deprecated and will be removed in a future release. Please use the new component registration system instead.
13029
13239
  */
13030
13240
  static get componentRegistry() {
13031
13241
  return this.#componentRegistry;
@@ -13069,11 +13279,268 @@
13069
13279
  }
13070
13280
  }
13071
13281
 
13282
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
13283
+ /**
13284
+ * Base class for ichigo.js-backed Web Components (Light DOM, no Shadow DOM).
13285
+ *
13286
+ * Mount timing:
13287
+ * - If the component declares no props, the VApplication is mounted synchronously
13288
+ * at the end of connectedCallback (the template is known, no props to wait for).
13289
+ * - If the component declares props, the VApplication is mounted the first time
13290
+ * _setProp() is called after connectedCallback has prepared the DOM. This
13291
+ * guarantees that the parent framework (e.g. ichigo.js VBindDirective) has
13292
+ * already delivered at least one prop value before data() is evaluated.
13293
+ *
13294
+ * Subclasses must set the static fields _template, _props and _buildOptions before
13295
+ * calling customElements.define(). defineComponent() handles this automatically.
13296
+ */
13297
+ class IchigoElement extends HTMLElement {
13298
+ /**
13299
+ * The mounted VApplication instance, present only while connected to the DOM.
13300
+ */
13301
+ #app;
13302
+ /**
13303
+ * Stores prop values received at any time (before or after mount).
13304
+ */
13305
+ #propValues = {};
13306
+ /**
13307
+ * The root element cloned from the template, ready for mounting.
13308
+ * Set by connectedCallback; cleared on disconnect.
13309
+ */
13310
+ #mountRoot;
13311
+ /**
13312
+ * Whether a mount microtask is already queued to avoid double-mounting.
13313
+ */
13314
+ #mountScheduled = false;
13315
+ connectedCallback() {
13316
+ // --- 0. Guard: skip if template not yet loaded ---
13317
+ // customElements.define() may run before loadComponent() completes (e.g. when
13318
+ // dynamic imports are inlined by the bundler). The static placeholder element
13319
+ // in the HTML will be removed and replaced by v-for once the template is ready,
13320
+ // so it is safe to do nothing here.
13321
+ const ctor = this.constructor;
13322
+ const templateEl = document.querySelector(ctor._template);
13323
+ if (!templateEl || !(templateEl instanceof HTMLTemplateElement)) {
13324
+ return;
13325
+ }
13326
+ // --- 1. Capture slot content before clearing children ---
13327
+ const defaultSlotNodes = [];
13328
+ const namedSlotNodes = new Map();
13329
+ for (const child of Array.from(this.childNodes)) {
13330
+ if (child.nodeType === Node.ELEMENT_NODE) {
13331
+ const el = child;
13332
+ const slotName = el.getAttribute('slot');
13333
+ if (slotName) {
13334
+ if (!namedSlotNodes.has(slotName)) {
13335
+ namedSlotNodes.set(slotName, []);
13336
+ }
13337
+ namedSlotNodes.get(slotName).push(el);
13338
+ // Remove slot attribute so ichigo.js doesn't try to bind it
13339
+ el.removeAttribute('slot');
13340
+ }
13341
+ else {
13342
+ defaultSlotNodes.push(el);
13343
+ }
13344
+ }
13345
+ else if (child.nodeType === Node.TEXT_NODE) {
13346
+ if ((child.textContent ?? '').trim()) {
13347
+ defaultSlotNodes.push(child);
13348
+ }
13349
+ }
13350
+ }
13351
+ // Clear host element so we can append the cloned template
13352
+ while (this.firstChild) {
13353
+ this.removeChild(this.firstChild);
13354
+ }
13355
+ // --- 2. Clone the component template ---
13356
+ const fragment = templateEl.content.cloneNode(true);
13357
+ const root = this.#findRootElement(fragment);
13358
+ // --- 3. Distribute named slot content ---
13359
+ for (const [name, nodes] of namedSlotNodes) {
13360
+ const slot = root.querySelector(`slot[name="${name}"]`);
13361
+ if (slot) {
13362
+ slot.replaceWith(...nodes);
13363
+ }
13364
+ }
13365
+ // --- 4. Distribute default slot content ---
13366
+ const defaultSlot = root.querySelector('slot:not([name])');
13367
+ if (defaultSlot && defaultSlotNodes.length > 0) {
13368
+ defaultSlot.replaceWith(...defaultSlotNodes);
13369
+ }
13370
+ // Attach the populated template to the host element
13371
+ this.appendChild(root);
13372
+ this.#mountRoot = root;
13373
+ // If props were set before connectedCallback, ensure mount is scheduled
13374
+ this.#scheduleMountIfNeeded();
13375
+ // --- 5. Mount strategy ---
13376
+ // If this component has no declared props, mount immediately.
13377
+ // If it has props, mount will be triggered from _setProp() once the parent
13378
+ // delivers the first prop value (via VBindDirective / forceUpdate).
13379
+ if (ctor._props.length === 0) {
13380
+ this.#doMount();
13381
+ }
13382
+ // else: wait for _setProp() to trigger #scheduleMountIfNeeded()
13383
+ }
13384
+ disconnectedCallback() {
13385
+ this.#mountRoot = undefined;
13386
+ this.#mountScheduled = false;
13387
+ if (this.#app) {
13388
+ this.#app.unmount();
13389
+ this.#app = undefined;
13390
+ }
13391
+ }
13392
+ /**
13393
+ * Called by the property setters generated by defineComponent().
13394
+ * Before mount: stores the value and schedules a mount microtask.
13395
+ * After mount: pushes the value directly into the reactive bindings.
13396
+ */
13397
+ _setProp(name, value) {
13398
+ this.#propValues[name] = value;
13399
+ if (this.#app) {
13400
+ this.#app.bindings?.set(name, value);
13401
+ }
13402
+ else {
13403
+ this.#scheduleMountIfNeeded();
13404
+ }
13405
+ }
13406
+ /**
13407
+ * Called by the property getters generated by defineComponent().
13408
+ */
13409
+ _getProp(name) {
13410
+ return this.#propValues[name];
13411
+ }
13412
+ // --- Static fields set by defineComponent() ---
13413
+ /**
13414
+ * CSS selector for the component's <template> element (e.g. '#my-card').
13415
+ */
13416
+ static _template;
13417
+ /**
13418
+ * List of declared prop names. Used to decide whether to defer mounting.
13419
+ */
13420
+ static _props = [];
13421
+ /**
13422
+ * Factory that builds VApplicationOptions from the current prop values.
13423
+ * Implemented by defineComponent() as a closure that captures the user's options.
13424
+ */
13425
+ static _buildOptions;
13426
+ // --- Private helpers ---
13427
+ /**
13428
+ * Schedules a mount microtask if the DOM root is ready and no mount is pending.
13429
+ * Called from _setProp() so the mount happens after the prop value is stored.
13430
+ */
13431
+ #scheduleMountIfNeeded() {
13432
+ if (this.#mountScheduled || this.#app || !this.#mountRoot) {
13433
+ return;
13434
+ }
13435
+ this.#mountScheduled = true;
13436
+ queueMicrotask(() => {
13437
+ this.#mountScheduled = false;
13438
+ this.#doMount();
13439
+ });
13440
+ }
13441
+ #doMount() {
13442
+ if (this.#app || !this.#mountRoot) {
13443
+ return;
13444
+ }
13445
+ const ctor = this.constructor;
13446
+ const options = ctor._buildOptions(this.#propValues);
13447
+ this.#app = VDOM.createApp(options);
13448
+ this.#app.mount(this.#mountRoot);
13449
+ }
13450
+ #findRootElement(fragment) {
13451
+ for (const node of Array.from(fragment.childNodes)) {
13452
+ if (node.nodeType === Node.ELEMENT_NODE) {
13453
+ return node;
13454
+ }
13455
+ }
13456
+ throw new Error(`IchigoElement: no root element found in template '${this.constructor._template}'`);
13457
+ }
13458
+ }
13459
+
13460
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
13461
+ /**
13462
+ * Defines and registers a custom element backed by ichigo.js reactivity.
13463
+ *
13464
+ * Usage:
13465
+ * ```html
13466
+ * <template id="my-list">
13467
+ * <div>
13468
+ * <ul v-if="items.length > 0">
13469
+ * <li v-for="item of items">{{item.name}}</li>
13470
+ * </ul>
13471
+ * <slot></slot>
13472
+ * </div>
13473
+ * </template>
13474
+ * ```
13475
+ * ```typescript
13476
+ * defineComponent('my-list', {
13477
+ * template: '#my-list',
13478
+ * props: ['items'],
13479
+ * data() {
13480
+ * return { items: this.items ?? [] };
13481
+ * },
13482
+ * });
13483
+ * ```
13484
+ * ```html
13485
+ * <my-list :items="searchResults">
13486
+ * <span slot="empty">No results.</span>
13487
+ * </my-list>
13488
+ * ```
13489
+ *
13490
+ * @param tagName Custom element tag name (must contain a hyphen, e.g. 'my-card').
13491
+ * @param options Component options including template selector and optional props.
13492
+ */
13493
+ function defineComponent(tagName, options) {
13494
+ const { props = [], template, data, computed, methods, watch, logLevel } = options;
13495
+ // Build a subclass of IchigoElement specific to this component
13496
+ class ComponentElement extends IchigoElement {
13497
+ static _template = template;
13498
+ static _props = props;
13499
+ static _buildOptions(propValues) {
13500
+ return {
13501
+ data() {
13502
+ // 'this' is the $ctx object provided by VApplication ({ $markRaw }).
13503
+ // We extend it with prop values so the user's data() can reference them
13504
+ // via 'this.propName' and supply defaults (e.g. `this.items ?? []`).
13505
+ const ctx = { $markRaw: ReactiveProxy.markRaw.bind(ReactiveProxy), ...propValues };
13506
+ const userData = data
13507
+ ? data.call(ctx)
13508
+ : {};
13509
+ // Props are always included in data so they are reactive from the start.
13510
+ // User-returned values take precedence (allow transforming/defaulting props).
13511
+ return { ...propValues, ...userData };
13512
+ },
13513
+ computed,
13514
+ methods,
13515
+ watch,
13516
+ logLevel,
13517
+ };
13518
+ }
13519
+ }
13520
+ // Generate a property getter/setter for each declared prop.
13521
+ // This enables the parent VApplication to push updates via `element.propName = value`.
13522
+ for (const prop of props) {
13523
+ Object.defineProperty(ComponentElement.prototype, prop, {
13524
+ get() {
13525
+ return this._getProp(prop);
13526
+ },
13527
+ set(value) {
13528
+ this._setProp(prop, value);
13529
+ },
13530
+ configurable: true,
13531
+ enumerable: true,
13532
+ });
13533
+ }
13534
+ customElements.define(tagName, ComponentElement);
13535
+ }
13536
+
13072
13537
  exports.ExpressionUtils = ExpressionUtils;
13538
+ exports.IchigoElement = IchigoElement;
13073
13539
  exports.ReactiveProxy = ReactiveProxy;
13074
13540
  exports.VComponent = VComponent;
13075
13541
  exports.VComponentRegistry = VComponentRegistry;
13076
13542
  exports.VDOM = VDOM;
13543
+ exports.defineComponent = defineComponent;
13077
13544
 
13078
13545
  }));
13079
13546
  //# sourceMappingURL=ichigo.cjs.map