@mintjamsinc/ichigojs 0.1.85 → 0.1.87

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.
@@ -6726,6 +6726,507 @@ base.MethodDefinition = base.PropertyDefinition = base.Property = function (node
6726
6726
  if (node.value) { c(node.value, st, "Expression"); }
6727
6727
  };
6728
6728
 
6729
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
6730
+ /**
6731
+ * Utility class for creating reactive proxies that automatically track changes.
6732
+ */
6733
+ class ReactiveProxy {
6734
+ /**
6735
+ * A WeakMap to store the proxy for each target object and path combination.
6736
+ * This prevents creating multiple proxies for the same object accessed from different paths.
6737
+ */
6738
+ static proxyCache = new WeakMap();
6739
+ /**
6740
+ * A WeakMap to track which objects are proxies, mapping proxy -> original target.
6741
+ * This prevents double-wrapping of already proxied objects.
6742
+ */
6743
+ static proxyToTarget = new WeakMap();
6744
+ /**
6745
+ * A WeakSet to track objects marked as "raw" (non-reactive).
6746
+ * These objects will not be wrapped with Proxy.
6747
+ */
6748
+ static rawObjects = new WeakSet();
6749
+ /**
6750
+ * A WeakMap to store the path for each proxy object.
6751
+ * This allows retrieving the source path of an object for computed property mapping.
6752
+ */
6753
+ static proxyPaths = new WeakMap();
6754
+ /**
6755
+ * Dispatchers per (target, path). Every target reached while walking a proxy
6756
+ * subtree registers an entry pointing at the same dispatcher as the outermost
6757
+ * proxy of that subtree, so callers can look up the dispatcher from any
6758
+ * intermediate proxy when subscribing.
6759
+ */
6760
+ static dispatchers = new WeakMap();
6761
+ /**
6762
+ * A Map to store path aliases.
6763
+ * Key: alias path (e.g., "editingNestedStep.steps")
6764
+ * Value: source path (e.g., "routes[0].steps[0].steps")
6765
+ * This allows mapping variable names to their actual source paths for dependency tracking.
6766
+ */
6767
+ static pathAliases = new Map();
6768
+ /**
6769
+ * Compiled patterns for identifiers that contain a "[*]" index, keyed by the
6770
+ * identifier and whether descendants of the path also match.
6771
+ */
6772
+ static pathPatterns = new Map();
6773
+ /**
6774
+ * The set collecting the paths read through any proxy while track() runs, or
6775
+ * undefined when no expression is being tracked.
6776
+ */
6777
+ static tracker;
6778
+ /**
6779
+ * Whether a property key is spelled as an array index ("0", "12") rather than a
6780
+ * property name. Both the proxy and the expression analyzer decide between
6781
+ * "items[0]" and "items.name" with this test alone, so a numeric key on a plain
6782
+ * object and a numeric key on an array get the same spelling.
6783
+ *
6784
+ * @param key The property key as a string.
6785
+ * @returns True if the key is a canonical non-negative integer.
6786
+ */
6787
+ static isIndexKey(key) {
6788
+ return /^(?:0|[1-9]\d*)$/.test(key);
6789
+ }
6790
+ /**
6791
+ * Appends a property key to a source path the way change paths are reported:
6792
+ * an index in brackets ("items[0]"), a name after a dot ("user.name").
6793
+ *
6794
+ * @param path The path of the containing object, or "" for a root binding.
6795
+ * @param key The property key being appended.
6796
+ * @returns The joined path.
6797
+ */
6798
+ static joinPath(path, key) {
6799
+ const keyStr = String(key);
6800
+ if (!path) {
6801
+ return keyStr;
6802
+ }
6803
+ return this.isIndexKey(keyStr) ? `${path}[${keyStr}]` : `${path}.${keyStr}`;
6804
+ }
6805
+ /**
6806
+ * Runs fn while recording, into paths, the source path of every property read
6807
+ * through a reactive proxy. Nested calls record into the innermost set only;
6808
+ * the previous set is restored when fn returns or throws.
6809
+ *
6810
+ * @param paths The set that receives the paths read.
6811
+ * @param fn The function to run.
6812
+ * @returns The value fn returns.
6813
+ */
6814
+ static track(paths, fn) {
6815
+ const previous = this.tracker;
6816
+ this.tracker = paths;
6817
+ try {
6818
+ return fn();
6819
+ }
6820
+ finally {
6821
+ this.tracker = previous;
6822
+ }
6823
+ }
6824
+ /**
6825
+ * The `Object.prototype.toString` tags of the *only* value types we deeply
6826
+ * proxy. Everything else is returned untouched (raw).
6827
+ *
6828
+ * Rationale: a Proxy can only safely intercept plain data containers. Built-in
6829
+ * and host objects — `Date`, `RegExp`, `File`, `Blob`, `FileList`, `Promise`,
6830
+ * `WeakMap`/`WeakSet`, typed arrays, DOM nodes, etc. — carry private internal
6831
+ * slots, and invoking their native methods with a Proxy as the `this`/receiver
6832
+ * throws "Illegal invocation". They must therefore never be wrapped. This is an
6833
+ * allow-list rather than a deny-list so that *every* such exotic type is
6834
+ * excluded by default, not just the handful we happened to enumerate.
6835
+ *
6836
+ * Plain user-defined class instances report `[object Object]` (unless they set
6837
+ * `Symbol.toStringTag`) and so remain reactive, preserving prior behaviour.
6838
+ * `Map`/`Set` stay reactive because the proxy specially wraps their mutation
6839
+ * methods (see the `get` trap); `WeakMap`/`WeakSet` are intentionally excluded
6840
+ * since they cannot be wrapped that way and would break the same as `File`.
6841
+ *
6842
+ * Callers that want a plain object kept non-reactive can still opt out
6843
+ * explicitly via {@link markRaw}.
6844
+ */
6845
+ static REACTIVE_TYPE_TAGS = new Set([
6846
+ '[object Object]',
6847
+ '[object Array]',
6848
+ '[object Arguments]',
6849
+ '[object Map]',
6850
+ '[object Set]',
6851
+ ]);
6852
+ /**
6853
+ * Whether the given non-null object value is one we deeply proxy.
6854
+ * See {@link REACTIVE_TYPE_TAGS} for the reasoning.
6855
+ */
6856
+ static isReactivable(value) {
6857
+ return this.REACTIVE_TYPE_TAGS.has(Object.prototype.toString.call(value));
6858
+ }
6859
+ /**
6860
+ * Creates a reactive proxy for the given object.
6861
+ * The proxy will call the onChange callback whenever a property is modified.
6862
+ *
6863
+ * @param target The object to make reactive.
6864
+ * @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
6865
+ * @param path The current path in the object tree (used internally for nested objects).
6866
+ * @param inheritedDispatcher Internal: the dispatcher inherited from an enclosing create() call when wrapping a nested target. External callers must omit this.
6867
+ * @returns A reactive proxy of the target object.
6868
+ */
6869
+ static create(target, onChange, path = '', inheritedDispatcher) {
6870
+ // If the target is not an object or is null, return it as-is
6871
+ if (typeof target !== 'object' || target === null) {
6872
+ return target;
6873
+ }
6874
+ // Don't wrap objects marked as raw (non-reactive)
6875
+ if (this.rawObjects.has(target)) {
6876
+ return target;
6877
+ }
6878
+ // Only deeply proxy plain data containers; return every built-in/host
6879
+ // object (Date, File, Blob, Promise, WeakMap, DOM nodes, …) as-is, since
6880
+ // their native methods break when invoked through a Proxy receiver.
6881
+ if (!this.isReactivable(target)) {
6882
+ return target;
6883
+ }
6884
+ // Check if the target is already a proxy - if so, return it as-is to prevent double-wrapping
6885
+ if (this.proxyToTarget.has(target)) {
6886
+ return target;
6887
+ }
6888
+ // Resolve (and register) the dispatcher for this (target, path).
6889
+ // Nested create() calls inherit the dispatcher from the enclosing call so
6890
+ // that a single dispatcher fans out changes at any depth of the subtree.
6891
+ const dispatcher = this.resolveDispatcher(target, path, inheritedDispatcher);
6892
+ if (onChange) {
6893
+ dispatcher.listeners.add(onChange);
6894
+ }
6895
+ // Check if we already have a proxy for this target with this path
6896
+ let pathMap = this.proxyCache.get(target);
6897
+ if (pathMap) {
6898
+ const existingProxy = pathMap.get(path);
6899
+ if (existingProxy) {
6900
+ return existingProxy;
6901
+ }
6902
+ }
6903
+ else {
6904
+ pathMap = new Map();
6905
+ this.proxyCache.set(target, pathMap);
6906
+ }
6907
+ // Create the proxy with path captured in closure
6908
+ const proxy = new Proxy(target, {
6909
+ get(obj, key, receiver) {
6910
+ const value = Reflect.get(obj, key);
6911
+ // Record the read for an expression being tracked (see track()).
6912
+ // Function-valued members (push, Map#get, …) are accesses to behaviour,
6913
+ // not data, so they are left out.
6914
+ if (ReactiveProxy.tracker && typeof key !== 'symbol' && typeof value !== 'function') {
6915
+ ReactiveProxy.tracker.add(ReactiveProxy.joinPath(path, key));
6916
+ }
6917
+ // If the value is an object or array, make it reactive too
6918
+ if (typeof value === 'object' && value !== null) {
6919
+ // Don't wrap objects marked as raw (non-reactive)
6920
+ if (ReactiveProxy.rawObjects.has(value)) {
6921
+ return value;
6922
+ }
6923
+ // Only deeply proxy plain data containers; hand back built-in
6924
+ // and host objects (Date, File, Blob, Promise, WeakMap, DOM
6925
+ // nodes, …) untouched so their native methods keep working.
6926
+ if (!ReactiveProxy.isReactivable(value)) {
6927
+ return value;
6928
+ }
6929
+ // Build the nested path
6930
+ const nestedPath = ReactiveProxy.joinPath(path, key);
6931
+ return ReactiveProxy.create(value, undefined, nestedPath, dispatcher);
6932
+ }
6933
+ // If the value is a function, we need to wrap it to ensure that any mutations it performs also trigger onChange
6934
+ if (typeof value === 'function') {
6935
+ // For arrays, we only want to wrap mutation methods, not read methods like 'slice', 'concat', etc.
6936
+ if (Array.isArray(obj)) {
6937
+ const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
6938
+ if (!arrayMutationMethods.includes(key)) {
6939
+ return value;
6940
+ }
6941
+ return function (...args) {
6942
+ const result = value.apply(this === receiver ? obj : this, args);
6943
+ ReactiveProxy.dispatch(dispatcher, path || undefined);
6944
+ return result;
6945
+ };
6946
+ }
6947
+ // For Map, we only want to wrap mutation methods, not read methods like 'get' or 'has'
6948
+ if (obj.constructor.name === 'Map') {
6949
+ const mapMutationMethods = ['set', 'delete', 'clear'];
6950
+ return function (...args) {
6951
+ const result = value.apply(this === receiver ? obj : this, args);
6952
+ if (mapMutationMethods.includes(key)) {
6953
+ ReactiveProxy.dispatch(dispatcher, path || undefined);
6954
+ }
6955
+ return result;
6956
+ };
6957
+ }
6958
+ // For Set, we only want to wrap mutation methods, not read methods like 'has'
6959
+ if (obj.constructor.name === 'Set') {
6960
+ const setMutationMethods = ['add', 'delete', 'clear'];
6961
+ return function (...args) {
6962
+ const result = value.apply(this === receiver ? obj : this, args);
6963
+ if (setMutationMethods.includes(key)) {
6964
+ ReactiveProxy.dispatch(dispatcher, path || undefined);
6965
+ }
6966
+ return result;
6967
+ };
6968
+ }
6969
+ }
6970
+ return value;
6971
+ },
6972
+ set(obj, key, value) {
6973
+ const oldValue = Reflect.get(obj, key);
6974
+ const result = Reflect.set(obj, key, value);
6975
+ // Only trigger onChange if the value actually changed
6976
+ if (oldValue !== value) {
6977
+ ReactiveProxy.dispatch(dispatcher, ReactiveProxy.joinPath(path, key));
6978
+ }
6979
+ return result;
6980
+ },
6981
+ deleteProperty(obj, key) {
6982
+ const result = Reflect.deleteProperty(obj, key);
6983
+ ReactiveProxy.dispatch(dispatcher, ReactiveProxy.joinPath(path, key));
6984
+ return result;
6985
+ }
6986
+ });
6987
+ // Cache the proxy for this path
6988
+ pathMap.set(path, proxy);
6989
+ // Track that this proxy wraps the target to prevent double-wrapping
6990
+ this.proxyToTarget.set(proxy, target);
6991
+ // Store the path for this proxy (for computed property mapping)
6992
+ if (path) {
6993
+ this.proxyPaths.set(proxy, path);
6994
+ }
6995
+ return proxy;
6996
+ }
6997
+ /**
6998
+ * Looks up the dispatcher associated with (target, path), or installs a new one.
6999
+ * When called for a nested target during proxy walking, the enclosing dispatcher
7000
+ * is reused so a single subtree fans out changes through one notification path.
7001
+ */
7002
+ static resolveDispatcher(target, path, inheritedDispatcher) {
7003
+ let pathMap = this.dispatchers.get(target);
7004
+ if (!pathMap) {
7005
+ pathMap = new Map();
7006
+ this.dispatchers.set(target, pathMap);
7007
+ }
7008
+ const existing = pathMap.get(path);
7009
+ if (existing) {
7010
+ return existing;
7011
+ }
7012
+ const dispatcher = inheritedDispatcher ?? { listeners: new Set() };
7013
+ pathMap.set(path, dispatcher);
7014
+ return dispatcher;
7015
+ }
7016
+ /**
7017
+ * Invokes every listener attached to the dispatcher.
7018
+ * Iterates a snapshot of the listener set so unsubscribing during dispatch is safe.
7019
+ */
7020
+ static dispatch(dispatcher, changedPath) {
7021
+ if (dispatcher.listeners.size === 0) {
7022
+ return;
7023
+ }
7024
+ const snapshot = Array.from(dispatcher.listeners);
7025
+ for (const listener of snapshot) {
7026
+ listener(changedPath);
7027
+ }
7028
+ }
7029
+ /**
7030
+ * Subscribes a listener to changes inside the subtree of an existing reactive proxy.
7031
+ *
7032
+ * The listener is scoped by the proxy's source path: only changes at or below that
7033
+ * path are delivered, which lets a child component receive notifications when the
7034
+ * nested contents of a prop change even though the prop reference itself is unchanged.
7035
+ *
7036
+ * @param proxyOrTarget A proxy returned from create(), or the underlying target object.
7037
+ * @param listener Called with the full source path of every relevant change.
7038
+ * @returns A function that removes the subscription.
7039
+ */
7040
+ static subscribe(proxyOrTarget, listener) {
7041
+ if (typeof proxyOrTarget !== 'object' || proxyOrTarget === null) {
7042
+ return () => { };
7043
+ }
7044
+ const target = (this.proxyToTarget.get(proxyOrTarget) ?? proxyOrTarget);
7045
+ const scopePath = this.proxyPaths.get(proxyOrTarget) ?? '';
7046
+ const pathMap = this.dispatchers.get(target);
7047
+ const dispatcher = pathMap?.get(scopePath);
7048
+ if (!dispatcher) {
7049
+ return () => { };
7050
+ }
7051
+ const wrapper = scopePath
7052
+ ? (changedPath) => {
7053
+ if (changedPath === scopePath ||
7054
+ (typeof changedPath === 'string' && (changedPath.startsWith(scopePath + '.') ||
7055
+ changedPath.startsWith(scopePath + '[')))) {
7056
+ listener(changedPath);
7057
+ }
7058
+ }
7059
+ : listener;
7060
+ dispatcher.listeners.add(wrapper);
7061
+ return () => {
7062
+ dispatcher.listeners.delete(wrapper);
7063
+ };
7064
+ }
7065
+ /**
7066
+ * Checks if the given object is a reactive proxy.
7067
+ *
7068
+ * @param obj The object to check.
7069
+ * @returns True if the object is a reactive proxy, false otherwise.
7070
+ */
7071
+ static isReactive(obj) {
7072
+ return this.proxyCache.has(obj);
7073
+ }
7074
+ /**
7075
+ * Unwraps a reactive proxy to get the original object.
7076
+ * If the object is not a proxy, returns it as-is.
7077
+ *
7078
+ * @param obj The object to unwrap.
7079
+ * @returns The original object.
7080
+ */
7081
+ static unwrap(obj) {
7082
+ // This is a simplified implementation
7083
+ // In a full implementation, we'd need to store a reverse mapping
7084
+ return obj;
7085
+ }
7086
+ /**
7087
+ * Marks an object as "raw" (non-reactive).
7088
+ * Objects marked as raw will not be wrapped with Proxy when accessed from reactive objects.
7089
+ * This is useful for objects that should not be reactive, such as:
7090
+ * - Objects with private fields (class instances with # fields)
7091
+ * - Third-party library instances
7092
+ * - Objects used only for method calls
7093
+ *
7094
+ * @param obj The object to mark as raw.
7095
+ * @returns The same object (for chaining).
7096
+ */
7097
+ static markRaw(obj) {
7098
+ if (typeof obj === 'object' && obj !== null) {
7099
+ this.rawObjects.add(obj);
7100
+ }
7101
+ return obj;
7102
+ }
7103
+ /**
7104
+ * Checks if an object is marked as raw (non-reactive).
7105
+ *
7106
+ * @param obj The object to check.
7107
+ * @returns True if the object is marked as raw, false otherwise.
7108
+ */
7109
+ static isRaw(obj) {
7110
+ return typeof obj === 'object' && obj !== null && this.rawObjects.has(obj);
7111
+ }
7112
+ /**
7113
+ * Gets the source path for a proxy object.
7114
+ * This is used to map computed property values back to their source paths.
7115
+ * For example, if a computed property returns `model.elements[0]`,
7116
+ * this method returns "model.elements[0]" for that object.
7117
+ *
7118
+ * @param obj The proxy object to get the path for.
7119
+ * @returns The source path, or undefined if not found.
7120
+ */
7121
+ static getPath(obj) {
7122
+ if (typeof obj !== 'object' || obj === null) {
7123
+ return undefined;
7124
+ }
7125
+ return this.proxyPaths.get(obj);
7126
+ }
7127
+ /**
7128
+ * Registers a path alias.
7129
+ * This is used when a variable is assigned to reference an existing reactive object.
7130
+ * For example, when `editingNestedStep = routes[0].steps[0]`, this creates an alias
7131
+ * so that changes to "routes[0].steps[0].steps" also match "editingNestedStep.steps".
7132
+ *
7133
+ * @param aliasPath The alias path (e.g., "editingNestedStep")
7134
+ * @param sourcePath The source path (e.g., "routes[0].steps[0]")
7135
+ */
7136
+ static registerPathAlias(aliasPath, sourcePath) {
7137
+ if (aliasPath && sourcePath && aliasPath !== sourcePath) {
7138
+ this.pathAliases.set(aliasPath, sourcePath);
7139
+ }
7140
+ }
7141
+ /**
7142
+ * Unregisters a path alias.
7143
+ *
7144
+ * @param aliasPath The alias path to remove
7145
+ */
7146
+ static unregisterPathAlias(aliasPath) {
7147
+ // Remove all aliases that start with the given path
7148
+ for (const key of this.pathAliases.keys()) {
7149
+ if (key === aliasPath || key.startsWith(aliasPath + '.') || key.startsWith(aliasPath + '[')) {
7150
+ this.pathAliases.delete(key);
7151
+ }
7152
+ }
7153
+ }
7154
+ /**
7155
+ * Resolves an alias path to its source path.
7156
+ * Also handles nested paths (e.g., "editingNestedStep.steps" -> "routes[0].steps[0].steps").
7157
+ *
7158
+ * @param aliasPath The alias path to resolve
7159
+ * @returns The source path, or undefined if no alias exists
7160
+ */
7161
+ static resolvePathAlias(aliasPath) {
7162
+ // Direct match
7163
+ if (this.pathAliases.has(aliasPath)) {
7164
+ return this.pathAliases.get(aliasPath);
7165
+ }
7166
+ // Check if this is a nested path of an aliased variable
7167
+ // e.g., "editingNestedStep.steps" when "editingNestedStep" is aliased
7168
+ for (const [alias, source] of this.pathAliases.entries()) {
7169
+ if (aliasPath.startsWith(alias + '.') || aliasPath.startsWith(alias + '[')) {
7170
+ const suffix = aliasPath.substring(alias.length);
7171
+ return source + suffix;
7172
+ }
7173
+ }
7174
+ return undefined;
7175
+ }
7176
+ /**
7177
+ * Checks if a change path matches an identifier, considering path aliases.
7178
+ * For example, if "editingNestedStep" is aliased to "routes[0].steps[0]",
7179
+ * then a change to "routes[0].steps[0].steps" should match "editingNestedStep.steps".
7180
+ *
7181
+ * @param changePath The path that was changed (e.g., "routes[0].steps[0].steps")
7182
+ * @param identifier The identifier to check (e.g., "editingNestedStep.steps")
7183
+ * @returns True if the change path matches the identifier
7184
+ */
7185
+ static doesChangeMatchIdentifier(changePath, identifier) {
7186
+ // Direct match
7187
+ if (this.pathMatches(changePath, identifier)) {
7188
+ return true;
7189
+ }
7190
+ // Check if the change path is the resolved identifier or lies below it
7191
+ // (handles cases like array item changes)
7192
+ const resolvedIdentifier = this.resolvePathAlias(identifier);
7193
+ if (resolvedIdentifier && this.pathMatches(changePath, resolvedIdentifier, true)) {
7194
+ return true;
7195
+ }
7196
+ return false;
7197
+ }
7198
+ /**
7199
+ * Checks whether a change path names the location an identifier refers to.
7200
+ * A "[*]" index in the identifier (e.g. "items[*].name" for `items[i].name`)
7201
+ * stands for an index known only when the expression runs and matches any
7202
+ * one index or key.
7203
+ *
7204
+ * @param changePath The path that was changed (e.g., "items[2].name")
7205
+ * @param identifier The identifier to check (e.g., "items[*].name")
7206
+ * @param descendants Whether a change below the identifier also matches
7207
+ * @returns True if the change path matches the identifier
7208
+ */
7209
+ static pathMatches(changePath, identifier, descendants = false) {
7210
+ if (changePath === identifier) {
7211
+ return true;
7212
+ }
7213
+ if (!identifier.includes('[*]')) {
7214
+ return descendants && (changePath.startsWith(identifier + '.') || changePath.startsWith(identifier + '['));
7215
+ }
7216
+ const cacheKey = descendants ? identifier + ' ' : identifier;
7217
+ let pattern = this.pathPatterns.get(cacheKey);
7218
+ if (!pattern) {
7219
+ const source = identifier
7220
+ .split('[*]')
7221
+ .map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
7222
+ .join('(?:\\[[^\\]]*\\]|\\.[^.[]+)');
7223
+ pattern = new RegExp(`^${source}${descendants ? '(?:[.[].*)?' : ''}$`);
7224
+ this.pathPatterns.set(cacheKey, pattern);
7225
+ }
7226
+ return pattern.test(changePath);
7227
+ }
7228
+ }
7229
+
6729
7230
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
6730
7231
  /**
6731
7232
  * Utility class for analyzing JavaScript expressions to extract variable and function dependencies.
@@ -6760,28 +7261,28 @@ class ExpressionUtils {
6760
7261
  }
6761
7262
  }
6762
7263
  else if (node.type === 'MemberExpression') {
6763
- // Reconstruct chain parts from a MemberExpression (e.g. a.b.c)
7264
+ // Reconstruct chain parts from a MemberExpression (e.g. a.b.c). Each part
7265
+ // carries its own separator so that the chain is spelled the way the reactive
7266
+ // proxy reports changes: array indices in brackets, properties after a dot
7267
+ // (e.g. "items[0].name").
6764
7268
  const parts = [];
6765
7269
  let cur = node;
6766
- let stop = false;
6767
- while (cur && !stop) {
7270
+ while (cur) {
6768
7271
  // property part (right side)
6769
7272
  if (cur.property) {
6770
7273
  if (!cur.computed && cur.property.type === 'Identifier') {
6771
- parts.unshift(cur.property.name);
7274
+ parts.unshift(`.${cur.property.name}`);
6772
7275
  }
6773
7276
  else if (cur.computed && cur.property.type === 'Literal') {
6774
- parts.unshift(String(cur.property.value));
6775
- }
6776
- else if (cur.computed && cur.property.type === 'Identifier') {
6777
- // e.g. obj[prop] -> include the prop identifier (it will also be added separately)
6778
- parts.unshift(cur.property.name);
7277
+ // items[0] and items["0"] both name an index; labels['a-b'] names a key.
7278
+ const key = String(cur.property.value);
7279
+ parts.unshift(ReactiveProxy.isIndexKey(key) ? `[${key}]` : `.${key}`);
6779
7280
  }
6780
- else {
6781
- // unknown property shape (e.g. expression) — stop adding chain here
6782
- // inner expressions will be visited separately by walk.full
6783
- stop = true;
6784
- break;
7281
+ else if (cur.computed) {
7282
+ // e.g. items[i] or items[i + 1]: the index is only known when the
7283
+ // expression runs, so "[*]" stands for any index or key. Identifiers
7284
+ // inside the index expression are visited separately by walk.full.
7285
+ parts.unshift('[*]');
6785
7286
  }
6786
7287
  }
6787
7288
  // object part (left side)
@@ -6801,7 +7302,6 @@ class ExpressionUtils {
6801
7302
  }
6802
7303
  else {
6803
7304
  // other object types (CallExpression, etc.) — stop here
6804
- stop = true;
6805
7305
  break;
6806
7306
  }
6807
7307
  }
@@ -6809,10 +7309,15 @@ class ExpressionUtils {
6809
7309
  break;
6810
7310
  }
6811
7311
  }
6812
- if (parts.length > 0) {
6813
- // Add progressive chains: 'a', 'a.b', 'a.b.c'
7312
+ // A chain that does not start at a name (e.g. `this.a.b` or `f().b`) begins
7313
+ // with its first property; one that begins with an index names no binding.
7314
+ if (parts.length > 0 && parts[0].startsWith('.')) {
7315
+ parts[0] = parts[0].substring(1);
7316
+ }
7317
+ if (parts.length > 0 && !parts[0].startsWith('[')) {
7318
+ // Add progressive chains: 'a', 'a.b', 'a.b[0]', 'a.b[0].c'
6814
7319
  for (let i = 0; i < parts.length; i++) {
6815
- const chain = parts.slice(0, i + 1).join('.');
7320
+ const chain = parts.slice(0, i + 1).join('');
6816
7321
  identifiers.add(chain);
6817
7322
  // Also apply functionDependencies lookup for the chain key
6818
7323
  if (functionDependencies[chain]) {
@@ -6826,6 +7331,16 @@ class ExpressionUtils {
6826
7331
  });
6827
7332
  return Array.from(identifiers);
6828
7333
  }
7334
+ /**
7335
+ * Returns the binding name an identifier chain starts with
7336
+ * (e.g. "items" for "items[0].name" or "user.name").
7337
+ * @param identifier An identifier or member chain returned by extractIdentifiers.
7338
+ * @returns The left-most name of the chain.
7339
+ */
7340
+ static baseIdentifier(identifier) {
7341
+ const end = identifier.search(/[.[]/);
7342
+ return end === -1 ? identifier : identifier.substring(0, end);
7343
+ }
6829
7344
  /**
6830
7345
  * Analyzes the dependencies of functions in the provided dictionary.
6831
7346
  * @param functions A dictionary mapping function names to their implementations.
@@ -7112,718 +7627,336 @@ class ExpressionUtils {
7112
7627
  return;
7113
7628
  }
7114
7629
  // Skip identifiers that are the property (not the object) of a non-computed
7115
- // member access (e.g., the `prop` in `obj.prop`).
7116
- if (parentNode?.type === 'MemberExpression' && !parentNode.computed && parentNode.property === node) {
7117
- return;
7118
- }
7119
- // Add to replacements list (adjust for the wrapping parentheses in expression mode)
7120
- replacements.push({
7121
- start: node.start - offsetShift,
7122
- end: node.end - offsetShift,
7123
- name: node.name
7124
- });
7125
- });
7126
- // Sort replacements by start position (descending) to replace from end to start
7127
- replacements.sort((a, b) => b.start - a.start);
7128
- // Apply replacements
7129
- let result = expression;
7130
- for (const replacement of replacements) {
7131
- const insertion = replacement.asThisAlias ? 'this' : `this.${replacement.name}`;
7132
- result = result.substring(0, replacement.start) + insertion + result.substring(replacement.end);
7133
- }
7134
- return result;
7135
- }
7136
- catch (error) {
7137
- // If AST parsing fails, fall back to the original expression
7138
- console.warn('Failed to rewrite expression:', expression, error);
7139
- return expression;
7140
- }
7141
- }
7142
- }
7143
-
7144
- // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7145
- /**
7146
- * A centralized expression evaluator that normalizes expression evaluation across all directives.
7147
- *
7148
- * This class provides:
7149
- * - Consistent identifier extraction and binding resolution
7150
- * - Unified global object whitelisting (Math, Date, JSON, etc.)
7151
- * - Expression caching for performance
7152
- * - Centralized error handling
7153
- * - Support for custom expression rewriting (for VOnDirective)
7154
- *
7155
- * Example usage:
7156
- * ```typescript
7157
- * const evaluator = ExpressionEvaluator.create(
7158
- * "count + 1",
7159
- * bindings,
7160
- * functionDependencies
7161
- * );
7162
- * const result = evaluator.evaluate();
7163
- * ```
7164
- */
7165
- class ExpressionEvaluator {
7166
- /**
7167
- * Cache for compiled expression functions.
7168
- * Key: expression string
7169
- * Value: compiled function
7170
- */
7171
- static cache = new Map();
7172
- /**
7173
- * Global objects that are whitelisted for use in expressions.
7174
- * These objects are available to all expressions for convenience and compatibility.
7175
- */
7176
- static GLOBAL_WHITELIST = {
7177
- Math,
7178
- Date,
7179
- JSON,
7180
- Array,
7181
- Object,
7182
- String,
7183
- Number,
7184
- Boolean,
7185
- parseInt,
7186
- parseFloat,
7187
- isNaN,
7188
- isFinite,
7189
- encodeURI,
7190
- decodeURI,
7191
- encodeURIComponent,
7192
- decodeURIComponent
7193
- };
7194
- /**
7195
- * The original expression string.
7196
- */
7197
- expression;
7198
- /**
7199
- * The identifiers extracted from the expression.
7200
- */
7201
- identifiers;
7202
- // The original extracted identifiers (may include member chains like 'a.b')
7203
- originalIdentifiers;
7204
- /**
7205
- * The bindings to use for evaluating the expression.
7206
- */
7207
- bindings;
7208
- /**
7209
- * The compiled function for evaluating the expression.
7210
- */
7211
- evaluatorFunc;
7212
- /**
7213
- * Additional context variables (e.g., 'event', '$ctx').
7214
- */
7215
- additionalContext;
7216
- /**
7217
- * Private constructor. Use ExpressionEvaluator.create() to create instances.
7218
- */
7219
- constructor(expression, bindings, identifiers, originalIdentifiers, evaluatorFunc, additionalContext) {
7220
- this.expression = expression;
7221
- this.bindings = bindings;
7222
- this.identifiers = identifiers;
7223
- this.originalIdentifiers = originalIdentifiers;
7224
- this.evaluatorFunc = evaluatorFunc;
7225
- this.additionalContext = additionalContext;
7226
- }
7227
- /**
7228
- * Creates an ExpressionEvaluator instance.
7229
- *
7230
- * @param expression The expression string to evaluate.
7231
- * @param bindings The bindings to use for evaluating the expression.
7232
- * @param functionDependencies A dictionary mapping function names to their dependencies.
7233
- * @param options Optional configuration for the evaluator.
7234
- * @returns An ExpressionEvaluator instance.
7235
- */
7236
- static create(expression, bindings, functionDependencies, options) {
7237
- // Extract identifiers from the expression (may include member chains like 'a.b')
7238
- const extractedIdentifiers = ExpressionUtils.extractIdentifiers(expression, functionDependencies);
7239
- // For compilation we must only pass simple identifier names (no dots).
7240
- // Use the base (left-most) segment of member chains and preserve order.
7241
- const baseIdentifiers = [];
7242
- for (const id of extractedIdentifiers) {
7243
- const base = id.split('.')[0];
7244
- if (!baseIdentifiers.includes(base)) {
7245
- baseIdentifiers.push(base);
7246
- }
7247
- }
7248
- // Apply custom rewrite if provided (used by VOnDirective)
7249
- let processedExpression = expression;
7250
- if (options?.rewriteExpression) {
7251
- // Pass the original extracted identifiers (may include member chains)
7252
- processedExpression = options.rewriteExpression(expression, extractedIdentifiers);
7253
- }
7254
- // Build cache key including options that affect compilation
7255
- const cacheKey = JSON.stringify({
7256
- expr: processedExpression,
7257
- additionalCtx: options?.additionalContext ? Object.keys(options.additionalContext).sort() : []
7258
- });
7259
- // Check cache
7260
- let evaluatorFunc = this.cache.get(cacheKey);
7261
- if (!evaluatorFunc) {
7262
- // Compile the expression using base identifiers (no dots)
7263
- evaluatorFunc = this.compileExpression(processedExpression, baseIdentifiers, options);
7264
- // Cache the compiled function
7265
- this.cache.set(cacheKey, evaluatorFunc);
7266
- }
7267
- return new ExpressionEvaluator(expression, bindings, baseIdentifiers, extractedIdentifiers, evaluatorFunc, options?.additionalContext);
7268
- }
7269
- /**
7270
- * Compiles an expression into a function.
7271
- *
7272
- * @param expression The expression string to compile.
7273
- * @param identifiers The identifiers extracted from the expression.
7274
- * @param options Optional configuration.
7275
- * @returns A compiled function.
7276
- */
7277
- static compileExpression(expression, identifiers, options) {
7278
- // Build parameter list: globals first, then identifiers (base names), then additional context
7279
- // Filter out identifiers that are already in the global whitelist to avoid duplicate parameters
7280
- const globalKeys = new Set(Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST));
7281
- const filteredIdentifiers = identifiers.filter(id => !globalKeys.has(id));
7282
- const params = [
7283
- ...Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST),
7284
- ...filteredIdentifiers
7285
- ];
7286
- // Add additional context parameters if provided
7287
- if (options?.additionalContext) {
7288
- params.push(...Object.keys(options.additionalContext));
7289
- }
7290
- const args = params.join(", ");
7291
- const funcBody = `return (${expression});`;
7292
- try {
7293
- // Create the function with error handling
7294
- return new Function(args, funcBody);
7295
- }
7296
- catch (error) {
7297
- throw new Error(`Failed to compile expression: ${expression}\nError: ${error}`);
7298
- }
7299
- }
7300
- /**
7301
- * Evaluates the expression with the current bindings.
7302
- *
7303
- * @returns The result of the expression evaluation.
7304
- */
7305
- evaluate() {
7306
- try {
7307
- // Build arguments: globals first, then identifiers, then additional context
7308
- // Filter out identifiers that are already in the global whitelist to match compileExpression
7309
- const globalKeys = new Set(Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST));
7310
- const filteredIdentifiers = this.identifiers.filter(id => !globalKeys.has(id));
7311
- const values = [
7312
- // Global whitelist values
7313
- ...Object.values(ExpressionEvaluator.GLOBAL_WHITELIST),
7314
- // Identifier values from bindings (excluding globals)
7315
- ...filteredIdentifiers.map(id => this.bindings.get(id))
7316
- ];
7317
- // Add additional context values if provided
7318
- if (this.additionalContext) {
7319
- values.push(...Object.values(this.additionalContext));
7630
+ // member access (e.g., the `prop` in `obj.prop`).
7631
+ if (parentNode?.type === 'MemberExpression' && !parentNode.computed && parentNode.property === node) {
7632
+ return;
7633
+ }
7634
+ // Add to replacements list (adjust for the wrapping parentheses in expression mode)
7635
+ replacements.push({
7636
+ start: node.start - offsetShift,
7637
+ end: node.end - offsetShift,
7638
+ name: node.name
7639
+ });
7640
+ });
7641
+ // Sort replacements by start position (descending) to replace from end to start
7642
+ replacements.sort((a, b) => b.start - a.start);
7643
+ // Apply replacements
7644
+ let result = expression;
7645
+ for (const replacement of replacements) {
7646
+ const insertion = replacement.asThisAlias ? 'this' : `this.${replacement.name}`;
7647
+ result = result.substring(0, replacement.start) + insertion + result.substring(replacement.end);
7320
7648
  }
7321
- // Call the compiled function with the gathered values
7322
- return this.evaluatorFunc(...values);
7649
+ return result;
7323
7650
  }
7324
7651
  catch (error) {
7325
- console.error(`[ichigo.js] Error evaluating expression: ${this.expression}`, error);
7326
- throw error;
7652
+ // If AST parsing fails, fall back to the original expression
7653
+ console.warn('Failed to rewrite expression:', expression, error);
7654
+ return expression;
7327
7655
  }
7328
7656
  }
7329
- /**
7330
- * Evaluates the expression and returns the result as a boolean.
7331
- *
7332
- * @returns The boolean result of the expression evaluation.
7333
- */
7334
- evaluateAsBoolean() {
7335
- return Boolean(this.evaluate());
7336
- }
7337
- /**
7338
- * Gets the list of identifiers extracted from the expression.
7339
- */
7340
- get dependentIdentifiers() {
7341
- // Return the original extracted identifiers (may include member chains like 'a.b')
7342
- return this.originalIdentifiers;
7343
- }
7344
- /**
7345
- * Clears the expression cache.
7346
- * This should be called sparingly, as it will force recompilation of all expressions.
7347
- */
7348
- static clearCache() {
7349
- this.cache.clear();
7350
- }
7351
7657
  }
7352
7658
 
7353
7659
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7354
7660
  /**
7355
- * Directive for binding dynamic attributes to DOM elements.
7356
- * The `v-bind` directive allows you to bind HTML attributes to expressions in your data model.
7357
- * The syntax for using the `v-bind` directive is `v-bind:attribute="expression"`, where `attribute` is the name of the HTML attribute you want to bind (e.g., `src`, `href`, `class`, etc.), and `expression` is a JavaScript expression that evaluates to the value you want to assign to that attribute.
7661
+ * A centralized expression evaluator that normalizes expression evaluation across all directives.
7662
+ *
7663
+ * This class provides:
7664
+ * - Consistent identifier extraction and binding resolution
7665
+ * - Unified global object whitelisting (Math, Date, JSON, etc.)
7666
+ * - Expression caching for performance
7667
+ * - Centralized error handling
7668
+ * - Support for custom expression rewriting (for VOnDirective)
7669
+ *
7358
7670
  * Example usage:
7359
- * <img v-bind:src="imageSrc" />
7360
- * The value of `imageSrc` should be defined in the component's data object.
7361
- * When the value of `imageSrc` changes, the `src` attribute of the `<img>` element will automatically update to reflect the new value.
7362
- * This directive is particularly useful for dynamically updating attributes based on user interactions or other data changes in your application.
7363
- * It helps keep the DOM in sync with the underlying data model, making it easier to build reactive user interfaces.
7364
- * The `v-bind` directive can also be used with shorthand syntax using a colon (:).
7365
- * For example, `:src="imageSrc"` is equivalent to `v-bind:src="imageSrc"`.
7366
- * This shorthand syntax is often used for brevity and improved readability in templates.
7367
- * Overall, the `v-bind` directive is a powerful tool for creating dynamic and responsive web applications by allowing seamless integration between the data model and the DOM.
7671
+ * ```typescript
7672
+ * const evaluator = ExpressionEvaluator.create(
7673
+ * "count + 1",
7674
+ * bindings,
7675
+ * functionDependencies
7676
+ * );
7677
+ * const result = evaluator.evaluate();
7678
+ * ```
7368
7679
  */
7369
- class VBindDirective {
7370
- /**
7371
- * The virtual node to which this directive is applied.
7372
- */
7373
- #vNode;
7374
- /**
7375
- * The expression evaluator for this directive.
7376
- */
7377
- #evaluator;
7378
- /**
7379
- * The name of the attribute to bind (e.g., "src", "class", "style").
7380
- */
7381
- #attributeName;
7382
- /**
7383
- * The original expression string from the directive.
7384
- */
7385
- #expression;
7386
- /**
7387
- * The set of class names managed by this directive (used when binding to the "class" attribute).
7388
- * This helps in tracking which classes were added by this directive to avoid conflicts with other class manipulations.
7389
- */
7390
- #managedClasses = new Set();
7391
- /**
7392
- * The set of style properties managed by this directive (used when binding to the "style" attribute).
7393
- * This helps in tracking which styles were added by this directive to avoid conflicts with other style manipulations.
7394
- */
7395
- #managedStyles = new Set();
7396
- /**
7397
- * @param context The context for parsing the directive.
7398
- */
7399
- constructor(context) {
7400
- this.#vNode = context.vNode;
7401
- // Extract the attribute name from the directive
7402
- // e.g., "v-bind:src" -> "src", ":href" -> "href"
7403
- const attrName = context.attribute.name;
7404
- if (attrName.startsWith('v-bind:')) {
7405
- this.#attributeName = attrName.substring(7);
7406
- }
7407
- else if (attrName.startsWith(':')) {
7408
- this.#attributeName = attrName.substring(1);
7409
- }
7410
- // Parse the expression and create the evaluator
7411
- this.#expression = context.attribute.value;
7412
- if (this.#expression && context.vNode.bindings) {
7413
- this.#evaluator = ExpressionEvaluator.create(this.#expression, context.vNode.bindings, context.vNode.vApplication.functionDependencies);
7414
- }
7415
- // Remove the directive attribute from the element
7416
- this.#vNode.node.removeAttribute(context.attribute.name);
7417
- }
7418
- /**
7419
- * @inheritdoc
7420
- */
7421
- get name() {
7422
- return StandardDirectiveName.V_BIND;
7423
- }
7424
- /**
7425
- * @inheritdoc
7426
- */
7427
- get vNode() {
7428
- return this.#vNode;
7429
- }
7430
- /**
7431
- * @inheritdoc
7432
- */
7433
- get needsAnchor() {
7434
- return false;
7435
- }
7436
- /**
7437
- * @inheritdoc
7438
- */
7439
- get bindingsPreparer() {
7440
- return undefined;
7441
- }
7442
- /**
7443
- * @inheritdoc
7444
- */
7445
- get domUpdater() {
7446
- const identifiers = this.#evaluator?.dependentIdentifiers ?? [];
7447
- // Create an updater that handles the attribute binding
7448
- const updater = {
7449
- get dependentIdentifiers() {
7450
- return identifiers;
7451
- },
7452
- applyToDOM: () => {
7453
- this.#render();
7454
- }
7455
- };
7456
- return updater;
7457
- }
7458
- /**
7459
- * @inheritdoc
7460
- */
7461
- get templatize() {
7462
- return false;
7463
- }
7464
- /**
7465
- * @inheritdoc
7466
- */
7467
- get dependentIdentifiers() {
7468
- return this.#evaluator?.dependentIdentifiers ?? [];
7469
- }
7470
- /**
7471
- * Indicates if this directive is binding the "key" attribute.
7472
- * The "key" attribute is special and is used for optimizing rendering of lists.
7473
- * If this directive is binding the "key" attribute, it will be handled by the VForDirective.
7474
- */
7475
- get isKey() {
7476
- return (this.#attributeName === 'key');
7477
- }
7478
- /**
7479
- * Indicates if this directive is binding the "options" attribute or any of its sub-properties (e.g., "options.intersection").
7480
- * The "options" attribute is special and is used for passing options to certain directives like VIntersectionDirective.
7481
- */
7482
- get isOptions() {
7483
- return (this.#attributeName === 'options' || this.#attributeName?.startsWith('options.') === true);
7484
- }
7485
- /**
7486
- * Gets the name of the attribute being bound (e.g., "src", "class", "options", "options.intersection").
7487
- */
7488
- get attributeName() {
7489
- return this.#attributeName;
7490
- }
7491
- /**
7492
- * Gets the original expression string from the directive.
7493
- */
7494
- get expression() {
7495
- return this.#expression;
7496
- }
7497
- /**
7498
- * Evaluates the bound expression and returns the current value.
7499
- * Returns undefined when no evaluator is available (e.g., empty expression).
7500
- */
7501
- evaluate() {
7502
- return this.#evaluator?.evaluate();
7503
- }
7504
- /**
7505
- * @inheritdoc
7506
- */
7507
- get onMount() {
7508
- // A constant expression (e.g. :label="'Apple'") has no reactive
7509
- // identifiers, so the identifier-driven update cycle never applies it.
7510
- // Render it once at mount instead.
7511
- if (this.#evaluator && this.#evaluator.dependentIdentifiers.length === 0) {
7512
- return () => {
7513
- this.#render();
7514
- };
7515
- }
7516
- return undefined;
7517
- }
7680
+ class ExpressionEvaluator {
7518
7681
  /**
7519
- * @inheritdoc
7682
+ * Cache for compiled expression functions.
7683
+ * Key: expression string
7684
+ * Value: compiled function
7520
7685
  */
7521
- get onMounted() {
7522
- return undefined;
7523
- }
7686
+ static cache = new Map();
7524
7687
  /**
7525
- * @inheritdoc
7688
+ * Global objects that are whitelisted for use in expressions.
7689
+ * These objects are available to all expressions for convenience and compatibility.
7526
7690
  */
7527
- get onUpdate() {
7528
- return undefined;
7529
- }
7691
+ static GLOBAL_WHITELIST = {
7692
+ Math,
7693
+ Date,
7694
+ JSON,
7695
+ Array,
7696
+ Object,
7697
+ String,
7698
+ Number,
7699
+ Boolean,
7700
+ parseInt,
7701
+ parseFloat,
7702
+ isNaN,
7703
+ isFinite,
7704
+ encodeURI,
7705
+ decodeURI,
7706
+ encodeURIComponent,
7707
+ decodeURIComponent
7708
+ };
7530
7709
  /**
7531
- * @inheritdoc
7710
+ * The original expression string.
7532
7711
  */
7533
- get onUpdated() {
7534
- return undefined;
7535
- }
7712
+ expression;
7536
7713
  /**
7537
- * @inheritdoc
7714
+ * The identifiers extracted from the expression.
7538
7715
  */
7539
- get onUnmount() {
7540
- return undefined;
7541
- }
7716
+ identifiers;
7717
+ // The original extracted identifiers (may include member chains like 'a.b')
7718
+ originalIdentifiers;
7542
7719
  /**
7543
- * @inheritdoc
7720
+ * The bindings to use for evaluating the expression.
7544
7721
  */
7545
- get onUnmounted() {
7546
- return undefined;
7547
- }
7722
+ bindings;
7548
7723
  /**
7549
- * @inheritdoc
7724
+ * The compiled function for evaluating the expression.
7550
7725
  */
7551
- destroy() {
7552
- // Do nothing. No special cleanup needed.
7553
- }
7726
+ evaluatorFunc;
7554
7727
  /**
7555
- * Renders the bound attribute by evaluating the expression and updating the DOM element.
7728
+ * Additional context variables (e.g., 'event', '$ctx').
7556
7729
  */
7557
- #render() {
7558
- // Do nothing for special attributes
7559
- if (this.isKey || this.isOptions) {
7560
- return;
7561
- }
7562
- const element = this.#vNode.node;
7563
- const attributeName = this.#attributeName;
7564
- // If there's no evaluator or attribute name, do nothing
7565
- if (!this.#evaluator || !attributeName) {
7566
- return;
7567
- }
7568
- // Evaluate the expression to get the value
7569
- const value = this.#evaluator.evaluate();
7570
- // Handle different attribute types
7571
- if (attributeName === 'class') {
7572
- this.#updateClass(element, value);
7573
- }
7574
- else if (attributeName === 'style') {
7575
- this.#updateStyle(element, value);
7576
- }
7577
- else if (this.#isCustomElementProperty(element, attributeName, value)) {
7578
- // Custom elements: set objects/arrays (and declared props) as properties
7579
- // so complex values are not serialized to strings via setAttribute.
7580
- // HTML attributes are lowercased by the browser, so resolve to the
7581
- // actual camelCase property name declared on the custom element.
7582
- const propName = this.#resolveCustomElementPropertyName(element, attributeName);
7583
- element[propName] = value;
7584
- }
7585
- else if (this.#isDOMProperty(attributeName)) {
7586
- this.#updateProperty(element, attributeName, value);
7587
- }
7588
- else if (this.#isBooleanAttribute(attributeName)) {
7589
- this.#updateBooleanAttribute(element, attributeName, value);
7590
- }
7591
- else {
7592
- this.#updateAttribute(element, attributeName, value);
7593
- }
7594
- }
7730
+ additionalContext;
7595
7731
  /**
7596
- * Updates the class attribute with support for string, array, and object formats.
7732
+ * Whether any identifier has an index known only at run time ("items[*].name").
7733
+ * Only then are the paths read during evaluation worth recording.
7597
7734
  */
7598
- #updateClass(element, value) {
7599
- // Determine the new set of classes to apply
7600
- let newClasses = [];
7601
- if (typeof value === 'string') {
7602
- newClasses = value.split(/\s+/).filter(Boolean);
7603
- }
7604
- else if (Array.isArray(value)) {
7605
- // Flatten array elements that may contain strings or objects
7606
- newClasses = value
7607
- .filter(Boolean)
7608
- .flatMap(cls => {
7609
- if (typeof cls === 'string') {
7610
- return cls.split(/\s+/).filter(Boolean);
7611
- }
7612
- else if (typeof cls === 'object' && cls !== null) {
7613
- // Handle object format within array: { className: condition }
7614
- // Keys may contain space-separated class names
7615
- return Object.keys(cls).filter(key => cls[key]).flatMap(key => key.split(/\s+/).filter(Boolean));
7616
- }
7617
- return [];
7618
- });
7619
- }
7620
- else if (typeof value === 'object' && value !== null) {
7621
- // Keys may contain space-separated class names
7622
- newClasses = Object.keys(value).filter(key => value[key]).flatMap(key => key.split(/\s+/).filter(Boolean));
7623
- }
7624
- // Remove previously managed classes
7625
- this.#managedClasses.forEach(cls => element.classList.remove(cls));
7626
- // Add newly managed classes
7627
- newClasses.forEach(cls => element.classList.add(cls));
7628
- // Update managed classes list
7629
- this.#managedClasses = new Set(newClasses);
7630
- }
7735
+ hasDynamicIndex;
7631
7736
  /**
7632
- * Updates the style attribute with support for object format.
7737
+ * The source paths read through reactive proxies during the last evaluation,
7738
+ * or undefined before the first evaluation and when nothing is recorded.
7633
7739
  */
7634
- #updateStyle(element, value) {
7635
- let newStyles = [];
7636
- if (typeof value === 'string') {
7637
- // Directly set the style string
7638
- element.style.cssText = value;
7639
- // Extract managed properties
7640
- newStyles = value.split(';').map(s => s.split(':')[0].trim()).filter(Boolean);
7641
- }
7642
- else if (typeof value === 'object' && value !== null) {
7643
- // Remove all previously managed properties
7644
- this.#managedStyles.forEach(prop => {
7645
- element.style.removeProperty(this.#camelToKebab(prop));
7646
- });
7647
- // Add newly managed properties
7648
- for (const key in value) {
7649
- if (Object.prototype.hasOwnProperty.call(value, key)) {
7650
- const cssKey = this.#camelToKebab(key);
7651
- const cssValue = value[key];
7652
- if (cssValue != null) {
7653
- element.style.setProperty(cssKey, String(cssValue));
7654
- newStyles.push(key);
7655
- }
7656
- }
7657
- }
7658
- }
7659
- // Update managed properties list
7660
- this.#managedStyles = new Set(newStyles);
7661
- }
7740
+ trackedPathSet;
7662
7741
  /**
7663
- * Converts camelCase to kebab-case for CSS properties.
7742
+ * Private constructor. Use ExpressionEvaluator.create() to create instances.
7664
7743
  */
7665
- #camelToKebab(str) {
7666
- return str.replace(/([A-Z])/g, '-$1').toLowerCase();
7744
+ constructor(expression, bindings, identifiers, originalIdentifiers, evaluatorFunc, additionalContext) {
7745
+ this.expression = expression;
7746
+ this.bindings = bindings;
7747
+ this.identifiers = identifiers;
7748
+ this.originalIdentifiers = originalIdentifiers;
7749
+ this.evaluatorFunc = evaluatorFunc;
7750
+ this.additionalContext = additionalContext;
7751
+ this.hasDynamicIndex = originalIdentifiers.some(id => id.includes('[*]'));
7667
7752
  }
7668
7753
  /**
7669
- * Returns true when the target is a custom element and the binding should be
7670
- * delivered as a property rather than an HTML attribute.
7754
+ * Creates an ExpressionEvaluator instance.
7671
7755
  *
7672
- * Three conditions trigger property delivery:
7673
- * 1. The value is an object or array — serialising these to a string attribute
7674
- * would lose type information.
7675
- * 2. The element exposes a matching property accessor (e.g. a prop declared via
7676
- * defineComponent), even when the value is a primitive.
7677
- * 3. The element declares a matching prop in its static _props (case-insensitive
7678
- * and kebab-case → camelCase tolerant), so primitive values reach the
7679
- * reactive binding via the generated setter instead of being lost as a
7680
- * plain HTML attribute that no attributeChangedCallback observes.
7756
+ * @param expression The expression string to evaluate.
7757
+ * @param bindings The bindings to use for evaluating the expression.
7758
+ * @param functionDependencies A dictionary mapping function names to their dependencies.
7759
+ * @param options Optional configuration for the evaluator.
7760
+ * @returns An ExpressionEvaluator instance.
7681
7761
  */
7682
- #isCustomElementProperty(element, name, value) {
7683
- if (!element.tagName.includes('-')) {
7684
- return false;
7762
+ static create(expression, bindings, functionDependencies, options) {
7763
+ // Extract identifiers from the expression (may include member chains like 'a.b')
7764
+ const extractedIdentifiers = ExpressionUtils.extractIdentifiers(expression, functionDependencies);
7765
+ // For compilation we must only pass simple identifier names (no dots).
7766
+ // Use the base (left-most) segment of member chains and preserve order.
7767
+ const baseIdentifiers = [];
7768
+ for (const id of extractedIdentifiers) {
7769
+ const base = ExpressionUtils.baseIdentifier(id);
7770
+ if (!baseIdentifiers.includes(base)) {
7771
+ baseIdentifiers.push(base);
7772
+ }
7685
7773
  }
7686
- const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
7687
- if (isObjectOrArray || name in element) {
7688
- return true;
7774
+ // Apply custom rewrite if provided (used by VOnDirective)
7775
+ let processedExpression = expression;
7776
+ if (options?.rewriteExpression) {
7777
+ // Pass the original extracted identifiers (may include member chains)
7778
+ processedExpression = options.rewriteExpression(expression, extractedIdentifiers);
7689
7779
  }
7690
- return this.#findDeclaredProp(element, name) !== undefined;
7691
- }
7692
- /**
7693
- * Resolves the actual property name on a custom element for a given
7694
- * (lowercased) HTML attribute name. HTML attributes are always lowercase,
7695
- * but custom element props are typically camelCase. This method checks the
7696
- * element's declared _props (set by defineComponent) for a case-insensitive
7697
- * match against both the raw attribute name and its kebab-case → camelCase
7698
- * conversion (so `user-name` resolves to `userName`), falling back to the
7699
- * original name when no match is found.
7700
- */
7701
- #resolveCustomElementPropertyName(element, name) {
7702
- // Fast path: exact match already exists
7703
- if (name in element) {
7704
- return name;
7780
+ // Build cache key including options that affect compilation
7781
+ const cacheKey = JSON.stringify({
7782
+ expr: processedExpression,
7783
+ additionalCtx: options?.additionalContext ? Object.keys(options.additionalContext).sort() : []
7784
+ });
7785
+ // Check cache
7786
+ let evaluatorFunc = this.cache.get(cacheKey);
7787
+ if (!evaluatorFunc) {
7788
+ // Compile the expression using base identifiers (no dots)
7789
+ evaluatorFunc = this.compileExpression(processedExpression, baseIdentifiers, options);
7790
+ // Cache the compiled function
7791
+ this.cache.set(cacheKey, evaluatorFunc);
7705
7792
  }
7706
- return this.#findDeclaredProp(element, name) ?? name;
7793
+ return new ExpressionEvaluator(expression, bindings, baseIdentifiers, extractedIdentifiers, evaluatorFunc, options?.additionalContext);
7707
7794
  }
7708
7795
  /**
7709
- * Looks up a declared prop on a custom element constructor whose name matches
7710
- * the given attribute name, treating the comparison as case-insensitive and
7711
- * tolerant of kebab-case → camelCase conversion. Returns the canonical prop
7712
- * name (as declared) when a match is found, or undefined otherwise.
7796
+ * Compiles an expression into a function.
7797
+ *
7798
+ * @param expression The expression string to compile.
7799
+ * @param identifiers The identifiers extracted from the expression.
7800
+ * @param options Optional configuration.
7801
+ * @returns A compiled function.
7713
7802
  */
7714
- #findDeclaredProp(element, name) {
7715
- const props = element.constructor._props;
7716
- if (!Array.isArray(props)) {
7717
- return undefined;
7803
+ static compileExpression(expression, identifiers, options) {
7804
+ // Build parameter list: globals first, then identifiers (base names), then additional context
7805
+ // Filter out identifiers that are already in the global whitelist to avoid duplicate parameters
7806
+ const globalKeys = new Set(Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST));
7807
+ const filteredIdentifiers = identifiers.filter(id => !globalKeys.has(id));
7808
+ const params = [
7809
+ ...Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST),
7810
+ ...filteredIdentifiers
7811
+ ];
7812
+ // Add additional context parameters if provided
7813
+ if (options?.additionalContext) {
7814
+ params.push(...Object.keys(options.additionalContext));
7815
+ }
7816
+ const args = params.join(", ");
7817
+ const funcBody = `return (${expression});`;
7818
+ try {
7819
+ // Create the function with error handling
7820
+ return new Function(args, funcBody);
7821
+ }
7822
+ catch (error) {
7823
+ throw new Error(`Failed to compile expression: ${expression}\nError: ${error}`);
7718
7824
  }
7719
- const lowerName = name.toLowerCase();
7720
- const camelLower = this.#kebabToCamel(lowerName).toLowerCase();
7721
- return props.find(p => {
7722
- const lowerProp = p.toLowerCase();
7723
- return lowerProp === lowerName || lowerProp === camelLower;
7724
- });
7725
- }
7726
- /**
7727
- * Converts kebab-case to camelCase (e.g. `user-name` → `userName`).
7728
- */
7729
- #kebabToCamel(str) {
7730
- return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
7731
7825
  }
7732
7826
  /**
7733
- * Checks if the attribute should be set as a DOM property.
7827
+ * Evaluates the expression with the current bindings.
7828
+ *
7829
+ * @returns The result of the expression evaluation.
7734
7830
  */
7735
- #isDOMProperty(name) {
7736
- const domProperties = ['value', 'checked', 'selected', 'innerHTML', 'textContent'];
7737
- return domProperties.includes(name);
7831
+ evaluate() {
7832
+ try {
7833
+ // Build arguments: globals first, then identifiers, then additional context
7834
+ // Filter out identifiers that are already in the global whitelist to match compileExpression
7835
+ const globalKeys = new Set(Object.keys(ExpressionEvaluator.GLOBAL_WHITELIST));
7836
+ const filteredIdentifiers = this.identifiers.filter(id => !globalKeys.has(id));
7837
+ const values = [
7838
+ // Global whitelist values
7839
+ ...Object.values(ExpressionEvaluator.GLOBAL_WHITELIST),
7840
+ // Identifier values from bindings (excluding globals)
7841
+ ...filteredIdentifiers.map(id => this.bindings.get(id))
7842
+ ];
7843
+ // Add additional context values if provided
7844
+ if (this.additionalContext) {
7845
+ values.push(...Object.values(this.additionalContext));
7846
+ }
7847
+ // Call the compiled function with the gathered values. When an index is
7848
+ // known only at run time, record which concrete paths this evaluation
7849
+ // read so that change matching can be narrowed to them (see trackedPaths).
7850
+ if (!this.hasDynamicIndex) {
7851
+ return this.evaluatorFunc(...values);
7852
+ }
7853
+ const paths = new Set();
7854
+ this.trackedPathSet = undefined;
7855
+ const result = ReactiveProxy.track(paths, () => this.evaluatorFunc(...values));
7856
+ this.trackedPathSet = paths;
7857
+ return result;
7858
+ }
7859
+ catch (error) {
7860
+ console.error(`[ichigo.js] Error evaluating expression: ${this.expression}`, error);
7861
+ throw error;
7862
+ }
7738
7863
  }
7739
7864
  /**
7740
- * Checks if the attribute is a boolean attribute.
7865
+ * The source paths read through reactive proxies during the last evaluation,
7866
+ * recorded only for an expression with a run-time index ("items[i].name").
7867
+ * Each evaluation replaces the previous set, so a path read for a former
7868
+ * index value is dropped as soon as the expression runs with the new one.
7869
+ * Undefined before the first evaluation and for expressions without such an index.
7741
7870
  */
7742
- #isBooleanAttribute(name) {
7743
- const booleanAttributes = [
7744
- 'disabled', 'readonly', 'required', 'checked', 'selected',
7745
- 'multiple', 'autofocus', 'autoplay', 'controls', 'loop',
7746
- 'muted', 'open', 'hidden'
7747
- ];
7748
- return booleanAttributes.includes(name);
7871
+ get trackedPaths() {
7872
+ return this.trackedPathSet;
7749
7873
  }
7750
7874
  /**
7751
- * Updates a DOM property.
7752
- * For value-like properties (value, textContent, innerHTML), null/undefined is converted to empty string
7753
- * to match Vue.js behavior and prevent "undefined" from being displayed.
7875
+ * Evaluates the expression and returns the result as a boolean.
7876
+ *
7877
+ * @returns The boolean result of the expression evaluation.
7754
7878
  */
7755
- #updateProperty(element, name, value) {
7756
- if (value == null && (name === 'value' || name === 'textContent' || name === 'innerHTML')) {
7757
- element[name] = '';
7758
- if (name === 'value') {
7759
- element._value = null;
7760
- }
7761
- }
7762
- else {
7763
- element[name] = value;
7764
- if (name === 'value') {
7765
- // Store the original typed value so that v-model on radio buttons
7766
- // can preserve the type (e.g., boolean false instead of string "false").
7767
- element._value = value;
7768
- }
7769
- }
7879
+ evaluateAsBoolean() {
7880
+ return Boolean(this.evaluate());
7770
7881
  }
7771
7882
  /**
7772
- * Updates a boolean attribute.
7883
+ * Gets the list of identifiers extracted from the expression.
7773
7884
  */
7774
- #updateBooleanAttribute(element, name, value) {
7775
- if (value) {
7776
- element.setAttribute(name, '');
7777
- }
7778
- else {
7779
- element.removeAttribute(name);
7780
- }
7885
+ get dependentIdentifiers() {
7886
+ // Return the original extracted identifiers (may include member chains like 'a.b')
7887
+ return this.originalIdentifiers;
7781
7888
  }
7782
7889
  /**
7783
- * Updates a regular attribute.
7784
- * An attribute that already holds the value is left alone: setting `src` on
7785
- * an <iframe>, even to the same URL, loads its page again.
7890
+ * Clears the expression cache.
7891
+ * This should be called sparingly, as it will force recompilation of all expressions.
7786
7892
  */
7787
- #updateAttribute(element, name, value) {
7788
- if (value == null || value === false) {
7789
- if (element.hasAttribute(name)) {
7790
- element.removeAttribute(name);
7791
- }
7792
- }
7793
- else {
7794
- const text = String(value);
7795
- if (element.getAttribute(name) !== text) {
7796
- element.setAttribute(name, text);
7797
- }
7798
- }
7893
+ static clearCache() {
7894
+ this.cache.clear();
7799
7895
  }
7800
7896
  }
7801
7897
 
7802
7898
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7803
7899
  /**
7804
- * Directive for rendering components.
7805
- * Usage: <div v-component="componentId" :options="props"></div>
7806
- *
7807
- * The :options binding is used to pass properties to the component.
7808
- * Example:
7809
- * <div v-component="my-component" :options="{message: 'Hello'}"></div>
7900
+ * Directive for binding dynamic attributes to DOM elements.
7901
+ * The `v-bind` directive allows you to bind HTML attributes to expressions in your data model.
7902
+ * The syntax for using the `v-bind` directive is `v-bind:attribute="expression"`, where `attribute` is the name of the HTML attribute you want to bind (e.g., `src`, `href`, `class`, etc.), and `expression` is a JavaScript expression that evaluates to the value you want to assign to that attribute.
7903
+ * Example usage:
7904
+ * <img v-bind:src="imageSrc" />
7905
+ * The value of `imageSrc` should be defined in the component's data object.
7906
+ * When the value of `imageSrc` changes, the `src` attribute of the `<img>` element will automatically update to reflect the new value.
7907
+ * This directive is particularly useful for dynamically updating attributes based on user interactions or other data changes in your application.
7908
+ * It helps keep the DOM in sync with the underlying data model, making it easier to build reactive user interfaces.
7909
+ * The `v-bind` directive can also be used with shorthand syntax using a colon (:).
7910
+ * For example, `:src="imageSrc"` is equivalent to `v-bind:src="imageSrc"`.
7911
+ * This shorthand syntax is often used for brevity and improved readability in templates.
7912
+ * Overall, the `v-bind` directive is a powerful tool for creating dynamic and responsive web applications by allowing seamless integration between the data model and the DOM.
7810
7913
  */
7811
- class VComponentDirective {
7914
+ class VBindDirective {
7812
7915
  /**
7813
7916
  * The virtual node to which this directive is applied.
7814
7917
  */
7815
7918
  #vNode;
7816
7919
  /**
7817
- * The component ID to render.
7920
+ * The expression evaluator for this directive.
7818
7921
  */
7819
- #componentId;
7922
+ #evaluator;
7820
7923
  /**
7821
- * The application instance for the component.
7924
+ * The name of the attribute to bind (e.g., "src", "class", "style").
7925
+ */
7926
+ #attributeName;
7927
+ /**
7928
+ * The original expression string from the directive.
7929
+ */
7930
+ #expression;
7931
+ /**
7932
+ * The set of class names managed by this directive (used when binding to the "class" attribute).
7933
+ * This helps in tracking which classes were added by this directive to avoid conflicts with other class manipulations.
7934
+ */
7935
+ #managedClasses = new Set();
7936
+ /**
7937
+ * The set of style properties managed by this directive (used when binding to the "style" attribute).
7938
+ * This helps in tracking which styles were added by this directive to avoid conflicts with other style manipulations.
7939
+ */
7940
+ #managedStyles = new Set();
7941
+ /**
7942
+ * @param context The context for parsing the directive.
7822
7943
  */
7823
- #componentApp;
7824
7944
  constructor(context) {
7825
7945
  this.#vNode = context.vNode;
7826
- this.#componentId = context.attribute.value.trim();
7946
+ // Extract the attribute name from the directive
7947
+ // e.g., "v-bind:src" -> "src", ":href" -> "href"
7948
+ const attrName = context.attribute.name;
7949
+ if (attrName.startsWith('v-bind:')) {
7950
+ this.#attributeName = attrName.substring(7);
7951
+ }
7952
+ else if (attrName.startsWith(':')) {
7953
+ this.#attributeName = attrName.substring(1);
7954
+ }
7955
+ // Parse the expression and create the evaluator
7956
+ this.#expression = context.attribute.value;
7957
+ if (this.#expression && context.vNode.bindings) {
7958
+ this.#evaluator = ExpressionEvaluator.create(this.#expression, context.vNode.bindings, context.vNode.vApplication.functionDependencies);
7959
+ }
7827
7960
  // Remove the directive attribute from the element
7828
7961
  this.#vNode.node.removeAttribute(context.attribute.name);
7829
7962
  }
@@ -7831,7 +7964,7 @@ class VComponentDirective {
7831
7964
  * @inheritdoc
7832
7965
  */
7833
7966
  get name() {
7834
- return StandardDirectiveName.V_COMPONENT;
7967
+ return StandardDirectiveName.V_BIND;
7835
7968
  }
7836
7969
  /**
7837
7970
  * @inheritdoc
@@ -7843,7 +7976,7 @@ class VComponentDirective {
7843
7976
  * @inheritdoc
7844
7977
  */
7845
7978
  get needsAnchor() {
7846
- return true;
7979
+ return false;
7847
7980
  }
7848
7981
  /**
7849
7982
  * @inheritdoc
@@ -7855,10 +7988,15 @@ class VComponentDirective {
7855
7988
  * @inheritdoc
7856
7989
  */
7857
7990
  get domUpdater() {
7858
- // Create and return the DOM updater
7991
+ const identifiers = this.#evaluator?.dependentIdentifiers ?? [];
7992
+ const evaluator = this.#evaluator;
7993
+ // Create an updater that handles the attribute binding
7859
7994
  const updater = {
7860
7995
  get dependentIdentifiers() {
7861
- return [];
7996
+ return identifiers;
7997
+ },
7998
+ get trackedPaths() {
7999
+ return evaluator?.trackedPaths;
7862
8000
  },
7863
8001
  applyToDOM: () => {
7864
8002
  this.#render();
@@ -7870,18 +8008,60 @@ class VComponentDirective {
7870
8008
  * @inheritdoc
7871
8009
  */
7872
8010
  get templatize() {
7873
- return true;
8011
+ return false;
7874
8012
  }
7875
8013
  /**
7876
8014
  * @inheritdoc
7877
8015
  */
7878
8016
  get dependentIdentifiers() {
7879
- return [];
8017
+ return this.#evaluator?.dependentIdentifiers ?? [];
8018
+ }
8019
+ /**
8020
+ * Indicates if this directive is binding the "key" attribute.
8021
+ * The "key" attribute is special and is used for optimizing rendering of lists.
8022
+ * If this directive is binding the "key" attribute, it will be handled by the VForDirective.
8023
+ */
8024
+ get isKey() {
8025
+ return (this.#attributeName === 'key');
8026
+ }
8027
+ /**
8028
+ * Indicates if this directive is binding the "options" attribute or any of its sub-properties (e.g., "options.intersection").
8029
+ * The "options" attribute is special and is used for passing options to certain directives like VIntersectionDirective.
8030
+ */
8031
+ get isOptions() {
8032
+ return (this.#attributeName === 'options' || this.#attributeName?.startsWith('options.') === true);
8033
+ }
8034
+ /**
8035
+ * Gets the name of the attribute being bound (e.g., "src", "class", "options", "options.intersection").
8036
+ */
8037
+ get attributeName() {
8038
+ return this.#attributeName;
8039
+ }
8040
+ /**
8041
+ * Gets the original expression string from the directive.
8042
+ */
8043
+ get expression() {
8044
+ return this.#expression;
8045
+ }
8046
+ /**
8047
+ * Evaluates the bound expression and returns the current value.
8048
+ * Returns undefined when no evaluator is available (e.g., empty expression).
8049
+ */
8050
+ evaluate() {
8051
+ return this.#evaluator?.evaluate();
7880
8052
  }
7881
8053
  /**
7882
8054
  * @inheritdoc
7883
8055
  */
7884
8056
  get onMount() {
8057
+ // A constant expression (e.g. :label="'Apple'") has no reactive
8058
+ // identifiers, so the identifier-driven update cycle never applies it.
8059
+ // Render it once at mount instead.
8060
+ if (this.#evaluator && this.#evaluator.dependentIdentifiers.length === 0) {
8061
+ return () => {
8062
+ this.#render();
8063
+ };
8064
+ }
7885
8065
  return undefined;
7886
8066
  }
7887
8067
  /**
@@ -7918,501 +8098,449 @@ class VComponentDirective {
7918
8098
  * @inheritdoc
7919
8099
  */
7920
8100
  destroy() {
7921
- if (!this.#componentApp) {
7922
- // Not rendered, no action needed
8101
+ // Do nothing. No special cleanup needed.
8102
+ }
8103
+ /**
8104
+ * Renders the bound attribute by evaluating the expression and updating the DOM element.
8105
+ */
8106
+ #render() {
8107
+ // Do nothing for special attributes
8108
+ if (this.isKey || this.isOptions) {
7923
8109
  return;
7924
8110
  }
7925
- // Get the component's root VNode
7926
- const componentVNode = this.#componentApp.rootVNode;
7927
- // Destroy component application first (calls @unmount hooks while DOM is still accessible)
7928
- this.#componentApp.unmount();
7929
- // Remove the component's root node from the DOM
7930
- if (componentVNode?.node.parentNode) {
7931
- componentVNode.node.parentNode.removeChild(componentVNode.node);
8111
+ const element = this.#vNode.node;
8112
+ const attributeName = this.#attributeName;
8113
+ // If there's no evaluator or attribute name, do nothing
8114
+ if (!this.#evaluator || !attributeName) {
8115
+ return;
8116
+ }
8117
+ // Evaluate the expression to get the value
8118
+ const value = this.#evaluator.evaluate();
8119
+ // Handle different attribute types
8120
+ if (attributeName === 'class') {
8121
+ this.#updateClass(element, value);
8122
+ }
8123
+ else if (attributeName === 'style') {
8124
+ this.#updateStyle(element, value);
8125
+ }
8126
+ else if (this.#isCustomElementProperty(element, attributeName, value)) {
8127
+ // Custom elements: set objects/arrays (and declared props) as properties
8128
+ // so complex values are not serialized to strings via setAttribute.
8129
+ // HTML attributes are lowercased by the browser, so resolve to the
8130
+ // actual camelCase property name declared on the custom element.
8131
+ const propName = this.#resolveCustomElementPropertyName(element, attributeName);
8132
+ element[propName] = value;
8133
+ }
8134
+ else if (this.#isDOMProperty(attributeName)) {
8135
+ this.#updateProperty(element, attributeName, value);
8136
+ }
8137
+ else if (this.#isBooleanAttribute(attributeName)) {
8138
+ this.#updateBooleanAttribute(element, attributeName, value);
8139
+ }
8140
+ else {
8141
+ this.#updateAttribute(element, attributeName, value);
7932
8142
  }
7933
- this.#componentApp = undefined;
7934
8143
  }
7935
8144
  /**
7936
- * Renders the component.
8145
+ * Updates the class attribute with support for string, array, and object formats.
7937
8146
  */
7938
- #render() {
7939
- if (this.#componentApp) {
7940
- // Already rendered, no action needed
7941
- return;
8147
+ #updateClass(element, value) {
8148
+ // Determine the new set of classes to apply
8149
+ let newClasses = [];
8150
+ if (typeof value === 'string') {
8151
+ newClasses = value.split(/\s+/).filter(Boolean);
7942
8152
  }
7943
- // Clone the component's template and replace the original node
7944
- const clone = this.#cloneNode();
7945
- this.#vNode.anchorNode?.parentNode?.insertBefore(clone, this.#vNode.anchorNode.nextSibling);
7946
- // Get properties from :options or :options.component directive
7947
- let properties = {};
7948
- const optionsDirective = this.#vNode.directiveManager?.optionsDirective('component');
7949
- if (optionsDirective && optionsDirective.expression && this.#vNode.bindings) {
7950
- // Evaluate the options expression using ExpressionEvaluator
7951
- const evaluator = ExpressionEvaluator.create(optionsDirective.expression, this.#vNode.bindings, this.#vNode.vApplication.functionDependencies);
7952
- const result = evaluator.evaluate();
7953
- if (typeof result === 'object' && result !== null) {
7954
- properties = result;
7955
- }
8153
+ else if (Array.isArray(value)) {
8154
+ // Flatten array elements that may contain strings or objects
8155
+ newClasses = value
8156
+ .filter(Boolean)
8157
+ .flatMap(cls => {
8158
+ if (typeof cls === 'string') {
8159
+ return cls.split(/\s+/).filter(Boolean);
8160
+ }
8161
+ else if (typeof cls === 'object' && cls !== null) {
8162
+ // Handle object format within array: { className: condition }
8163
+ // Keys may contain space-separated class names
8164
+ return Object.keys(cls).filter(key => cls[key]).flatMap(key => key.split(/\s+/).filter(Boolean));
8165
+ }
8166
+ return [];
8167
+ });
7956
8168
  }
7957
- // Get component definition from the application's component registry
7958
- const component = this.#vNode.vApplication.componentRegistry.get(this.#componentId);
7959
- if (!component) {
7960
- throw new Error(`Component '${this.#componentId}' not found in registry`);
8169
+ else if (typeof value === 'object' && value !== null) {
8170
+ // Keys may contain space-separated class names
8171
+ newClasses = Object.keys(value).filter(key => value[key]).flatMap(key => key.split(/\s+/).filter(Boolean));
7961
8172
  }
7962
- // Create component instance
7963
- const instance = component.createInstance(properties);
7964
- // Create and mount child application using the parent application's registries
7965
- this.#componentApp = this.#vNode.vApplication.createChildApp(instance);
7966
- this.#componentApp.mount(clone);
8173
+ // Remove previously managed classes
8174
+ this.#managedClasses.forEach(cls => element.classList.remove(cls));
8175
+ // Add newly managed classes
8176
+ newClasses.forEach(cls => element.classList.add(cls));
8177
+ // Update managed classes list
8178
+ this.#managedClasses = new Set(newClasses);
7967
8179
  }
7968
8180
  /**
7969
- * Clones the component's template and returns the root element.
7970
- * @returns The cloned root HTMLElement of the component.
7971
- * @throws Error if the component or its template is not found.
8181
+ * Updates the style attribute with support for object format.
7972
8182
  */
7973
- #cloneNode() {
7974
- // Get component definition from the application's component registry
7975
- const component = this.#vNode.vApplication.componentRegistry.get(this.#componentId);
7976
- if (!component) {
7977
- throw new Error(`Component '${this.#componentId}' not found in registry`);
7978
- }
7979
- // Get template element
7980
- const finalTemplateID = component.templateID || component.id;
7981
- const templateElement = document.querySelector(`#${finalTemplateID}`);
7982
- if (!templateElement || !(templateElement instanceof HTMLTemplateElement)) {
7983
- throw new Error(`Template element '#${finalTemplateID}' not found`);
8183
+ #updateStyle(element, value) {
8184
+ let newStyles = [];
8185
+ if (typeof value === 'string') {
8186
+ // Directly set the style string
8187
+ element.style.cssText = value;
8188
+ // Extract managed properties
8189
+ newStyles = value.split(';').map(s => s.split(':')[0].trim()).filter(Boolean);
7984
8190
  }
7985
- // Clone template content
7986
- const fragment = templateElement.content.cloneNode(true);
7987
- const childNodes = Array.from(fragment.childNodes);
7988
- // Find the first element node
7989
- for (const node of childNodes) {
7990
- if (node.nodeType === Node.ELEMENT_NODE) {
7991
- return node;
8191
+ else if (typeof value === 'object' && value !== null) {
8192
+ // Remove all previously managed properties
8193
+ this.#managedStyles.forEach(prop => {
8194
+ element.style.removeProperty(this.#camelToKebab(prop));
8195
+ });
8196
+ // Add newly managed properties
8197
+ for (const key in value) {
8198
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
8199
+ const cssKey = this.#camelToKebab(key);
8200
+ const cssValue = value[key];
8201
+ if (cssValue != null) {
8202
+ element.style.setProperty(cssKey, String(cssValue));
8203
+ newStyles.push(key);
8204
+ }
8205
+ }
7992
8206
  }
7993
8207
  }
7994
- throw new Error(`No element found in template '#${finalTemplateID}'`);
8208
+ // Update managed properties list
8209
+ this.#managedStyles = new Set(newStyles);
7995
8210
  }
7996
- }
7997
-
7998
- // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7999
- /**
8000
- * Utility class for creating reactive proxies that automatically track changes.
8001
- */
8002
- class ReactiveProxy {
8003
8211
  /**
8004
- * A WeakMap to store the proxy for each target object and path combination.
8005
- * This prevents creating multiple proxies for the same object accessed from different paths.
8212
+ * Converts camelCase to kebab-case for CSS properties.
8006
8213
  */
8007
- static proxyCache = new WeakMap();
8214
+ #camelToKebab(str) {
8215
+ return str.replace(/([A-Z])/g, '-$1').toLowerCase();
8216
+ }
8008
8217
  /**
8009
- * A WeakMap to track which objects are proxies, mapping proxy -> original target.
8010
- * This prevents double-wrapping of already proxied objects.
8218
+ * Returns true when the target is a custom element and the binding should be
8219
+ * delivered as a property rather than an HTML attribute.
8220
+ *
8221
+ * Three conditions trigger property delivery:
8222
+ * 1. The value is an object or array — serialising these to a string attribute
8223
+ * would lose type information.
8224
+ * 2. The element exposes a matching property accessor (e.g. a prop declared via
8225
+ * defineComponent), even when the value is a primitive.
8226
+ * 3. The element declares a matching prop in its static _props (case-insensitive
8227
+ * and kebab-case → camelCase tolerant), so primitive values reach the
8228
+ * reactive binding via the generated setter instead of being lost as a
8229
+ * plain HTML attribute that no attributeChangedCallback observes.
8011
8230
  */
8012
- static proxyToTarget = new WeakMap();
8231
+ #isCustomElementProperty(element, name, value) {
8232
+ if (!element.tagName.includes('-')) {
8233
+ return false;
8234
+ }
8235
+ const isObjectOrArray = Array.isArray(value) || (typeof value === 'object' && value !== null);
8236
+ if (isObjectOrArray || name in element) {
8237
+ return true;
8238
+ }
8239
+ return this.#findDeclaredProp(element, name) !== undefined;
8240
+ }
8013
8241
  /**
8014
- * A WeakSet to track objects marked as "raw" (non-reactive).
8015
- * These objects will not be wrapped with Proxy.
8242
+ * Resolves the actual property name on a custom element for a given
8243
+ * (lowercased) HTML attribute name. HTML attributes are always lowercase,
8244
+ * but custom element props are typically camelCase. This method checks the
8245
+ * element's declared _props (set by defineComponent) for a case-insensitive
8246
+ * match against both the raw attribute name and its kebab-case → camelCase
8247
+ * conversion (so `user-name` resolves to `userName`), falling back to the
8248
+ * original name when no match is found.
8016
8249
  */
8017
- static rawObjects = new WeakSet();
8250
+ #resolveCustomElementPropertyName(element, name) {
8251
+ // Fast path: exact match already exists
8252
+ if (name in element) {
8253
+ return name;
8254
+ }
8255
+ return this.#findDeclaredProp(element, name) ?? name;
8256
+ }
8018
8257
  /**
8019
- * A WeakMap to store the path for each proxy object.
8020
- * This allows retrieving the source path of an object for computed property mapping.
8258
+ * Looks up a declared prop on a custom element constructor whose name matches
8259
+ * the given attribute name, treating the comparison as case-insensitive and
8260
+ * tolerant of kebab-case → camelCase conversion. Returns the canonical prop
8261
+ * name (as declared) when a match is found, or undefined otherwise.
8021
8262
  */
8022
- static proxyPaths = new WeakMap();
8263
+ #findDeclaredProp(element, name) {
8264
+ const props = element.constructor._props;
8265
+ if (!Array.isArray(props)) {
8266
+ return undefined;
8267
+ }
8268
+ const lowerName = name.toLowerCase();
8269
+ const camelLower = this.#kebabToCamel(lowerName).toLowerCase();
8270
+ return props.find(p => {
8271
+ const lowerProp = p.toLowerCase();
8272
+ return lowerProp === lowerName || lowerProp === camelLower;
8273
+ });
8274
+ }
8023
8275
  /**
8024
- * Dispatchers per (target, path). Every target reached while walking a proxy
8025
- * subtree registers an entry pointing at the same dispatcher as the outermost
8026
- * proxy of that subtree, so callers can look up the dispatcher from any
8027
- * intermediate proxy when subscribing.
8276
+ * Converts kebab-case to camelCase (e.g. `user-name` → `userName`).
8028
8277
  */
8029
- static dispatchers = new WeakMap();
8278
+ #kebabToCamel(str) {
8279
+ return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
8280
+ }
8030
8281
  /**
8031
- * A Map to store path aliases.
8032
- * Key: alias path (e.g., "editingNestedStep.steps")
8033
- * Value: source path (e.g., "routes[0].steps[0].steps")
8034
- * This allows mapping variable names to their actual source paths for dependency tracking.
8282
+ * Checks if the attribute should be set as a DOM property.
8035
8283
  */
8036
- static pathAliases = new Map();
8284
+ #isDOMProperty(name) {
8285
+ const domProperties = ['value', 'checked', 'selected', 'innerHTML', 'textContent'];
8286
+ return domProperties.includes(name);
8287
+ }
8037
8288
  /**
8038
- * The `Object.prototype.toString` tags of the *only* value types we deeply
8039
- * proxy. Everything else is returned untouched (raw).
8040
- *
8041
- * Rationale: a Proxy can only safely intercept plain data containers. Built-in
8042
- * and host objects — `Date`, `RegExp`, `File`, `Blob`, `FileList`, `Promise`,
8043
- * `WeakMap`/`WeakSet`, typed arrays, DOM nodes, etc. — carry private internal
8044
- * slots, and invoking their native methods with a Proxy as the `this`/receiver
8045
- * throws "Illegal invocation". They must therefore never be wrapped. This is an
8046
- * allow-list rather than a deny-list so that *every* such exotic type is
8047
- * excluded by default, not just the handful we happened to enumerate.
8048
- *
8049
- * Plain user-defined class instances report `[object Object]` (unless they set
8050
- * `Symbol.toStringTag`) and so remain reactive, preserving prior behaviour.
8051
- * `Map`/`Set` stay reactive because the proxy specially wraps their mutation
8052
- * methods (see the `get` trap); `WeakMap`/`WeakSet` are intentionally excluded
8053
- * since they cannot be wrapped that way and would break the same as `File`.
8054
- *
8055
- * Callers that want a plain object kept non-reactive can still opt out
8056
- * explicitly via {@link markRaw}.
8289
+ * Checks if the attribute is a boolean attribute.
8057
8290
  */
8058
- static REACTIVE_TYPE_TAGS = new Set([
8059
- '[object Object]',
8060
- '[object Array]',
8061
- '[object Arguments]',
8062
- '[object Map]',
8063
- '[object Set]',
8064
- ]);
8291
+ #isBooleanAttribute(name) {
8292
+ const booleanAttributes = [
8293
+ 'disabled', 'readonly', 'required', 'checked', 'selected',
8294
+ 'multiple', 'autofocus', 'autoplay', 'controls', 'loop',
8295
+ 'muted', 'open', 'hidden'
8296
+ ];
8297
+ return booleanAttributes.includes(name);
8298
+ }
8065
8299
  /**
8066
- * Whether the given non-null object value is one we deeply proxy.
8067
- * See {@link REACTIVE_TYPE_TAGS} for the reasoning.
8300
+ * Updates a DOM property.
8301
+ * For value-like properties (value, textContent, innerHTML), null/undefined is converted to empty string
8302
+ * to match Vue.js behavior and prevent "undefined" from being displayed.
8068
8303
  */
8069
- static isReactivable(value) {
8070
- return this.REACTIVE_TYPE_TAGS.has(Object.prototype.toString.call(value));
8304
+ #updateProperty(element, name, value) {
8305
+ if (value == null && (name === 'value' || name === 'textContent' || name === 'innerHTML')) {
8306
+ element[name] = '';
8307
+ if (name === 'value') {
8308
+ element._value = null;
8309
+ }
8310
+ }
8311
+ else {
8312
+ element[name] = value;
8313
+ if (name === 'value') {
8314
+ // Store the original typed value so that v-model on radio buttons
8315
+ // can preserve the type (e.g., boolean false instead of string "false").
8316
+ element._value = value;
8317
+ }
8318
+ }
8071
8319
  }
8072
8320
  /**
8073
- * Creates a reactive proxy for the given object.
8074
- * The proxy will call the onChange callback whenever a property is modified.
8075
- *
8076
- * @param target The object to make reactive.
8077
- * @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
8078
- * @param path The current path in the object tree (used internally for nested objects).
8079
- * @param inheritedDispatcher Internal: the dispatcher inherited from an enclosing create() call when wrapping a nested target. External callers must omit this.
8080
- * @returns A reactive proxy of the target object.
8321
+ * Updates a boolean attribute.
8081
8322
  */
8082
- static create(target, onChange, path = '', inheritedDispatcher) {
8083
- // If the target is not an object or is null, return it as-is
8084
- if (typeof target !== 'object' || target === null) {
8085
- return target;
8086
- }
8087
- // Don't wrap objects marked as raw (non-reactive)
8088
- if (this.rawObjects.has(target)) {
8089
- return target;
8090
- }
8091
- // Only deeply proxy plain data containers; return every built-in/host
8092
- // object (Date, File, Blob, Promise, WeakMap, DOM nodes, …) as-is, since
8093
- // their native methods break when invoked through a Proxy receiver.
8094
- if (!this.isReactivable(target)) {
8095
- return target;
8096
- }
8097
- // Check if the target is already a proxy - if so, return it as-is to prevent double-wrapping
8098
- if (this.proxyToTarget.has(target)) {
8099
- return target;
8323
+ #updateBooleanAttribute(element, name, value) {
8324
+ if (value) {
8325
+ element.setAttribute(name, '');
8100
8326
  }
8101
- // Resolve (and register) the dispatcher for this (target, path).
8102
- // Nested create() calls inherit the dispatcher from the enclosing call so
8103
- // that a single dispatcher fans out changes at any depth of the subtree.
8104
- const dispatcher = this.resolveDispatcher(target, path, inheritedDispatcher);
8105
- if (onChange) {
8106
- dispatcher.listeners.add(onChange);
8327
+ else {
8328
+ element.removeAttribute(name);
8107
8329
  }
8108
- // Check if we already have a proxy for this target with this path
8109
- let pathMap = this.proxyCache.get(target);
8110
- if (pathMap) {
8111
- const existingProxy = pathMap.get(path);
8112
- if (existingProxy) {
8113
- return existingProxy;
8330
+ }
8331
+ /**
8332
+ * Updates a regular attribute.
8333
+ * An attribute that already holds the value is left alone: setting `src` on
8334
+ * an <iframe>, even to the same URL, loads its page again.
8335
+ */
8336
+ #updateAttribute(element, name, value) {
8337
+ if (value == null || value === false) {
8338
+ if (element.hasAttribute(name)) {
8339
+ element.removeAttribute(name);
8114
8340
  }
8115
8341
  }
8116
8342
  else {
8117
- pathMap = new Map();
8118
- this.proxyCache.set(target, pathMap);
8119
- }
8120
- // Create the proxy with path captured in closure
8121
- const proxy = new Proxy(target, {
8122
- get(obj, key, receiver) {
8123
- const value = Reflect.get(obj, key);
8124
- // If the value is an object or array, make it reactive too
8125
- if (typeof value === 'object' && value !== null) {
8126
- // Don't wrap objects marked as raw (non-reactive)
8127
- if (ReactiveProxy.rawObjects.has(value)) {
8128
- return value;
8129
- }
8130
- // Only deeply proxy plain data containers; hand back built-in
8131
- // and host objects (Date, File, Blob, Promise, WeakMap, DOM
8132
- // nodes, …) untouched so their native methods keep working.
8133
- if (!ReactiveProxy.isReactivable(value)) {
8134
- return value;
8135
- }
8136
- // Build the nested path
8137
- const keyStr = String(key);
8138
- const nestedPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
8139
- return ReactiveProxy.create(value, undefined, nestedPath, dispatcher);
8140
- }
8141
- // If the value is a function, we need to wrap it to ensure that any mutations it performs also trigger onChange
8142
- if (typeof value === 'function') {
8143
- // For arrays, we only want to wrap mutation methods, not read methods like 'slice', 'concat', etc.
8144
- if (Array.isArray(obj)) {
8145
- const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
8146
- if (!arrayMutationMethods.includes(key)) {
8147
- return value;
8148
- }
8149
- return function (...args) {
8150
- const result = value.apply(this === receiver ? obj : this, args);
8151
- ReactiveProxy.dispatch(dispatcher, path || undefined);
8152
- return result;
8153
- };
8154
- }
8155
- // For Map, we only want to wrap mutation methods, not read methods like 'get' or 'has'
8156
- if (obj.constructor.name === 'Map') {
8157
- const mapMutationMethods = ['set', 'delete', 'clear'];
8158
- return function (...args) {
8159
- const result = value.apply(this === receiver ? obj : this, args);
8160
- if (mapMutationMethods.includes(key)) {
8161
- ReactiveProxy.dispatch(dispatcher, path || undefined);
8162
- }
8163
- return result;
8164
- };
8165
- }
8166
- // For Set, we only want to wrap mutation methods, not read methods like 'has'
8167
- if (obj.constructor.name === 'Set') {
8168
- const setMutationMethods = ['add', 'delete', 'clear'];
8169
- return function (...args) {
8170
- const result = value.apply(this === receiver ? obj : this, args);
8171
- if (setMutationMethods.includes(key)) {
8172
- ReactiveProxy.dispatch(dispatcher, path || undefined);
8173
- }
8174
- return result;
8175
- };
8176
- }
8177
- }
8178
- return value;
8179
- },
8180
- set(obj, key, value) {
8181
- const oldValue = Reflect.get(obj, key);
8182
- const result = Reflect.set(obj, key, value);
8183
- // Only trigger onChange if the value actually changed
8184
- if (oldValue !== value) {
8185
- const keyStr = String(key);
8186
- const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
8187
- ReactiveProxy.dispatch(dispatcher, fullPath);
8188
- }
8189
- return result;
8190
- },
8191
- deleteProperty(obj, key) {
8192
- const result = Reflect.deleteProperty(obj, key);
8193
- const keyStr = String(key);
8194
- const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
8195
- ReactiveProxy.dispatch(dispatcher, fullPath);
8196
- return result;
8343
+ const text = String(value);
8344
+ if (element.getAttribute(name) !== text) {
8345
+ element.setAttribute(name, text);
8197
8346
  }
8198
- });
8199
- // Cache the proxy for this path
8200
- pathMap.set(path, proxy);
8201
- // Track that this proxy wraps the target to prevent double-wrapping
8202
- this.proxyToTarget.set(proxy, target);
8203
- // Store the path for this proxy (for computed property mapping)
8204
- if (path) {
8205
- this.proxyPaths.set(proxy, path);
8206
8347
  }
8207
- return proxy;
8208
8348
  }
8349
+ }
8350
+
8351
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
8352
+ /**
8353
+ * Directive for rendering components.
8354
+ * Usage: <div v-component="componentId" :options="props"></div>
8355
+ *
8356
+ * The :options binding is used to pass properties to the component.
8357
+ * Example:
8358
+ * <div v-component="my-component" :options="{message: 'Hello'}"></div>
8359
+ */
8360
+ class VComponentDirective {
8209
8361
  /**
8210
- * Looks up the dispatcher associated with (target, path), or installs a new one.
8211
- * When called for a nested target during proxy walking, the enclosing dispatcher
8212
- * is reused so a single subtree fans out changes through one notification path.
8362
+ * The virtual node to which this directive is applied.
8213
8363
  */
8214
- static resolveDispatcher(target, path, inheritedDispatcher) {
8215
- let pathMap = this.dispatchers.get(target);
8216
- if (!pathMap) {
8217
- pathMap = new Map();
8218
- this.dispatchers.set(target, pathMap);
8219
- }
8220
- const existing = pathMap.get(path);
8221
- if (existing) {
8222
- return existing;
8223
- }
8224
- const dispatcher = inheritedDispatcher ?? { listeners: new Set() };
8225
- pathMap.set(path, dispatcher);
8226
- return dispatcher;
8364
+ #vNode;
8365
+ /**
8366
+ * The component ID to render.
8367
+ */
8368
+ #componentId;
8369
+ /**
8370
+ * The application instance for the component.
8371
+ */
8372
+ #componentApp;
8373
+ constructor(context) {
8374
+ this.#vNode = context.vNode;
8375
+ this.#componentId = context.attribute.value.trim();
8376
+ // Remove the directive attribute from the element
8377
+ this.#vNode.node.removeAttribute(context.attribute.name);
8227
8378
  }
8228
8379
  /**
8229
- * Invokes every listener attached to the dispatcher.
8230
- * Iterates a snapshot of the listener set so unsubscribing during dispatch is safe.
8380
+ * @inheritdoc
8231
8381
  */
8232
- static dispatch(dispatcher, changedPath) {
8233
- if (dispatcher.listeners.size === 0) {
8234
- return;
8235
- }
8236
- const snapshot = Array.from(dispatcher.listeners);
8237
- for (const listener of snapshot) {
8238
- listener(changedPath);
8239
- }
8382
+ get name() {
8383
+ return StandardDirectiveName.V_COMPONENT;
8240
8384
  }
8241
8385
  /**
8242
- * Subscribes a listener to changes inside the subtree of an existing reactive proxy.
8243
- *
8244
- * The listener is scoped by the proxy's source path: only changes at or below that
8245
- * path are delivered, which lets a child component receive notifications when the
8246
- * nested contents of a prop change even though the prop reference itself is unchanged.
8247
- *
8248
- * @param proxyOrTarget A proxy returned from create(), or the underlying target object.
8249
- * @param listener Called with the full source path of every relevant change.
8250
- * @returns A function that removes the subscription.
8386
+ * @inheritdoc
8251
8387
  */
8252
- static subscribe(proxyOrTarget, listener) {
8253
- if (typeof proxyOrTarget !== 'object' || proxyOrTarget === null) {
8254
- return () => { };
8255
- }
8256
- const target = (this.proxyToTarget.get(proxyOrTarget) ?? proxyOrTarget);
8257
- const scopePath = this.proxyPaths.get(proxyOrTarget) ?? '';
8258
- const pathMap = this.dispatchers.get(target);
8259
- const dispatcher = pathMap?.get(scopePath);
8260
- if (!dispatcher) {
8261
- return () => { };
8262
- }
8263
- const wrapper = scopePath
8264
- ? (changedPath) => {
8265
- if (changedPath === scopePath ||
8266
- (typeof changedPath === 'string' && (changedPath.startsWith(scopePath + '.') ||
8267
- changedPath.startsWith(scopePath + '[')))) {
8268
- listener(changedPath);
8269
- }
8388
+ get vNode() {
8389
+ return this.#vNode;
8390
+ }
8391
+ /**
8392
+ * @inheritdoc
8393
+ */
8394
+ get needsAnchor() {
8395
+ return true;
8396
+ }
8397
+ /**
8398
+ * @inheritdoc
8399
+ */
8400
+ get bindingsPreparer() {
8401
+ return undefined;
8402
+ }
8403
+ /**
8404
+ * @inheritdoc
8405
+ */
8406
+ get domUpdater() {
8407
+ // Create and return the DOM updater
8408
+ const updater = {
8409
+ get dependentIdentifiers() {
8410
+ return [];
8411
+ },
8412
+ applyToDOM: () => {
8413
+ this.#render();
8270
8414
  }
8271
- : listener;
8272
- dispatcher.listeners.add(wrapper);
8273
- return () => {
8274
- dispatcher.listeners.delete(wrapper);
8275
8415
  };
8416
+ return updater;
8417
+ }
8418
+ /**
8419
+ * @inheritdoc
8420
+ */
8421
+ get templatize() {
8422
+ return true;
8276
8423
  }
8277
8424
  /**
8278
- * Checks if the given object is a reactive proxy.
8279
- *
8280
- * @param obj The object to check.
8281
- * @returns True if the object is a reactive proxy, false otherwise.
8425
+ * @inheritdoc
8282
8426
  */
8283
- static isReactive(obj) {
8284
- return this.proxyCache.has(obj);
8427
+ get dependentIdentifiers() {
8428
+ return [];
8285
8429
  }
8286
8430
  /**
8287
- * Unwraps a reactive proxy to get the original object.
8288
- * If the object is not a proxy, returns it as-is.
8289
- *
8290
- * @param obj The object to unwrap.
8291
- * @returns The original object.
8431
+ * @inheritdoc
8292
8432
  */
8293
- static unwrap(obj) {
8294
- // This is a simplified implementation
8295
- // In a full implementation, we'd need to store a reverse mapping
8296
- return obj;
8433
+ get onMount() {
8434
+ return undefined;
8297
8435
  }
8298
8436
  /**
8299
- * Marks an object as "raw" (non-reactive).
8300
- * Objects marked as raw will not be wrapped with Proxy when accessed from reactive objects.
8301
- * This is useful for objects that should not be reactive, such as:
8302
- * - Objects with private fields (class instances with # fields)
8303
- * - Third-party library instances
8304
- * - Objects used only for method calls
8305
- *
8306
- * @param obj The object to mark as raw.
8307
- * @returns The same object (for chaining).
8437
+ * @inheritdoc
8308
8438
  */
8309
- static markRaw(obj) {
8310
- if (typeof obj === 'object' && obj !== null) {
8311
- this.rawObjects.add(obj);
8312
- }
8313
- return obj;
8439
+ get onMounted() {
8440
+ return undefined;
8314
8441
  }
8315
8442
  /**
8316
- * Checks if an object is marked as raw (non-reactive).
8317
- *
8318
- * @param obj The object to check.
8319
- * @returns True if the object is marked as raw, false otherwise.
8443
+ * @inheritdoc
8320
8444
  */
8321
- static isRaw(obj) {
8322
- return typeof obj === 'object' && obj !== null && this.rawObjects.has(obj);
8445
+ get onUpdate() {
8446
+ return undefined;
8323
8447
  }
8324
8448
  /**
8325
- * Gets the source path for a proxy object.
8326
- * This is used to map computed property values back to their source paths.
8327
- * For example, if a computed property returns `model.elements[0]`,
8328
- * this method returns "model.elements[0]" for that object.
8329
- *
8330
- * @param obj The proxy object to get the path for.
8331
- * @returns The source path, or undefined if not found.
8449
+ * @inheritdoc
8332
8450
  */
8333
- static getPath(obj) {
8334
- if (typeof obj !== 'object' || obj === null) {
8335
- return undefined;
8336
- }
8337
- return this.proxyPaths.get(obj);
8451
+ get onUpdated() {
8452
+ return undefined;
8338
8453
  }
8339
8454
  /**
8340
- * Registers a path alias.
8341
- * This is used when a variable is assigned to reference an existing reactive object.
8342
- * For example, when `editingNestedStep = routes[0].steps[0]`, this creates an alias
8343
- * so that changes to "routes[0].steps[0].steps" also match "editingNestedStep.steps".
8344
- *
8345
- * @param aliasPath The alias path (e.g., "editingNestedStep")
8346
- * @param sourcePath The source path (e.g., "routes[0].steps[0]")
8455
+ * @inheritdoc
8347
8456
  */
8348
- static registerPathAlias(aliasPath, sourcePath) {
8349
- if (aliasPath && sourcePath && aliasPath !== sourcePath) {
8350
- this.pathAliases.set(aliasPath, sourcePath);
8351
- }
8457
+ get onUnmount() {
8458
+ return undefined;
8352
8459
  }
8353
8460
  /**
8354
- * Unregisters a path alias.
8355
- *
8356
- * @param aliasPath The alias path to remove
8461
+ * @inheritdoc
8357
8462
  */
8358
- static unregisterPathAlias(aliasPath) {
8359
- // Remove all aliases that start with the given path
8360
- for (const key of this.pathAliases.keys()) {
8361
- if (key === aliasPath || key.startsWith(aliasPath + '.') || key.startsWith(aliasPath + '[')) {
8362
- this.pathAliases.delete(key);
8363
- }
8463
+ get onUnmounted() {
8464
+ return undefined;
8465
+ }
8466
+ /**
8467
+ * @inheritdoc
8468
+ */
8469
+ destroy() {
8470
+ if (!this.#componentApp) {
8471
+ // Not rendered, no action needed
8472
+ return;
8473
+ }
8474
+ // Get the component's root VNode
8475
+ const componentVNode = this.#componentApp.rootVNode;
8476
+ // Destroy component application first (calls @unmount hooks while DOM is still accessible)
8477
+ this.#componentApp.unmount();
8478
+ // Remove the component's root node from the DOM
8479
+ if (componentVNode?.node.parentNode) {
8480
+ componentVNode.node.parentNode.removeChild(componentVNode.node);
8364
8481
  }
8482
+ this.#componentApp = undefined;
8365
8483
  }
8366
8484
  /**
8367
- * Resolves an alias path to its source path.
8368
- * Also handles nested paths (e.g., "editingNestedStep.steps" -> "routes[0].steps[0].steps").
8369
- *
8370
- * @param aliasPath The alias path to resolve
8371
- * @returns The source path, or undefined if no alias exists
8485
+ * Renders the component.
8372
8486
  */
8373
- static resolvePathAlias(aliasPath) {
8374
- // Direct match
8375
- if (this.pathAliases.has(aliasPath)) {
8376
- return this.pathAliases.get(aliasPath);
8487
+ #render() {
8488
+ if (this.#componentApp) {
8489
+ // Already rendered, no action needed
8490
+ return;
8377
8491
  }
8378
- // Check if this is a nested path of an aliased variable
8379
- // e.g., "editingNestedStep.steps" when "editingNestedStep" is aliased
8380
- for (const [alias, source] of this.pathAliases.entries()) {
8381
- if (aliasPath.startsWith(alias + '.') || aliasPath.startsWith(alias + '[')) {
8382
- const suffix = aliasPath.substring(alias.length);
8383
- return source + suffix;
8492
+ // Clone the component's template and replace the original node
8493
+ const clone = this.#cloneNode();
8494
+ this.#vNode.anchorNode?.parentNode?.insertBefore(clone, this.#vNode.anchorNode.nextSibling);
8495
+ // Get properties from :options or :options.component directive
8496
+ let properties = {};
8497
+ const optionsDirective = this.#vNode.directiveManager?.optionsDirective('component');
8498
+ if (optionsDirective && optionsDirective.expression && this.#vNode.bindings) {
8499
+ // Evaluate the options expression using ExpressionEvaluator
8500
+ const evaluator = ExpressionEvaluator.create(optionsDirective.expression, this.#vNode.bindings, this.#vNode.vApplication.functionDependencies);
8501
+ const result = evaluator.evaluate();
8502
+ if (typeof result === 'object' && result !== null) {
8503
+ properties = result;
8384
8504
  }
8385
8505
  }
8386
- return undefined;
8506
+ // Get component definition from the application's component registry
8507
+ const component = this.#vNode.vApplication.componentRegistry.get(this.#componentId);
8508
+ if (!component) {
8509
+ throw new Error(`Component '${this.#componentId}' not found in registry`);
8510
+ }
8511
+ // Create component instance
8512
+ const instance = component.createInstance(properties);
8513
+ // Create and mount child application using the parent application's registries
8514
+ this.#componentApp = this.#vNode.vApplication.createChildApp(instance);
8515
+ this.#componentApp.mount(clone);
8387
8516
  }
8388
8517
  /**
8389
- * Checks if a change path matches an identifier, considering path aliases.
8390
- * For example, if "editingNestedStep" is aliased to "routes[0].steps[0]",
8391
- * then a change to "routes[0].steps[0].steps" should match "editingNestedStep.steps".
8392
- *
8393
- * @param changePath The path that was changed (e.g., "routes[0].steps[0].steps")
8394
- * @param identifier The identifier to check (e.g., "editingNestedStep.steps")
8395
- * @returns True if the change path matches the identifier
8518
+ * Clones the component's template and returns the root element.
8519
+ * @returns The cloned root HTMLElement of the component.
8520
+ * @throws Error if the component or its template is not found.
8396
8521
  */
8397
- static doesChangeMatchIdentifier(changePath, identifier) {
8398
- // Direct match
8399
- if (changePath === identifier) {
8400
- return true;
8522
+ #cloneNode() {
8523
+ // Get component definition from the application's component registry
8524
+ const component = this.#vNode.vApplication.componentRegistry.get(this.#componentId);
8525
+ if (!component) {
8526
+ throw new Error(`Component '${this.#componentId}' not found in registry`);
8401
8527
  }
8402
- // Check if the identifier has an alias that matches
8403
- const resolvedIdentifier = this.resolvePathAlias(identifier);
8404
- if (resolvedIdentifier && changePath === resolvedIdentifier) {
8405
- return true;
8528
+ // Get template element
8529
+ const finalTemplateID = component.templateID || component.id;
8530
+ const templateElement = document.querySelector(`#${finalTemplateID}`);
8531
+ if (!templateElement || !(templateElement instanceof HTMLTemplateElement)) {
8532
+ throw new Error(`Template element '#${finalTemplateID}' not found`);
8406
8533
  }
8407
- // Check if change path starts with the resolved identifier
8408
- // (handles cases like array item changes)
8409
- if (resolvedIdentifier) {
8410
- if (changePath.startsWith(resolvedIdentifier + '.') ||
8411
- changePath.startsWith(resolvedIdentifier + '[')) {
8412
- return true;
8534
+ // Clone template content
8535
+ const fragment = templateElement.content.cloneNode(true);
8536
+ const childNodes = Array.from(fragment.childNodes);
8537
+ // Find the first element node
8538
+ for (const node of childNodes) {
8539
+ if (node.nodeType === Node.ELEMENT_NODE) {
8540
+ return node;
8413
8541
  }
8414
8542
  }
8415
- return false;
8543
+ throw new Error(`No element found in template '#${finalTemplateID}'`);
8416
8544
  }
8417
8545
  }
8418
8546
 
@@ -8443,6 +8571,13 @@ class VBindings {
8443
8571
  * The set of changed identifiers.
8444
8572
  */
8445
8573
  #changes = new Set();
8574
+ /**
8575
+ * On the root bindings: every descendant scope (a v-for row, a component) that
8576
+ * recorded a change since the last clearChanges(). The root clears their change
8577
+ * sets together with its own at the end of the update cycle, so a loop variable
8578
+ * set when a row was created does not count as changed in every later cycle.
8579
+ */
8580
+ #dirtyScopes;
8446
8581
  /**
8447
8582
  * Cache for array lengths to detect length changes when the same object reference is used.
8448
8583
  */
@@ -8585,7 +8720,7 @@ class VBindings {
8585
8720
  for (const part of changedPath?.split('.') || []) {
8586
8721
  path = path ? `${path}.${part}` : part;
8587
8722
  this.#logger?.debug(`Binding changed: ${path}`);
8588
- this.#changes.add(path);
8723
+ this.#addChange(path);
8589
8724
  }
8590
8725
  if (!this.#suppressOnChange) {
8591
8726
  this.#onChange?.(changedPath);
@@ -8625,7 +8760,7 @@ class VBindings {
8625
8760
  for (const part of changedPath.split('.')) {
8626
8761
  path = path ? `${path}.${part}` : part;
8627
8762
  this.#logger?.debug(`Binding changed (external): ${path}`);
8628
- this.#changes.add(path);
8763
+ this.#addChange(path);
8629
8764
  }
8630
8765
  if (!this.#suppressOnChange) {
8631
8766
  this.#onChange?.(changedPath);
@@ -8652,7 +8787,7 @@ class VBindings {
8652
8787
  const newValuePreview = newValueString.length > 100 ? `${newValueString.substring(0, 100)}...` : newValueString;
8653
8788
  this.#logger.debug(`Binding set on ${target === obj ? 'local' : 'parent'}: ${key}: ${oldValuePreview} -> ${newValuePreview}`);
8654
8789
  }
8655
- this.#changes.add(key);
8790
+ this.#addChange(key);
8656
8791
  if (!this.#suppressOnChange) {
8657
8792
  this.#onChange?.(key);
8658
8793
  }
@@ -8667,7 +8802,7 @@ class VBindings {
8667
8802
  this.#externalSubscriptions.delete(key);
8668
8803
  }
8669
8804
  this.#logger?.debug(`Binding deleted: ${key}`);
8670
- this.#changes.add(key);
8805
+ this.#addChange(key);
8671
8806
  this.#onChange?.(key);
8672
8807
  return result;
8673
8808
  }
@@ -8708,6 +8843,27 @@ class VBindings {
8708
8843
  */
8709
8844
  clearChanges() {
8710
8845
  this.#changes.clear();
8846
+ if (this.#dirtyScopes) {
8847
+ for (const scope of this.#dirtyScopes) {
8848
+ scope.#changes.clear();
8849
+ }
8850
+ this.#dirtyScopes.clear();
8851
+ }
8852
+ }
8853
+ /**
8854
+ * Records a changed identifier on this scope and, for a non-root scope, registers the
8855
+ * scope with the root so that the change is cleared at the end of the update cycle.
8856
+ */
8857
+ #addChange(key) {
8858
+ this.#changes.add(key);
8859
+ if (!this.#parent) {
8860
+ return;
8861
+ }
8862
+ let root = this;
8863
+ while (root.#parent) {
8864
+ root = root.#parent;
8865
+ }
8866
+ (root.#dirtyScopes ??= new Set()).add(this);
8711
8867
  }
8712
8868
  /**
8713
8869
  * Sets a binding value.
@@ -8883,7 +9039,7 @@ class VBindings {
8883
9039
  * @param key The identifier to mark as changed.
8884
9040
  */
8885
9041
  markChanged(key) {
8886
- this.#changes.add(key);
9042
+ this.#addChange(key);
8887
9043
  }
8888
9044
  /**
8889
9045
  * Resolves a local path alias for the given identifier by walking up the bindings chain.
@@ -8909,18 +9065,15 @@ class VBindings {
8909
9065
  * global aliases (computed properties etc.).
8910
9066
  */
8911
9067
  doesChangeMatchIdentifier(changePath, identifier) {
8912
- // Direct match
8913
- if (changePath === identifier) {
9068
+ // Direct match (an index "[*]" in the identifier matches any index)
9069
+ if (ReactiveProxy.pathMatches(changePath, identifier)) {
8914
9070
  return true;
8915
9071
  }
8916
9072
  // Resolve via local (scoped) alias chain
8917
9073
  const resolved = this.resolveAlias(identifier);
8918
9074
  if (resolved) {
8919
- if (changePath === resolved) {
8920
- return true;
8921
- }
8922
- // changePath is a descendant of resolved (e.g. "files[0].isModified" for resolved "files[0]")
8923
- if (changePath.startsWith(resolved + '.') || changePath.startsWith(resolved + '[')) {
9075
+ // changePath is resolved itself or a descendant of it (e.g. "files[0].isModified" for resolved "files[0]")
9076
+ if (ReactiveProxy.pathMatches(changePath, resolved, true)) {
8924
9077
  return true;
8925
9078
  }
8926
9079
  // changePath is an ancestor of resolved (e.g. "files" for resolved "files[0].isModified")
@@ -8933,6 +9086,54 @@ class VBindings {
8933
9086
  // Fall back to global alias resolution (computed properties, etc.)
8934
9087
  return ReactiveProxy.doesChangeMatchIdentifier(changePath, identifier);
8935
9088
  }
9089
+ /**
9090
+ * Checks whether any of the changes affects an expression with the given identifiers.
9091
+ *
9092
+ * An identifier with a run-time index ("frames[*].url" for frames[i].url) normally
9093
+ * matches a change at any index. When trackedPaths holds the paths the expression read
9094
+ * during its last evaluation, the identifier is narrowed to the concrete paths among
9095
+ * them ("frames[0].url"), so a write to another element is ignored. When no recorded
9096
+ * path fits the identifier (not evaluated yet, a raw object, a short-circuited read),
9097
+ * the identifier keeps matching every index.
9098
+ *
9099
+ * @param changes The change paths of the current update cycle.
9100
+ * @param identifiers The identifiers the expression depends on.
9101
+ * @param trackedPaths The paths read during the expression's last evaluation, if recorded.
9102
+ * @returns True if some change matches some identifier.
9103
+ */
9104
+ isAffected(changes, identifiers, trackedPaths) {
9105
+ if (changes.length === 0) {
9106
+ return false;
9107
+ }
9108
+ for (const identifier of identifiers) {
9109
+ const candidates = this.#concretePaths(identifier, trackedPaths) ?? [identifier];
9110
+ for (const candidate of candidates) {
9111
+ if (changes.some(change => this.doesChangeMatchIdentifier(change, candidate))) {
9112
+ return true;
9113
+ }
9114
+ }
9115
+ }
9116
+ return false;
9117
+ }
9118
+ /**
9119
+ * Returns the recorded paths that an identifier with a run-time index stood for during
9120
+ * the last evaluation, or undefined when the identifier has no such index or nothing
9121
+ * recorded fits it. The identifier is tried both as spelled and resolved through path
9122
+ * aliases, since recorded paths are source paths.
9123
+ */
9124
+ #concretePaths(identifier, trackedPaths) {
9125
+ if (!trackedPaths || trackedPaths.size === 0 || !identifier.includes('[*]')) {
9126
+ return undefined;
9127
+ }
9128
+ const resolved = this.resolveAlias(identifier);
9129
+ const paths = [];
9130
+ for (const path of trackedPaths) {
9131
+ if (ReactiveProxy.pathMatches(path, identifier) || (resolved !== undefined && ReactiveProxy.pathMatches(path, resolved))) {
9132
+ paths.push(path);
9133
+ }
9134
+ }
9135
+ return paths.length > 0 ? paths : undefined;
9136
+ }
8936
9137
  }
8937
9138
 
8938
9139
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
@@ -9127,10 +9328,14 @@ class VSlotOutletDirective {
9127
9328
  */
9128
9329
  get domUpdater() {
9129
9330
  const identifiers = this.dependentIdentifiers;
9331
+ const directive = this;
9130
9332
  const updater = {
9131
9333
  get dependentIdentifiers() {
9132
9334
  return identifiers;
9133
9335
  },
9336
+ get trackedPaths() {
9337
+ return directive.#collectTrackedPaths();
9338
+ },
9134
9339
  applyToDOM: () => {
9135
9340
  this.#refresh();
9136
9341
  }
@@ -9153,6 +9358,21 @@ class VSlotOutletDirective {
9153
9358
  }
9154
9359
  return Array.from(ids);
9155
9360
  }
9361
+ /**
9362
+ * The paths the prop expressions read during their last evaluation, united across
9363
+ * props, or undefined when no prop expression has a run-time index.
9364
+ */
9365
+ #collectTrackedPaths() {
9366
+ let paths;
9367
+ for (const prop of this.#props) {
9368
+ const tracked = prop.evaluator?.trackedPaths;
9369
+ if (tracked) {
9370
+ paths ??= new Set();
9371
+ tracked.forEach(path => paths.add(path));
9372
+ }
9373
+ }
9374
+ return paths;
9375
+ }
9156
9376
  /**
9157
9377
  * @inheritdoc
9158
9378
  *
@@ -9958,6 +10178,11 @@ class VTextEvaluator {
9958
10178
  * The function used to evaluate the text with current bindings.
9959
10179
  */
9960
10180
  #evaluate;
10181
+ /**
10182
+ * The source paths read through reactive proxies during the last evaluation,
10183
+ * recorded only when some expression has a run-time index ("items[i].name").
10184
+ */
10185
+ #trackedPaths;
9961
10186
  /**
9962
10187
  * Constructs a VTextEvaluator instance.
9963
10188
  * @param text The text containing embedded expressions in the form of {{...}}.
@@ -9985,7 +10210,7 @@ class VTextEvaluator {
9985
10210
  // Build base parameter names (no dots), preserving order of first appearance
9986
10211
  const baseArgs = [];
9987
10212
  for (const id of ids) {
9988
- const base = id.split('.')[0];
10213
+ const base = ExpressionUtils.baseIdentifier(id);
9989
10214
  if (!baseArgs.includes(base)) {
9990
10215
  baseArgs.push(base);
9991
10216
  }
@@ -9998,8 +10223,10 @@ class VTextEvaluator {
9998
10223
  func: new Function(args, funcBody)
9999
10224
  };
10000
10225
  });
10226
+ // Only an expression with an index known at run time needs its reads recorded
10227
+ const hasDynamicIndex = this.#identifiers.some(id => id.includes('[*]'));
10001
10228
  // Create a function to reconstruct the text with evaluated expressions
10002
- this.#evaluate = (bindings) => {
10229
+ const evaluateText = (bindings) => {
10003
10230
  let result = text;
10004
10231
  evaluators.forEach((evaluator, i) => {
10005
10232
  // Build values for baseArgs (no dots) in the same order as function parameters
@@ -10017,6 +10244,15 @@ class VTextEvaluator {
10017
10244
  });
10018
10245
  return result;
10019
10246
  };
10247
+ this.#evaluate = hasDynamicIndex
10248
+ ? (bindings) => {
10249
+ const paths = new Set();
10250
+ this.#trackedPaths = undefined;
10251
+ const result = ReactiveProxy.track(paths, () => evaluateText(bindings));
10252
+ this.#trackedPaths = paths;
10253
+ return result;
10254
+ }
10255
+ : evaluateText;
10020
10256
  }
10021
10257
  /**
10022
10258
  * Checks if the given text contains any expressions in the form of {{...}}.
@@ -10033,6 +10269,14 @@ class VTextEvaluator {
10033
10269
  get identifiers() {
10034
10270
  return this.#identifiers;
10035
10271
  }
10272
+ /**
10273
+ * The source paths read through reactive proxies during the last evaluation.
10274
+ * Recorded only when an expression has a run-time index ("items[i].name");
10275
+ * undefined before the first evaluation and for texts without such an index.
10276
+ */
10277
+ get trackedPaths() {
10278
+ return this.#trackedPaths;
10279
+ }
10036
10280
  /**
10037
10281
  * Evaluates the text with the provided bindings and returns the resulting string.
10038
10282
  * @param bindings The bindings to use for evaluating the expressions.
@@ -10522,7 +10766,7 @@ class VNode {
10522
10766
  if (this.#nodeType === Node.TEXT_NODE && this.#textEvaluator) {
10523
10767
  // If this is a text node with a text evaluator, update its content if needed
10524
10768
  // Check if any of the identifiers are in the changed identifiers (considering path aliases)
10525
- const changed = this.#textEvaluator.identifiers.some(id => changes.some(change => this.bindings.doesChangeMatchIdentifier(change, id)));
10769
+ const changed = this.bindings.isAffected(changes, this.#textEvaluator.identifiers, this.#textEvaluator.trackedPaths);
10526
10770
  // If the text node has changed, update its content
10527
10771
  if (changed) {
10528
10772
  const text = this.#node;
@@ -10545,7 +10789,7 @@ class VNode {
10545
10789
  }
10546
10790
  // Prepare bindings for each preparer if relevant identifiers have changed
10547
10791
  for (const preparer of this.#directiveManager.bindingsPreparers) {
10548
- const changed = preparer.dependentIdentifiers.some(id => changes.some(change => this.bindings.doesChangeMatchIdentifier(change, id)));
10792
+ const changed = this.bindings.isAffected(changes, preparer.dependentIdentifiers, preparer.trackedPaths);
10549
10793
  if (changed) {
10550
10794
  preparer.prepareBindings();
10551
10795
  }
@@ -10554,7 +10798,7 @@ class VNode {
10554
10798
  // Apply DOM updaters from directives, if any
10555
10799
  if (this.#directiveManager?.domUpdaters) {
10556
10800
  for (const updater of this.#directiveManager.domUpdaters) {
10557
- const changed = updater.dependentIdentifiers.some(id => changes.some(change => this.bindings.doesChangeMatchIdentifier(change, id)));
10801
+ const changed = this.bindings.isAffected(changes, updater.dependentIdentifiers, updater.trackedPaths);
10558
10802
  if (changed) {
10559
10803
  updater.applyToDOM();
10560
10804
  }
@@ -10807,6 +11051,21 @@ class VConditionalDirectiveContext {
10807
11051
  get allDependentIdentifiers() {
10808
11052
  return this.#allDependentIdentifiers;
10809
11053
  }
11054
+ /**
11055
+ * The paths the conditions of the chain read during their last evaluation, united
11056
+ * across directives, or undefined when no condition has a run-time index.
11057
+ */
11058
+ get trackedPaths() {
11059
+ let paths;
11060
+ for (const directive of this.#directives) {
11061
+ const tracked = directive.trackedPaths;
11062
+ if (tracked) {
11063
+ paths ??= new Set();
11064
+ tracked.forEach(path => paths.add(path));
11065
+ }
11066
+ }
11067
+ return paths;
11068
+ }
10810
11069
  /**
10811
11070
  * Adds a directive (v-else-if or v-else) to the conditional context.
10812
11071
  * @param directive The directive to add.
@@ -11003,11 +11262,15 @@ class VConditionalDirective {
11003
11262
  */
11004
11263
  get domUpdater() {
11005
11264
  const identifiers = this.#conditionalContext.allDependentIdentifiers;
11265
+ const conditionalContext = this.#conditionalContext;
11006
11266
  // Create an updater that handles the conditional rendering
11007
11267
  const updater = {
11008
11268
  get dependentIdentifiers() {
11009
11269
  return identifiers;
11010
11270
  },
11271
+ get trackedPaths() {
11272
+ return conditionalContext.trackedPaths;
11273
+ },
11011
11274
  applyToDOM: () => {
11012
11275
  this.#render();
11013
11276
  }
@@ -11026,6 +11289,13 @@ class VConditionalDirective {
11026
11289
  get dependentIdentifiers() {
11027
11290
  return this.#evaluator?.dependentIdentifiers ?? [];
11028
11291
  }
11292
+ /**
11293
+ * The paths the condition read during its last evaluation, when it has an index
11294
+ * known only at run time (see ExpressionEvaluator.trackedPaths).
11295
+ */
11296
+ get trackedPaths() {
11297
+ return this.#evaluator?.trackedPaths;
11298
+ }
11029
11299
  /**
11030
11300
  * The context for managing related conditional directives (v-if, v-else-if, v-else).
11031
11301
  */
@@ -11392,11 +11662,15 @@ class VForDirective {
11392
11662
  */
11393
11663
  get domUpdater() {
11394
11664
  const identifiers = this.#sourceEvaluator?.dependentIdentifiers ?? [];
11665
+ const sourceEvaluator = this.#sourceEvaluator;
11395
11666
  // Create and return the DOM updater
11396
11667
  const updater = {
11397
11668
  get dependentIdentifiers() {
11398
11669
  return identifiers;
11399
11670
  },
11671
+ get trackedPaths() {
11672
+ return sourceEvaluator?.trackedPaths;
11673
+ },
11400
11674
  applyToDOM: () => {
11401
11675
  this.#render();
11402
11676
  }
@@ -12116,6 +12390,9 @@ class VHtmlDirective {
12116
12390
  get dependentIdentifiers() {
12117
12391
  return identifiers;
12118
12392
  },
12393
+ get trackedPaths() {
12394
+ return evaluator.trackedPaths;
12395
+ },
12119
12396
  applyToDOM: () => {
12120
12397
  const element = vNode.node;
12121
12398
  const htmlContent = evaluator.evaluate() ?? '';
@@ -12330,7 +12607,7 @@ class VIntersectionDirective {
12330
12607
  const identifiers = optionsDirective.dependentIdentifiers;
12331
12608
  const baseArgs = [];
12332
12609
  for (const id of identifiers) {
12333
- const base = id.split('.')[0];
12610
+ const base = ExpressionUtils.baseIdentifier(id);
12334
12611
  if (!baseArgs.includes(base))
12335
12612
  baseArgs.push(base);
12336
12613
  }
@@ -12410,7 +12687,7 @@ class VIntersectionDirective {
12410
12687
  // Note: inline expressions receive entries and $ctx as parameters
12411
12688
  const baseArgs = [];
12412
12689
  for (const id of identifiers) {
12413
- const base = id.split('.')[0];
12690
+ const base = ExpressionUtils.baseIdentifier(id);
12414
12691
  if (!baseArgs.includes(base))
12415
12692
  baseArgs.push(base);
12416
12693
  }
@@ -12542,6 +12819,9 @@ class VModelDirective {
12542
12819
  get dependentIdentifiers() {
12543
12820
  return self.#collectDependentIdentifiers();
12544
12821
  },
12822
+ get trackedPaths() {
12823
+ return self.#evaluator?.trackedPaths;
12824
+ },
12545
12825
  applyToDOM: () => {
12546
12826
  self.#render();
12547
12827
  }
@@ -13548,7 +13828,7 @@ class VPerformanceDirective {
13548
13828
  const identifiers = optionsDirective.dependentIdentifiers;
13549
13829
  const baseArgs = [];
13550
13830
  for (const id of identifiers) {
13551
- const base = id.split('.')[0];
13831
+ const base = ExpressionUtils.baseIdentifier(id);
13552
13832
  if (!baseArgs.includes(base))
13553
13833
  baseArgs.push(base);
13554
13834
  }
@@ -13636,7 +13916,7 @@ class VPerformanceDirective {
13636
13916
  // Note: inline expressions receive entries, observer, options, and $ctx as parameters
13637
13917
  const baseArgs = [];
13638
13918
  for (const id of identifiers) {
13639
- const base = id.split('.')[0];
13919
+ const base = ExpressionUtils.baseIdentifier(id);
13640
13920
  if (!baseArgs.includes(base))
13641
13921
  baseArgs.push(base);
13642
13922
  }
@@ -13769,7 +14049,7 @@ class VResizeDirective {
13769
14049
  const identifiers = optionsDirective.dependentIdentifiers;
13770
14050
  const baseArgs = [];
13771
14051
  for (const id of identifiers) {
13772
- const base = id.split('.')[0];
14052
+ const base = ExpressionUtils.baseIdentifier(id);
13773
14053
  if (!baseArgs.includes(base))
13774
14054
  baseArgs.push(base);
13775
14055
  }
@@ -13849,7 +14129,7 @@ class VResizeDirective {
13849
14129
  // Note: inline expressions receive entries and $ctx as parameters
13850
14130
  const baseArgs = [];
13851
14131
  for (const id of identifiers) {
13852
- const base = id.split('.')[0];
14132
+ const base = ExpressionUtils.baseIdentifier(id);
13853
14133
  if (!baseArgs.includes(base))
13854
14134
  baseArgs.push(base);
13855
14135
  }
@@ -13940,6 +14220,9 @@ class VShowDirective {
13940
14220
  get dependentIdentifiers() {
13941
14221
  return identifiers;
13942
14222
  },
14223
+ get trackedPaths() {
14224
+ return evaluator.trackedPaths;
14225
+ },
13943
14226
  applyToDOM() {
13944
14227
  const shouldRender = evaluator.evaluateAsBoolean();
13945
14228
  if (shouldRender) {
@@ -14107,6 +14390,9 @@ class VTextDirective {
14107
14390
  get dependentIdentifiers() {
14108
14391
  return identifiers;
14109
14392
  },
14393
+ get trackedPaths() {
14394
+ return evaluator.trackedPaths;
14395
+ },
14110
14396
  applyToDOM() {
14111
14397
  const element = vNode.node;
14112
14398
  element.textContent = toDisplayString(evaluator.evaluate());
@@ -14269,6 +14555,9 @@ class VFocusDirective {
14269
14555
  get dependentIdentifiers() {
14270
14556
  return identifiers;
14271
14557
  },
14558
+ get trackedPaths() {
14559
+ return evaluator.trackedPaths;
14560
+ },
14272
14561
  applyToDOM: () => {
14273
14562
  const value = evaluator.evaluateAsBoolean();
14274
14563
  const previous = this.#previousValue;