@ape-egg/vibe 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.3] - 2026-01-23
4
+
5
+ ### Added
6
+
7
+ - **MutationObserver**: Now processes `<!-- if -->` and `<!-- each -->` comments when new nodes are added to DOM, enabling dynamic content to use vibe's control flow without manual processing
8
+
9
+ ### Fixed
10
+
11
+ - **affected.js**: Only marks elements as affected when their specific bound values change, preventing unnecessary DOM updates when unrelated state changes (fixes `@[color]` in `<style>` updating on every keystroke when `@[firstName]` changes)
12
+
13
+ ---
14
+
3
15
  ## [1.0.2] - 2026-01-20
4
16
 
5
17
  ### Fixed
@@ -58,13 +58,13 @@ const hasNestedStructures = (tree) => {
58
58
  */
59
59
  export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys) => {
60
60
  const templateHtml = template.element.innerHTML.trim();
61
- const escaped = templateHtml
62
- .replace(/\\/g, '\\\\')
63
- .replace(/`/g, '\\`')
64
- .replace(/\$\{/g, '\\${');
61
+ const escaped = templateHtml.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
65
62
  const code = escaped.replace(BINDING_REGEX, (_, expr) => '${' + expr + '}');
66
63
 
67
- return new Function('arr', ...stateKeys, `
64
+ return new Function(
65
+ 'arr',
66
+ ...stateKeys,
67
+ `
68
68
  let html = '';
69
69
  const len = arr.length;
70
70
  for (let ${indexAlias} = 0; ${indexAlias} < len; ${indexAlias}++) {
@@ -72,7 +72,8 @@ export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys) => {
72
72
  html += \`${code}\`;
73
73
  }
74
74
  return html;
75
- `);
75
+ `,
76
+ );
76
77
  };
77
78
 
78
79
  /**
@@ -92,7 +93,7 @@ export const renderFast = (iterationNode, array, state, parent, endComment) => {
92
93
  const stateKeys = iterationNode.runtime.stateKeys;
93
94
 
94
95
  // Build HTML string using batch function
95
- const stateValues = stateKeys.map(k => state[k]);
96
+ const stateValues = stateKeys.map((k) => state[k]);
96
97
  const html = batchFn(array, ...stateValues);
97
98
 
98
99
  // Parse with reusable template element
@@ -141,7 +142,7 @@ export const updateFast = (iterationNode, newArray, state, startComment, endComm
141
142
  // Build HTML string using cached batch function
142
143
  const batchFn = iterationNode.runtime.batchFn;
143
144
  const stateKeys = iterationNode.runtime.stateKeys;
144
- const stateValues = stateKeys.map(k => state[k]);
145
+ const stateValues = stateKeys.map((k) => state[k]);
145
146
  const html = batchFn(newArray, ...stateValues);
146
147
 
147
148
  // Parse and insert
package/affected.js CHANGED
@@ -1,6 +1,6 @@
1
- import { resolvePath, deepEqual } from './iteration-utils.js'
2
- import { extractDependencies } from './conditionals.js'
3
- import { BINDING_REGEX } from './constants.js'
1
+ import { resolvePath, deepEqual } from './iteration-utils.js';
2
+ import { extractDependencies } from './conditionals.js';
3
+ import { BINDING_REGEX } from './constants.js';
4
4
 
5
5
  // Evaluate conditional expression
6
6
  const evaluateCondition = (expression, state) => {
@@ -29,7 +29,7 @@ const recursive = (tree, state, newState, affected) => {
29
29
  affected.push({
30
30
  type: 'iteration',
31
31
  node: tree,
32
- changeType: 'array'
32
+ changeType: 'array',
33
33
  });
34
34
  }
35
35
  return affected;
@@ -45,7 +45,7 @@ const recursive = (tree, state, newState, affected) => {
45
45
  affected.push({
46
46
  type: 'conditional',
47
47
  node: tree,
48
- changeType: 'expression'
48
+ changeType: 'expression',
49
49
  });
50
50
  return affected;
51
51
  }
@@ -70,21 +70,37 @@ const recursive = (tree, state, newState, affected) => {
70
70
  if (matches.length) {
71
71
  const shallowState = Object.keys(state);
72
72
  const shallowNewState = Object.keys(newState);
73
+ const isInitialHydration = state === newState;
73
74
 
74
75
  let hasAffected = false;
75
76
  const checkedMatches = [];
76
77
 
77
78
  for (const m of matches) {
78
- const noMatch = !shallowState.some(key => matchesKey(m.inner, key));
79
- const newMatches = shallowNewState.filter(key => matchesKey(m.inner, key));
79
+ const noMatch = !shallowState.some((key) => matchesKey(m.inner, key));
80
+
81
+ let shouldAffect = false;
82
+ let relevantKeys = [];
83
+
84
+ if (isInitialHydration) {
85
+ // Initial hydration: affect all matched keys
86
+ relevantKeys = shallowNewState.filter((key) => matchesKey(m.inner, key));
87
+ shouldAffect = noMatch || relevantKeys.length > 0;
88
+ } else {
89
+ // Update: only affect if value changed
90
+ const changedKeys = shallowNewState.filter((key) =>
91
+ matchesKey(m.inner, key) && state[key] !== newState[key]
92
+ );
93
+ relevantKeys = changedKeys.length > 0 ? changedKeys : shallowState.filter((key) => matchesKey(m.inner, key));
94
+ shouldAffect = noMatch || changedKeys.length > 0;
95
+ }
80
96
 
81
- if (noMatch || newMatches.length) {
97
+ if (shouldAffect) {
82
98
  hasAffected = true;
83
99
  }
84
100
 
85
101
  checkedMatches.push({
86
102
  ...m,
87
- matches: newMatches.length ? newMatches : shallowState.filter(key => matchesKey(m.inner, key))
103
+ matches: relevantKeys,
88
104
  });
89
105
  }
90
106
 
@@ -95,7 +111,7 @@ const recursive = (tree, state, newState, affected) => {
95
111
  matchInner: m.inner,
96
112
  input: m.input,
97
113
  matches: m.matches,
98
- element: tree.element
114
+ element: tree.element,
99
115
  });
100
116
  }
101
117
  }
@@ -105,6 +121,7 @@ const recursive = (tree, state, newState, affected) => {
105
121
  if (tree.attributes) {
106
122
  const shallowState = Object.keys(state);
107
123
  const shallowNewState = Object.keys(newState);
124
+ const isInitialHydration = state === newState;
108
125
 
109
126
  for (const [attrName, attrValue] of Object.entries(tree.attributes)) {
110
127
  BINDING_REGEX.lastIndex = 0;
@@ -115,17 +132,30 @@ const recursive = (tree, state, newState, affected) => {
115
132
  }
116
133
 
117
134
  for (const m of attrMatches) {
118
- const noMatch = !shallowState.some(key => matchesKey(m.inner, key));
119
- const newMatches = shallowNewState.filter(key => matchesKey(m.inner, key));
135
+ const noMatch = !shallowState.some((key) => matchesKey(m.inner, key));
136
+
137
+ let shouldAffect = false;
138
+
139
+ if (isInitialHydration) {
140
+ // Initial hydration: affect all matched keys
141
+ const newMatches = shallowNewState.filter((key) => matchesKey(m.inner, key));
142
+ shouldAffect = noMatch || newMatches.length > 0;
143
+ } else {
144
+ // Update: only affect if value changed
145
+ const changedKeys = shallowNewState.filter((key) =>
146
+ matchesKey(m.inner, key) && state[key] !== newState[key]
147
+ );
148
+ shouldAffect = noMatch || changedKeys.length > 0;
149
+ }
120
150
 
121
- if (noMatch || newMatches.length) {
151
+ if (shouldAffect) {
122
152
  affected.push({
123
153
  type: 'attribute',
124
154
  attrName,
125
155
  attrValue,
126
156
  matchOuter: m.outer,
127
157
  matchInner: m.inner,
128
- element: tree.element
158
+ element: tree.element,
129
159
  });
130
160
  }
131
161
  }
@@ -146,4 +176,4 @@ const recursive = (tree, state, newState, affected) => {
146
176
  return affected;
147
177
  };
148
178
 
149
- export default (tree, state, newState) => recursive(tree, state, newState, [])
179
+ export default (tree, state, newState) => recursive(tree, state, newState, []);
package/conditionals.js CHANGED
@@ -44,7 +44,7 @@ export const renderAllConditionals = (tree, state, linkList, parentScope = {}) =
44
44
 
45
45
  // Recursively render conditionals in child nodes
46
46
  if (tree.children) {
47
- Object.keys(tree.children).forEach(key => {
47
+ Object.keys(tree.children).forEach((key) => {
48
48
  const child = tree.children[key];
49
49
  if (typeof child === 'object' && child !== null) {
50
50
  renderAllConditionals(child, state, linkList, parentScope);
@@ -104,15 +104,18 @@ const mountBranch = (node, branchData, state, linkList, parentScope) => {
104
104
  const parent = startComment.parentNode;
105
105
 
106
106
  // Create scoped state (with parent scope if inside iteration)
107
- const scopedState = Object.keys(parentScope).length > 0
108
- ? createScopedState(state, parentScope)
109
- : state;
107
+ const scopedState =
108
+ Object.keys(parentScope).length > 0 ? createScopedState(state, parentScope) : state;
110
109
 
111
110
  // Initialize block (clone, parse, hydrate)
112
- const { element: firstElement, tree: branchTree, clonedNodes } = initializeBlock(templateContent, scopedState);
111
+ const {
112
+ element: firstElement,
113
+ tree: branchTree,
114
+ clonedNodes,
115
+ } = initializeBlock(templateContent, scopedState);
113
116
 
114
117
  // Insert cloned nodes into DOM
115
- clonedNodes.forEach(clonedNode => parent.insertBefore(clonedNode, endComment));
118
+ clonedNodes.forEach((clonedNode) => parent.insertBefore(clonedNode, endComment));
116
119
 
117
120
  // Recursively render any nested iterations and conditionals
118
121
  if (branchTree) {
@@ -124,7 +127,7 @@ const mountBranch = (node, branchData, state, linkList, parentScope) => {
124
127
  node.runtime.activeInstance = {
125
128
  branch: branchData,
126
129
  nodes: clonedNodes,
127
- parsedTree: branchTree
130
+ parsedTree: branchTree,
128
131
  };
129
132
  };
130
133
 
@@ -135,7 +138,7 @@ const unmountBranch = (node) => {
135
138
  if (!activeInstance) return;
136
139
 
137
140
  // Remove all nodes from DOM
138
- activeInstance.nodes.forEach(domNode => {
141
+ activeInstance.nodes.forEach((domNode) => {
139
142
  if (domNode.parentNode) {
140
143
  domNode.parentNode.removeChild(domNode);
141
144
  }
@@ -170,13 +173,11 @@ export const updateConditional = (node, newState, oldState, linkList, parentScop
170
173
  const { activeInstance } = node.runtime;
171
174
 
172
175
  if (activeInstance && activeInstance.parsedTree) {
173
- const scopedState = Object.keys(parentScope).length > 0
174
- ? createScopedState(newState, parentScope)
175
- : newState;
176
+ const scopedState =
177
+ Object.keys(parentScope).length > 0 ? createScopedState(newState, parentScope) : newState;
176
178
 
177
179
  const affectedElements = affected(activeInstance.parsedTree, oldState, scopedState);
178
180
  hydrate(affectedElements, scopedState);
179
181
  }
180
182
  }
181
183
  };
182
-
package/constants.js CHANGED
@@ -1,56 +1,137 @@
1
1
  // Elements that should not have reactive bindings
2
- export const NON_REACTIVE_ELEMENTS = ["SCRIPT", "HEAD", "PRE"];
2
+ export const NON_REACTIVE_ELEMENTS = ['SCRIPT', 'HEAD', 'PRE'];
3
3
 
4
4
  // Attributes where the string value is meaningful (should NOT be removed when falsy)
5
5
  // All other attributes are treated as boolean-like (removed when falsy, present when truthy)
6
6
  export const VALUE_ATTRS = [
7
7
  // Global attributes
8
- 'class', 'style', 'id', 'title', 'lang', 'dir', 'tabindex', 'accesskey',
9
- 'slot', 'part', 'is', 'nonce', 'popover', 'anchor',
8
+ 'class',
9
+ 'style',
10
+ 'id',
11
+ 'title',
12
+ 'lang',
13
+ 'dir',
14
+ 'tabindex',
15
+ 'accesskey',
16
+ 'slot',
17
+ 'part',
18
+ 'is',
19
+ 'nonce',
20
+ 'popover',
21
+ 'anchor',
10
22
 
11
23
  // Enumerated (take specific string values, not truly boolean)
12
- 'contenteditable', 'draggable', 'spellcheck', 'translate', 'autocapitalize',
13
- 'inputmode', 'enterkeyhint', 'virtualkeyboardpolicy',
24
+ 'contenteditable',
25
+ 'draggable',
26
+ 'spellcheck',
27
+ 'translate',
28
+ 'autocapitalize',
29
+ 'inputmode',
30
+ 'enterkeyhint',
31
+ 'virtualkeyboardpolicy',
14
32
 
15
33
  // URLs and sources
16
- 'href', 'src', 'action', 'cite', 'data', 'poster', 'srcset', 'imagesrcset',
17
- 'formaction', 'ping', 'usemap', 'manifest', 'codebase',
34
+ 'href',
35
+ 'src',
36
+ 'action',
37
+ 'cite',
38
+ 'data',
39
+ 'poster',
40
+ 'srcset',
41
+ 'imagesrcset',
42
+ 'formaction',
43
+ 'ping',
44
+ 'usemap',
45
+ 'manifest',
46
+ 'codebase',
18
47
 
19
48
  // Form attributes
20
- 'name', 'type', 'value', 'placeholder', 'pattern', 'min', 'max', 'step',
21
- 'minlength', 'maxlength', 'size', 'accept', 'autocomplete', 'list', 'form',
22
- 'formmethod', 'formtarget', 'formenctype', 'wrap', 'method', 'enctype',
23
- 'for', 'dirname',
49
+ 'name',
50
+ 'type',
51
+ 'value',
52
+ 'placeholder',
53
+ 'pattern',
54
+ 'min',
55
+ 'max',
56
+ 'step',
57
+ 'minlength',
58
+ 'maxlength',
59
+ 'size',
60
+ 'accept',
61
+ 'autocomplete',
62
+ 'list',
63
+ 'form',
64
+ 'formmethod',
65
+ 'formtarget',
66
+ 'formenctype',
67
+ 'wrap',
68
+ 'method',
69
+ 'enctype',
70
+ 'for',
71
+ 'dirname',
24
72
 
25
73
  // Text/accessibility
26
- 'alt', 'label', 'summary', 'abbr',
74
+ 'alt',
75
+ 'label',
76
+ 'summary',
77
+ 'abbr',
27
78
 
28
79
  // Dimensions and layout
29
- 'width', 'height', 'cols', 'rows', 'span', 'rowspan', 'colspan',
30
- 'low', 'high', 'optimum',
80
+ 'width',
81
+ 'height',
82
+ 'cols',
83
+ 'rows',
84
+ 'span',
85
+ 'rowspan',
86
+ 'colspan',
87
+ 'low',
88
+ 'high',
89
+ 'optimum',
31
90
 
32
91
  // Link/resource hints
33
- 'target', 'rel', 'hreflang', 'download', 'as', 'media', 'type', 'charset',
34
- 'crossorigin', 'integrity', 'loading', 'decoding', 'fetchpriority',
35
- 'referrerpolicy', 'blocking', 'imagesizes', 'sizes',
92
+ 'target',
93
+ 'rel',
94
+ 'hreflang',
95
+ 'download',
96
+ 'as',
97
+ 'media',
98
+ 'type',
99
+ 'charset',
100
+ 'crossorigin',
101
+ 'integrity',
102
+ 'loading',
103
+ 'decoding',
104
+ 'fetchpriority',
105
+ 'referrerpolicy',
106
+ 'blocking',
107
+ 'imagesizes',
108
+ 'sizes',
36
109
 
37
110
  // Media
38
- 'preload', 'kind', 'srclang',
111
+ 'preload',
112
+ 'kind',
113
+ 'srclang',
39
114
 
40
115
  // Meta
41
- 'content', 'http-equiv',
116
+ 'content',
117
+ 'http-equiv',
42
118
 
43
119
  // iframe/embed
44
- 'sandbox', 'allow', 'srcdoc', 'credentialless',
120
+ 'sandbox',
121
+ 'allow',
122
+ 'srcdoc',
123
+ 'credentialless',
45
124
 
46
125
  // Table
47
- 'headers', 'scope',
126
+ 'headers',
127
+ 'scope',
48
128
 
49
129
  // Datetime
50
130
  'datetime',
51
131
 
52
132
  // Object/embed legacy
53
- 'coords', 'shape',
133
+ 'coords',
134
+ 'shape',
54
135
  ];
55
136
 
56
137
  // Properties that should be set directly on the DOM element (not as attributes)
package/hydrate.js CHANGED
@@ -1,6 +1,6 @@
1
- import { updateIteration } from './iterate.js'
2
- import { updateConditional } from './conditionals.js'
3
- import { VALUE_ATTRS, DOM_PROPERTIES, BINDING_REGEX, PURE_BINDING_REGEX } from './constants.js'
1
+ import { updateIteration } from './iterate.js';
2
+ import { updateConditional } from './conditionals.js';
3
+ import { VALUE_ATTRS, DOM_PROPERTIES, BINDING_REGEX, PURE_BINDING_REGEX } from './constants.js';
4
4
 
5
5
  // Keep track of old state for diffing
6
6
  let previousState = {};
@@ -40,12 +40,12 @@ export default (affected, state, linkList = {}) => {
40
40
  if (aff.type === 'attribute') {
41
41
  const { attrName, attrValue, element } = aff;
42
42
  try {
43
-
44
43
  // Check if this is a pure binding (e.g., value="@[inputValue]")
45
44
  const isPureBinding = attrValue.match(PURE_BINDING_REGEX);
46
45
  const isDomProperty = DOM_PROPERTIES.includes(attrName);
47
46
  // Value attrs keep their string value; everything else is boolean-like (removed when falsy)
48
- const isValueAttr = VALUE_ATTRS.includes(attrName) ||
47
+ const isValueAttr =
48
+ VALUE_ATTRS.includes(attrName) ||
49
49
  attrName.startsWith('data-') ||
50
50
  attrName.startsWith('aria-') ||
51
51
  attrName.startsWith('on');
@@ -97,4 +97,4 @@ export default (affected, state, linkList = {}) => {
97
97
 
98
98
  // Update previous state for next diff
99
99
  setPreviousState(state);
100
- }
100
+ };
package/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import state from "./state.js";
2
- import parse from "./parse.js";
3
- import link from "./link.js";
4
- import hydrate, { setPreviousState } from "./hydrate.js";
5
- import affected from "./affected.js";
6
- import { deepMerge, hash } from "./utils.js";
7
- import { renderAllIterations, setRenderAllConditionals } from "./iterate.js";
8
- import { renderAllConditionals } from "./conditionals.js";
9
- import { NON_REACTIVE_ELEMENTS } from "./constants.js";
1
+ import state from './state.js';
2
+ import parse from './parse.js';
3
+ import link from './link.js';
4
+ import hydrate, { setPreviousState } from './hydrate.js';
5
+ import affected from './affected.js';
6
+ import { deepMerge, hash } from './utils.js';
7
+ import { renderAllIterations, setRenderAllConditionals } from './iterate.js';
8
+ import { renderAllConditionals } from './conditionals.js';
9
+ import { NON_REACTIVE_ELEMENTS } from './constants.js';
10
10
 
11
11
  // Wire up cross-module dependency after all modules are loaded
12
12
  setRenderAllConditionals(renderAllConditionals);
@@ -29,12 +29,12 @@ const isNonReactiveOrInside = (node) => {
29
29
  // Navigate tree using dot notation (handles .children at each level)
30
30
  const navigateTree = (tree, path) => {
31
31
  if (!path) return tree;
32
- return path.split(".").reduce((node, key) => node?.children?.[key], tree);
32
+ return path.split('.').reduce((node, key) => node?.children?.[key], tree);
33
33
  };
34
34
 
35
35
  // Get or create a node in the tree at the given path
36
36
  const ensureNode = (tree, path) => {
37
- const keys = path.split(".");
37
+ const keys = path.split('.');
38
38
  return keys.reduce((node, key) => {
39
39
  if (!node.children[key]) {
40
40
  node.children[key] = { children: {} };
@@ -43,14 +43,12 @@ const ensureNode = (tree, path) => {
43
43
  }, tree);
44
44
  };
45
45
 
46
- const main = (s, attrName = "vibe") => {
46
+ const main = (s, attrName = 'vibe') => {
47
47
  // Find element(s) with the specified attribute
48
48
  const elements = document.querySelectorAll(`[${attrName}]`);
49
49
 
50
50
  if (elements.length === 0) {
51
- console.info(
52
- `[vibe] No element found with attribute "${attrName}". Falling back to body.`,
53
- );
51
+ console.info(`[vibe] No element found with attribute "${attrName}". Falling back to body.`);
54
52
  } else if (elements.length > 1) {
55
53
  console.info(
56
54
  `[vibe] Multiple elements (${elements.length}) found with attribute "${attrName}". Hydrating the first one.`,
@@ -90,9 +88,7 @@ const main = (s, attrName = "vibe") => {
90
88
 
91
89
  const prev = structuredClone(previousState);
92
90
  previousState = { ...$, ...newState };
93
- hooks.afterUpdate.forEach((callback) =>
94
- callback(structuredClone({ ...$ }), prev),
95
- );
91
+ hooks.afterUpdate.forEach((callback) => callback(structuredClone({ ...$ }), prev));
96
92
  });
97
93
 
98
94
  // Add hook subscription method (non-enumerable so it won't be spread/cloned with state)
@@ -101,7 +97,7 @@ const main = (s, attrName = "vibe") => {
101
97
  if (hooks[event]) {
102
98
  hooks[event].push(callback);
103
99
  }
104
- return () => hooks[event] = hooks[event].filter((cb) => cb !== callback);
100
+ return () => (hooks[event] = hooks[event].filter((cb) => cb !== callback));
105
101
  },
106
102
  enumerable: false,
107
103
  });
@@ -126,9 +122,7 @@ const main = (s, attrName = "vibe") => {
126
122
  mutations.forEach(({ addedNodes, removedNodes, target }) => {
127
123
  // Process removed nodes first (cleanup before additions)
128
124
  removedNodes.forEach((node) => {
129
- const entry = Object.entries(linkList).find(
130
- ([_, element]) => element === node,
131
- );
125
+ const entry = Object.entries(linkList).find(([_, element]) => element === node);
132
126
 
133
127
  // Skip nodes that aren't tracked (e.g., iteration-generated nodes or nodes outside reactive scope)
134
128
  if (!entry) return;
@@ -136,9 +130,9 @@ const main = (s, attrName = "vibe") => {
136
130
  const [dotAnnotation] = entry;
137
131
  delete linkList[dotAnnotation];
138
132
 
139
- const dotPath = dotAnnotation.split(".");
133
+ const dotPath = dotAnnotation.split('.');
140
134
  const name = dotPath.pop();
141
- const parentDotAnnotation = dotPath.join(".");
135
+ const parentDotAnnotation = dotPath.join('.');
142
136
 
143
137
  const picked = navigateTree(parsedTree, parentDotAnnotation);
144
138
 
@@ -165,9 +159,7 @@ const main = (s, attrName = "vibe") => {
165
159
  return;
166
160
  }
167
161
 
168
- const entry = Object.entries(linkList).find(
169
- ([_, element]) => element === target,
170
- );
162
+ const entry = Object.entries(linkList).find(([_, element]) => element === target);
171
163
 
172
164
  // If parent isn't tracked, this node is outside the reactive scope
173
165
  if (!entry) return;
@@ -206,6 +198,10 @@ const main = (s, attrName = "vibe") => {
206
198
 
207
199
  hydrate(affectedElements, $, linkList);
208
200
 
201
+ // Process iterations and conditionals in the newly added node
202
+ renderAllIterations(parsedNode, $, linkList);
203
+ renderAllConditionals(parsedNode, $, linkList);
204
+
209
205
  hadChanges = true;
210
206
  });
211
207
  });
package/iterate.js CHANGED
@@ -32,7 +32,7 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
32
32
  parsed: originalTree.parsed,
33
33
  element: clonedRoot,
34
34
  children: {},
35
- ...(originalTree.attributes && { attributes: originalTree.attributes })
35
+ ...(originalTree.attributes && { attributes: originalTree.attributes }),
36
36
  };
37
37
 
38
38
  if (!originalTree.children) return cloned;
@@ -50,7 +50,9 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
50
50
 
51
51
  // Find corresponding comments in cloned DOM
52
52
  const clonedStart = findComment(clonedChildNodes, startComment.textContent);
53
- const clonedElse = elseComment ? findComment(clonedChildNodes, elseComment.textContent) : null;
53
+ const clonedElse = elseComment
54
+ ? findComment(clonedChildNodes, elseComment.textContent)
55
+ : null;
54
56
  const clonedEnd = findComment(clonedChildNodes, endComment.textContent);
55
57
 
56
58
  cloned.children[key] = {
@@ -60,14 +62,14 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
60
62
  startComment: clonedStart || startComment,
61
63
  elseComment: clonedElse,
62
64
  endComment: clonedEnd || endComment,
63
- branches // Branch templates are reused (they're cloned during mount)
65
+ branches, // Branch templates are reused (they're cloned during mount)
64
66
  },
65
67
  runtime: {
66
68
  activeBranch: undefined,
67
69
  activeInstance: null,
68
- templateRemoved: false
70
+ templateRemoved: false,
69
71
  },
70
- children: {}
72
+ children: {},
71
73
  };
72
74
  continue;
73
75
  }
@@ -88,13 +90,13 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
88
90
  indexAlias: child.meta.indexAlias,
89
91
  startComment: clonedStart || startComment,
90
92
  endComment: clonedEnd || endComment,
91
- template // Template is reused (cloned during render)
93
+ template, // Template is reused (cloned during render)
92
94
  },
93
95
  runtime: {
94
96
  instances: [],
95
- templateRemoved: false
97
+ templateRemoved: false,
96
98
  },
97
- children: {}
99
+ children: {},
98
100
  };
99
101
  continue;
100
102
  }
@@ -150,7 +152,7 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
150
152
  // Fallback for text-only templates
151
153
  if (!firstElement && clonedNodes.length > 0) {
152
154
  const container = document.createElement('span');
153
- clonedNodes.forEach(node => container.appendChild(node.cloneNode(true)));
155
+ clonedNodes.forEach((node) => container.appendChild(node.cloneNode(true)));
154
156
  firstElement = container;
155
157
  }
156
158
 
@@ -165,7 +167,7 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
165
167
  return {
166
168
  element: firstElement,
167
169
  tree,
168
- clonedNodes
170
+ clonedNodes,
169
171
  };
170
172
  };
171
173
 
@@ -222,7 +224,7 @@ export const createScopedState = (globalState, localVars, parentScope = {}) => {
222
224
  return { configurable: true, enumerable: true, value: parentScope[prop] };
223
225
  }
224
226
  return Reflect.getOwnPropertyDescriptor(target, prop);
225
- }
227
+ },
226
228
  });
227
229
  };
228
230
 
@@ -247,11 +249,14 @@ export const renderAllIterations = (tree, state, linkList, parentScope = {}) =>
247
249
 
248
250
  // Callback for rendering conditionals - set by conditionals.js to avoid circular import
249
251
  let _renderAllConditionals = () => {};
250
- export const setRenderAllConditionals = (fn) => { _renderAllConditionals = fn; };
252
+ export const setRenderAllConditionals = (fn) => {
253
+ _renderAllConditionals = fn;
254
+ };
251
255
 
252
256
  // Initial render of an iteration block
253
257
  export const renderIteration = (iterationNode, state, linkList, parentScope = {}) => {
254
- const { arrayPath, itemAlias, indexAlias, template, startComment, endComment } = iterationNode.meta;
258
+ const { arrayPath, itemAlias, indexAlias, template, startComment, endComment } =
259
+ iterationNode.meta;
255
260
 
256
261
  // Already rendered - updates go through updateIteration
257
262
  if (iterationNode.runtime.instances?.length > 0) return;
@@ -294,7 +299,7 @@ export const renderIteration = (iterationNode, state, linkList, parentScope = {}
294
299
  const { element, tree, clonedNodes } = initializeBlock(templateNodes, scopedState, template);
295
300
 
296
301
  // Insert cloned nodes
297
- clonedNodes.forEach(node => parent.insertBefore(node, endComment));
302
+ clonedNodes.forEach((node) => parent.insertBefore(node, endComment));
298
303
 
299
304
  // Recursively render nested iterations and conditionals
300
305
  if (tree) {
@@ -323,7 +328,11 @@ export const updateIteration = (iterationNode, newState, oldState, linkList, par
323
328
  const isFullToEmpty = oldArray.length > 0 && newArray.length === 0;
324
329
  const isLargeArray = newArray.length > 100 || oldArray.length > 100;
325
330
 
326
- if (window.__VIBE_FAST_ITERATION__ && fastPath.canUseFastPath(template) && (isEmptyToFull || isFullToEmpty || isLargeArray)) {
331
+ if (
332
+ window.__VIBE_FAST_ITERATION__ &&
333
+ fastPath.canUseFastPath(template) &&
334
+ (isEmptyToFull || isFullToEmpty || isLargeArray)
335
+ ) {
327
336
  fastPath.updateFast(iterationNode, newArray, newState, startComment, endComment);
328
337
  return;
329
338
  }
@@ -333,16 +342,26 @@ export const updateIteration = (iterationNode, newState, oldState, linkList, par
333
342
  const newKeys = newArray.map((item, i) => getItemKey(item, i));
334
343
  const operations = computeDiff(oldKeys, newKeys, oldArray, newArray);
335
344
 
336
- operations.forEach(op => {
345
+ operations.forEach((op) => {
337
346
  switch (op.type) {
338
- case 'REMOVE': removeInstance(iterationNode, op.index); break;
339
- case 'ADD': addInstance(iterationNode, op.item, op.index, newState, linkList, parentScope); break;
340
- case 'MOVE': moveInstance(iterationNode, op.from, op.to); break;
341
- case 'UPDATE': updateInstance(iterationNode, op.index, op.item, newState, linkList, parentScope); break;
347
+ case 'REMOVE':
348
+ removeInstance(iterationNode, op.index);
349
+ break;
350
+ case 'ADD':
351
+ addInstance(iterationNode, op.item, op.index, newState, linkList, parentScope);
352
+ break;
353
+ case 'MOVE':
354
+ moveInstance(iterationNode, op.from, op.to);
355
+ break;
356
+ case 'UPDATE':
357
+ updateInstance(iterationNode, op.index, op.item, newState, linkList, parentScope);
358
+ break;
342
359
  }
343
360
  });
344
361
 
345
- iterationNode.runtime.instances.forEach((inst, i) => { inst.index = i; });
362
+ iterationNode.runtime.instances.forEach((inst, i) => {
363
+ inst.index = i;
364
+ });
346
365
  };
347
366
 
348
367
  // Add a new instance at the specified index
@@ -356,12 +375,14 @@ const addInstance = (iterationNode, item, index, state, linkList, parentScope) =
356
375
  const { element, tree, clonedNodes } = initializeBlock(templateNodes, scopedState, template);
357
376
 
358
377
  // Find insertion point
359
- const insertBefore = index < iterationNode.runtime.instances.length
360
- ? iterationNode.runtime.instances[index].clonedNodes?.[0] || iterationNode.runtime.instances[index].element
361
- : endComment;
378
+ const insertBefore =
379
+ index < iterationNode.runtime.instances.length
380
+ ? iterationNode.runtime.instances[index].clonedNodes?.[0] ||
381
+ iterationNode.runtime.instances[index].element
382
+ : endComment;
362
383
 
363
384
  // Insert cloned nodes
364
- clonedNodes.forEach(node => startComment.parentNode.insertBefore(node, insertBefore));
385
+ clonedNodes.forEach((node) => startComment.parentNode.insertBefore(node, insertBefore));
365
386
 
366
387
  // Recursively render nested iterations and conditionals
367
388
  if (tree) {
@@ -380,7 +401,9 @@ const removeInstance = (iterationNode, index) => {
380
401
  const instance = iterationNode.runtime.instances[index];
381
402
 
382
403
  // Remove all cloned nodes from DOM
383
- (instance.clonedNodes || [instance.element]).forEach(node => node?.parentNode?.removeChild(node));
404
+ (instance.clonedNodes || [instance.element]).forEach((node) =>
405
+ node?.parentNode?.removeChild(node),
406
+ );
384
407
 
385
408
  iterationNode.runtime.instances.splice(index, 1);
386
409
  };
@@ -401,11 +424,11 @@ const moveInstance = (iterationNode, fromIndex, toIndex) => {
401
424
  // Find new insertion point
402
425
  const nextInstance = iterationNode.runtime.instances[toIndex + 1];
403
426
  const insertBefore = nextInstance
404
- ? (nextInstance.clonedNodes?.[0] || nextInstance.element)
427
+ ? nextInstance.clonedNodes?.[0] || nextInstance.element
405
428
  : iterationNode.meta.endComment;
406
429
 
407
430
  // Move all nodes
408
- nodes.forEach(node => parent.insertBefore(node, insertBefore));
431
+ nodes.forEach((node) => parent.insertBefore(node, insertBefore));
409
432
  };
410
433
 
411
434
  // Update an instance with new item data
@@ -426,8 +449,8 @@ const updateInstance = (iterationNode, index, newItem, state, linkList, parentSc
426
449
  const insertBefore = oldNodes[oldNodes.length - 1]?.nextSibling;
427
450
  const parent = oldNodes[0]?.parentNode;
428
451
 
429
- oldNodes.forEach(node => node?.parentNode?.removeChild(node));
430
- clonedNodes.forEach(node => parent.insertBefore(node, insertBefore));
452
+ oldNodes.forEach((node) => node?.parentNode?.removeChild(node));
453
+ clonedNodes.forEach((node) => parent.insertBefore(node, insertBefore));
431
454
 
432
455
  // Recursively render nested iterations and conditionals
433
456
  if (tree) {
@@ -447,5 +470,5 @@ export default {
447
470
  renderIteration,
448
471
  updateIteration,
449
472
  renderAllIterations,
450
- createScopedState
473
+ createScopedState,
451
474
  };
@@ -4,7 +4,7 @@ import { ITERATION_START_REGEX, CONDITIONAL_START_REGEX } from './constants.js';
4
4
  // Resolve nested paths in state (e.g., "user.items" -> state.user.items)
5
5
  export const resolvePath = (obj, path) => {
6
6
  if (!path || !obj) return undefined;
7
- return path.split(".").reduce((acc, part) => acc?.[part], obj);
7
+ return path.split('.').reduce((acc, part) => acc?.[part], obj);
8
8
  };
9
9
 
10
10
  // Clone template element preserving structure
@@ -16,11 +16,11 @@ export const cloneTemplate = (templateElement) => {
16
16
  export const findEndComment = (nodes, startIndex) => {
17
17
  let depth = 1;
18
18
  for (let i = startIndex; i < nodes.length; i++) {
19
- if (nodes[i].nodeName === "#comment") {
19
+ if (nodes[i].nodeName === '#comment') {
20
20
  const text = nodes[i].textContent.trim();
21
21
  if (ITERATION_START_REGEX.test(text)) {
22
22
  depth++;
23
- } else if (text === "/each") {
23
+ } else if (text === '/each') {
24
24
  depth--;
25
25
  if (depth === 0) {
26
26
  return i;
@@ -28,7 +28,7 @@ export const findEndComment = (nodes, startIndex) => {
28
28
  }
29
29
  }
30
30
  }
31
- throw new Error("Unmatched <!-- each --> comment: missing <!-- /each -->");
31
+ throw new Error('Unmatched <!-- each --> comment: missing <!-- /each -->');
32
32
  };
33
33
 
34
34
  // Find matching <!-- /if --> and optional <!-- else --> with depth tracking
@@ -38,7 +38,7 @@ export const findConditionalEnd = (nodes, startIndex) => {
38
38
  let elseIndex = null;
39
39
 
40
40
  for (let i = startIndex; i < nodes.length; i++) {
41
- if (nodes[i].nodeName === "#comment") {
41
+ if (nodes[i].nodeName === '#comment') {
42
42
  const text = nodes[i].textContent.trim();
43
43
 
44
44
  // Check for nested if
@@ -46,11 +46,11 @@ export const findConditionalEnd = (nodes, startIndex) => {
46
46
  depth++;
47
47
  }
48
48
  // Check for else at current depth
49
- else if (text === "else" && depth === 1 && elseIndex === null) {
49
+ else if (text === 'else' && depth === 1 && elseIndex === null) {
50
50
  elseIndex = i;
51
51
  }
52
52
  // Check for /if
53
- else if (text === "/if") {
53
+ else if (text === '/if') {
54
54
  depth--;
55
55
  if (depth === 0) {
56
56
  return { elseIndex, endIndex: i };
@@ -59,13 +59,13 @@ export const findConditionalEnd = (nodes, startIndex) => {
59
59
  }
60
60
  }
61
61
 
62
- throw new Error("Unmatched <!-- if --> comment: missing <!-- /if -->");
62
+ throw new Error('Unmatched <!-- if --> comment: missing <!-- /if -->');
63
63
  };
64
64
 
65
65
  // Generate stable hash for objects
66
66
  export const stableHash = (obj) => {
67
- if (obj === null || obj === undefined) return "null";
68
- if (typeof obj !== "object") return String(obj);
67
+ if (obj === null || obj === undefined) return 'null';
68
+ if (typeof obj !== 'object') return String(obj);
69
69
 
70
70
  try {
71
71
  // Sort keys for stable hashing
@@ -96,7 +96,7 @@ export const deepEqual = (a, b) => {
96
96
  return a === b;
97
97
  }
98
98
 
99
- if (typeof a !== "object" || typeof b !== "object") {
99
+ if (typeof a !== 'object' || typeof b !== 'object') {
100
100
  return a === b;
101
101
  }
102
102
 
@@ -166,12 +166,12 @@ export const longestCommonSubsequence = (arr1, arr2) => {
166
166
  // Generate unique key for array items
167
167
  export const getItemKey = (item, index) => {
168
168
  // 1. If item has 'id' property, use it
169
- if (item && typeof item === "object" && "id" in item) {
169
+ if (item && typeof item === 'object' && 'id' in item) {
170
170
  return `id_${item.id}`;
171
171
  }
172
172
 
173
173
  // 2. If item is primitive, use value + index
174
- if (typeof item !== "object" || item === null) {
174
+ if (typeof item !== 'object' || item === null) {
175
175
  return `val_${item}_${index}`;
176
176
  }
177
177
 
@@ -204,7 +204,7 @@ export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
204
204
  // Check if item content changed
205
205
  if (!deepEqual(oldArray[oldIdx], newArray[newIdx])) {
206
206
  operations.push({
207
- type: "UPDATE",
207
+ type: 'UPDATE',
208
208
  index: newIdx,
209
209
  oldIndex: oldIdx,
210
210
  item: newArray[newIdx],
@@ -220,7 +220,7 @@ export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
220
220
  oldKeys.forEach((key, idx) => {
221
221
  if (!processedOld.has(idx)) {
222
222
  operations.push({
223
- type: "REMOVE",
223
+ type: 'REMOVE',
224
224
  index: idx,
225
225
  key,
226
226
  });
@@ -231,7 +231,7 @@ export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
231
231
  newKeys.forEach((key, idx) => {
232
232
  if (!processedNew.has(idx)) {
233
233
  operations.push({
234
- type: "ADD",
234
+ type: 'ADD',
235
235
  index: idx,
236
236
  item: newArray[idx],
237
237
  key,
@@ -246,7 +246,7 @@ export const computeDiff = (oldKeys, newKeys, oldArray, newArray) => {
246
246
  return priority[a.type] - priority[b.type];
247
247
  }
248
248
  // For REMOVE, process from end to beginning
249
- if (a.type === "REMOVE") {
249
+ if (a.type === 'REMOVE') {
250
250
  return b.index - a.index;
251
251
  }
252
252
  // For others, process in order
package/link.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const recursive = (tree, results, tagChain) => {
2
- results[tagChain.join(".")] = tree.element;
2
+ results[tagChain.join('.')] = tree.element;
3
3
 
4
4
  if (tree.children && Object.keys(tree.children).length > 0) {
5
5
  Object.keys(tree.children).forEach((tag) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "description": "Runtime-first reactivity",
6
6
  "main": "index.js",
package/parse.js CHANGED
@@ -1,18 +1,23 @@
1
- import { hash } from './utils.js'
2
- import { findEndComment, findConditionalEnd } from './iteration-utils.js'
3
- import { NON_REACTIVE_ELEMENTS, BINDING_REGEX, ITERATION_REGEX, CONDITIONAL_REGEX } from './constants.js'
1
+ import { hash } from './utils.js';
2
+ import { findEndComment, findConditionalEnd } from './iteration-utils.js';
3
+ import {
4
+ NON_REACTIVE_ELEMENTS,
5
+ BINDING_REGEX,
6
+ ITERATION_REGEX,
7
+ CONDITIONAL_REGEX,
8
+ } from './constants.js';
4
9
 
5
10
  const parseHTML = (children, rootKey = undefined) =>
6
11
  children.reduce((s, element, i) => {
7
- const { nodeName, textContent } = element
12
+ const { nodeName, textContent } = element;
8
13
  if (['#comment'].includes(nodeName)) {
9
- return `${s}${nodeName === '#comment' ? `asd` : textContent}`
14
+ return `${s}${nodeName === '#comment' ? `asd` : textContent}`;
10
15
  }
11
- const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName
12
- const innerNodeIdentifier = `${name}_${i}`.toLowerCase()
16
+ const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName;
17
+ const innerNodeIdentifier = `${name}_${i}`.toLowerCase();
13
18
 
14
- return rootKey || `${s}${`\$[${innerNodeIdentifier}]`}`
15
- }, '')
19
+ return rootKey || `${s}${`\$[${innerNodeIdentifier}]`}`;
20
+ }, '');
16
21
 
17
22
  const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
18
23
  let result = {};
@@ -44,7 +49,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
44
49
 
45
50
  // Create a temporary container for the template
46
51
  const templateContainer = document.createElement('div');
47
- templateNodes.forEach(node => {
52
+ templateNodes.forEach((node) => {
48
53
  templateContainer.appendChild(node.cloneNode(true));
49
54
  });
50
55
 
@@ -64,14 +69,14 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
64
69
  template: {
65
70
  parsed: parseHTML([...templateContainer.childNodes]),
66
71
  element: templateContainer,
67
- children: templateParsed
68
- }
72
+ children: templateParsed,
73
+ },
69
74
  },
70
75
  runtime: {
71
76
  instances: [],
72
- templateRemoved: false
77
+ templateRemoved: false,
73
78
  },
74
- children: {}
79
+ children: {},
75
80
  };
76
81
 
77
82
  // Mark template indices as processed
@@ -105,7 +110,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
105
110
 
106
111
  // Create temporary container for true branch
107
112
  const trueBranchContainer = document.createElement('div');
108
- trueBranchNodes.forEach(node => {
113
+ trueBranchNodes.forEach((node) => {
109
114
  trueBranchContainer.appendChild(node.cloneNode(true));
110
115
  });
111
116
 
@@ -118,7 +123,7 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
118
123
  if (elseIndex !== null) {
119
124
  const falseBranchNodes = Array.from(children).slice(elseIndex + 1, endIndex);
120
125
  falseBranchContainer = document.createElement('div');
121
- falseBranchNodes.forEach(node => {
126
+ falseBranchNodes.forEach((node) => {
122
127
  falseBranchContainer.appendChild(node.cloneNode(true));
123
128
  });
124
129
  falseBranchParsed = recursive([...falseBranchContainer.childNodes]);
@@ -137,21 +142,23 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
137
142
  if: {
138
143
  parsed: parseHTML([...trueBranchContainer.childNodes]),
139
144
  element: trueBranchContainer,
140
- children: trueBranchParsed
145
+ children: trueBranchParsed,
141
146
  },
142
- else: falseBranchContainer ? {
143
- parsed: parseHTML([...falseBranchContainer.childNodes]),
144
- element: falseBranchContainer,
145
- children: falseBranchParsed
146
- } : null
147
- }
147
+ else: falseBranchContainer
148
+ ? {
149
+ parsed: parseHTML([...falseBranchContainer.childNodes]),
150
+ element: falseBranchContainer,
151
+ children: falseBranchParsed,
152
+ }
153
+ : null,
154
+ },
148
155
  },
149
156
  runtime: {
150
157
  activeBranch: undefined,
151
158
  activeInstance: null,
152
- templateRemoved: false
159
+ templateRemoved: false,
153
160
  },
154
- children: {}
161
+ children: {},
155
162
  };
156
163
 
157
164
  // Mark template indices as processed
@@ -199,23 +206,23 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
199
206
  parsed,
200
207
  element,
201
208
  children: recursive(iteratableChildren),
202
- ...(hasAttributeBindings && { attributes })
209
+ ...(hasAttributeBindings && { attributes }),
203
210
  };
204
211
  } else {
205
212
  result[nodeIdentifier] = {
206
213
  parsed: innerHTML || textContent,
207
214
  element,
208
215
  children: {},
209
- ...(hasAttributeBindings && { attributes })
216
+ ...(hasAttributeBindings && { attributes }),
210
217
  };
211
218
  }
212
219
  }
213
220
 
214
221
  return result;
215
- }
222
+ };
216
223
 
217
224
  export default (root, rootKey = undefined) => {
218
- const { childNodes } = root
225
+ const { childNodes } = root;
219
226
 
220
227
  // Check for attribute bindings on the root element itself (only if element has attributes)
221
228
  let attributes = null;
@@ -235,6 +242,6 @@ export default (root, rootKey = undefined) => {
235
242
  parsed: parseHTML([...childNodes], rootKey),
236
243
  element: root,
237
244
  children: recursive(Array.from(childNodes), rootKey),
238
- ...(attributes && { attributes })
239
- }
240
- }
245
+ ...(attributes && { attributes }),
246
+ };
247
+ };
package/state.js CHANGED
@@ -1,12 +1,12 @@
1
1
  export default (state, rerender) =>
2
2
  new Proxy(state, {
3
3
  set(obj, prop, value) {
4
- const ref = Reflect.set(...arguments)
4
+ const ref = Reflect.set(...arguments);
5
5
  rerender({
6
6
  [prop]: value,
7
- })
7
+ });
8
8
  // console.info("state change", { obj, prop, value });
9
- return ref
9
+ return ref;
10
10
  },
11
11
  get(target, prop) {
12
12
  // if (typeof target[prop] === 'function') {
@@ -21,6 +21,6 @@ export default (state, rerender) =>
21
21
 
22
22
  // }
23
23
 
24
- return Reflect.get(...arguments)
24
+ return Reflect.get(...arguments);
25
25
  },
26
- })
26
+ });
package/utils.js CHANGED
@@ -9,7 +9,7 @@ export const deepMerge = (target, source) => {
9
9
  if (target == null) return source;
10
10
 
11
11
  // If source is not an object, return it
12
- if (typeof source !== "object") return source;
12
+ if (typeof source !== 'object') return source;
13
13
 
14
14
  // If source is an array, replace target array (don't merge arrays)
15
15
  if (Array.isArray(source)) return source;
@@ -27,8 +27,8 @@ export const deepMerge = (target, source) => {
27
27
  if (
28
28
  targetValue != null &&
29
29
  sourceValue != null &&
30
- typeof targetValue === "object" &&
31
- typeof sourceValue === "object" &&
30
+ typeof targetValue === 'object' &&
31
+ typeof sourceValue === 'object' &&
32
32
  !Array.isArray(targetValue) &&
33
33
  !Array.isArray(sourceValue)
34
34
  ) {
@@ -42,4 +42,3 @@ export const deepMerge = (target, source) => {
42
42
 
43
43
  return result;
44
44
  };
45
-