@ape-egg/vibe 2.1.22 → 3.0.0

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.
Files changed (56) hide show
  1. package/README.md +112 -5
  2. package/boot.js +4 -4
  3. package/component.js +27 -29
  4. package/hot-module-refresh.js +4 -4
  5. package/index.js +26 -17
  6. package/llms.txt +36 -5
  7. package/package.json +20 -14
  8. package/runtime/affected.js +159 -36
  9. package/runtime/cleanup.js +45 -1
  10. package/runtime/component.js +360 -98
  11. package/runtime/conditionals.js +111 -14
  12. package/runtime/debug.js +24 -0
  13. package/runtime/dispatch.js +172 -0
  14. package/runtime/hydrate.js +277 -110
  15. package/runtime/index.js +189 -65
  16. package/runtime/iterate.js +125 -50
  17. package/runtime/iteration-utils.js +59 -8
  18. package/runtime/manifest.js +77 -2
  19. package/runtime/parse.js +81 -11
  20. package/runtime/pre-compiled-iterations.js +19 -6
  21. package/runtime/pre-compiled-manifest.js +13 -4
  22. package/runtime/staging.js +153 -0
  23. package/runtime/state.js +31 -0
  24. package/runtime/tracking.js +173 -0
  25. package/runtime/utils.js +155 -78
  26. package/spa.js +206 -0
  27. package/vibe.css +8 -4
  28. package/CHANGELOG.md +0 -1159
  29. package/ROADMAP.md +0 -397
  30. package/compiler/bin/vibe-compile.js +0 -121
  31. package/compiler/native/.gitkeep +0 -0
  32. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  33. package/compiler/native/vibe-compiler-linux-x64 +0 -0
  34. package/compiler/src/Cargo.lock +0 -2023
  35. package/compiler/src/Cargo.toml +0 -38
  36. package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
  37. package/compiler/src/compiler/binding_case.rs +0 -88
  38. package/compiler/src/compiler/compile.rs +0 -2522
  39. package/compiler/src/compiler/component_tagger.rs +0 -469
  40. package/compiler/src/compiler/iteration_optimizer.rs +0 -455
  41. package/compiler/src/compiler/js_analyzer.rs +0 -715
  42. package/compiler/src/compiler/manifest_builder.rs +0 -693
  43. package/compiler/src/compiler/mod.rs +0 -15
  44. package/compiler/src/compiler/name_binding_protect.rs +0 -207
  45. package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
  46. package/compiler/src/compiler/state_extractor.rs +0 -263
  47. package/compiler/src/compiler/value_stamper.rs +0 -921
  48. package/compiler/src/compiler/watcher.rs +0 -1147
  49. package/compiler/src/config.rs +0 -239
  50. package/compiler/src/main.rs +0 -347
  51. package/compiler/src/parser/element.rs +0 -96
  52. package/compiler/src/parser/html.rs +0 -1004
  53. package/compiler/src/parser/mod.rs +0 -8
  54. package/runtime/pre-compiled-manifest.test.mjs +0 -58
  55. package/runtime/scope.js +0 -50
  56. package/test-results/.last-run.json +0 -4
@@ -1,12 +1,201 @@
1
1
  import { updateIteration } from './iterate.js';
2
2
  import { updateConditional, managedNodes } from './conditionals.js';
3
- import { isComponentWrapper, liveComponentWrapper, remountComponent } from './component.js';
3
+ import { liveComponentWrapper, remountComponent, forceRemount } from './component.js';
4
+ import { isComponentWrapper, parkRootFor, parkBinding } from './staging.js';
4
5
  import { VALUE_ATTRS, DOM_PROPERTIES, BINDING_REGEX, PURE_BINDING_REGEX } from './constants.js';
5
- import { evalInScope, resolveCaseInsensitivePath } from './utils.js';
6
+ import { evalInScope, resolveCaseInsensitivePath, overlayKeysOf } from './utils.js';
6
7
  import { RawHtml } from './raw-html.js';
8
+ import { beginTracking, endTracking } from './tracking.js';
9
+ import { reportEvalError } from './debug.js';
10
+
11
+ // Run one entry's evaluation inside a tracking window. The subscriber is
12
+ // hosted on the walk's cached binding object (aff.binding) — stable per
13
+ // binding across flushes, invalidated exactly when the binding cache
14
+ // rebuilds. Entries without a binding host (hand-built lists from external
15
+ // callers) evaluate untracked, exactly as before.
16
+ const tracked = (aff, effectiveState, fn) => {
17
+ const m = aff.binding;
18
+ if (!m) return fn();
19
+ const sub = (m._sub ??= {
20
+ kind: aff.type ?? 'text',
21
+ anchor: aff.textNode ?? aff.element,
22
+ });
23
+ sub.entry = aff; // latest entry shape — dispatch rebuilds scope at fire time
24
+ sub.lastScope = effectiveState; // overlay source for dispatch-time rescoping
25
+ beginTracking(sub, overlayKeysOf(effectiveState));
26
+ try {
27
+ return fn();
28
+ } finally {
29
+ endTracking();
30
+ }
31
+ };
32
+
33
+ const applyNameBinding = (aff, effectiveState) => {
34
+ const { nameBinding, matchInner, element } = aff;
35
+ try {
36
+ // HTML lowercases attribute names, so we need case-insensitive lookup.
37
+ // Try exact match first, then walk the path case-insensitively if the
38
+ // exact lookup returned nothing — this recovers camelCase property
39
+ // names in dotted paths like `<icon @[fx.convertsIcon]>` (arrives at
40
+ // runtime as `@[fx.convertsicon]`).
41
+ let attrName = evalInScope(matchInner, effectiveState, element);
42
+
43
+ if (!attrName) {
44
+ attrName = resolveCaseInsensitivePath(effectiveState, matchInner);
45
+ }
46
+
47
+ // Track multiple name bindings per element (need a map of binding -> evaluated attr)
48
+ if (!element._vibeNameBindings) {
49
+ element._vibeNameBindings = new Map();
50
+ }
51
+
52
+ // Remove the old evaluated attribute for this specific binding
53
+ const oldAttrName = element._vibeNameBindings.get(nameBinding);
54
+ if (oldAttrName) {
55
+ element.removeAttribute(oldAttrName);
56
+ }
57
+
58
+ // Remove the binding attribute itself
59
+ if (element.hasAttribute(nameBinding)) {
60
+ element.removeAttribute(nameBinding);
61
+ }
62
+
63
+ // Set the new attribute (empty value for boolean-like attributes)
64
+ if (attrName) {
65
+ element.setAttribute(attrName, '');
66
+ element._vibeNameBindings.set(nameBinding, attrName);
67
+ } else {
68
+ element._vibeNameBindings.delete(nameBinding);
69
+ }
70
+
71
+ // A binding relocated into `data-vibe-namebind` (clone path) has served its
72
+ // transport purpose once the real attribute is set — drop it so the rendered
73
+ // DOM matches the batch path, which never emits it.
74
+ if (element.hasAttribute('data-vibe-namebind')) {
75
+ element.removeAttribute('data-vibe-namebind');
76
+ }
77
+ } catch (e) {
78
+ console.error('Error hydrating name binding:', e);
79
+ }
80
+ };
81
+
82
+ const applyAttributeBinding = (aff, effectiveState) => {
83
+ const { attrName, attrValue, element } = aff;
84
+ try {
85
+ // Check if this is a pure binding (e.g., value="@[inputValue]")
86
+ const isPureBinding = attrValue.match(PURE_BINDING_REGEX);
87
+ const isDomProperty = DOM_PROPERTIES.includes(attrName);
88
+ // Value attrs keep their string value; everything else is boolean-like (removed when falsy)
89
+ const isValueAttr =
90
+ VALUE_ATTRS.includes(attrName) ||
91
+ attrName.startsWith('data-') ||
92
+ attrName.startsWith('aria-') ||
93
+ attrName.startsWith('on');
94
+
95
+ if (isDomProperty && isPureBinding) {
96
+ // For DOM properties (value, checked, selected), set BOTH property AND attribute
97
+ // Property: Fast runtime updates, what the user sees
98
+ // Attribute: Truthful DOM representation, enables compiler snapshots via outerHTML
99
+ const expr = isPureBinding[1];
100
+ const value = evalInScope(expr, effectiveState, element);
101
+ if (element[attrName] !== value) element[attrName] = value;
102
+ if (attrName === 'value') {
103
+ if (value !== undefined && value !== null) {
104
+ const str = String(value);
105
+ if (element.getAttribute(attrName) !== str) element.setAttribute(attrName, str);
106
+ }
107
+ } else if (value) {
108
+ // checked/selected are boolean — the truthful attribute form is
109
+ // presence (empty) when truthy, absence when falsy. Stringifying
110
+ // would leave checked="false", which is "checked" to CSS and HTML.
111
+ if (element.getAttribute(attrName) !== '') element.setAttribute(attrName, '');
112
+ } else if (element.hasAttribute(attrName)) {
113
+ element.removeAttribute(attrName);
114
+ }
115
+ } else if (!isValueAttr && isPureBinding) {
116
+ // Boolean-like attributes: add or remove based on truthiness.
117
+ // Compare both presence AND value — initial hydration starts with
118
+ // the raw `@[...]` binding text as the attribute value, so
119
+ // `hasAttribute` alone isn't enough to know the canonical state is
120
+ // already set.
121
+ const expr = isPureBinding[1];
122
+ const value = evalInScope(expr, effectiveState, element);
123
+ if (value) {
124
+ if (element.getAttribute(attrName) !== '') {
125
+ element.setAttribute(attrName, '');
126
+ }
127
+ } else if (element.hasAttribute(attrName)) {
128
+ element.removeAttribute(attrName);
129
+ }
130
+ } else {
131
+ // Value attribute - replace bindings with values
132
+ const newValue = attrValue.replace(BINDING_REGEX, (_, expr) => {
133
+ return evalInScope(expr, effectiveState, element);
134
+ });
135
+ if (element.getAttribute(attrName) !== newValue) {
136
+ element.setAttribute(attrName, newValue);
137
+ }
138
+ }
139
+ } catch (e) {
140
+ reportEvalError(attrValue, element, e);
141
+ }
142
+ };
7
143
 
8
144
  export default (affected, state, manifest = {}, oldState = {}) => {
9
- affected.forEach((aff) => {
145
+ // Wrappers that entered a reactive-src remount DURING this pass (the src/key
146
+ // handlers below). affected() collected this list before the outgoing flag
147
+ // existed, so the outgoing subtree's entries are still in it — and this is
148
+ // exactly the flush where route state flips. Applying them would paint the
149
+ // outgoing page against the incoming route for the whole fetch+mount window
150
+ // (the mid-navigation CSS collapse). Tree-walk order guarantees the
151
+ // wrapper's own src/key entries precede its subtree's entries, so skipping
152
+ // by ancestry here is airtight. Null until a remount actually happens —
153
+ // zero cost on ordinary flushes.
154
+ let outgoingRoots = null;
155
+ const underOutgoingRoot = (aff) => {
156
+ if (!outgoingRoots) return false;
157
+ const el = aff.element ?? aff.node?.meta?.startComment?.parentElement;
158
+ if (!el) return false;
159
+ for (let i = 0; i < outgoingRoots.length; i++) {
160
+ const root = outgoingRoots[i];
161
+ if (root !== el && root.contains(el)) return true;
162
+ }
163
+ return false;
164
+ };
165
+
166
+ // Two passes: wrapper src/key entries (remount triggers) first, everything
167
+ // else second. A remount marks its wrapper outgoing and starts staging —
168
+ // the freeze skip below and the ancestor-binding parking both depend on
169
+ // that registration, and tree-walk order would otherwise process an
170
+ // ancestor's bindings (the `<page @[page.name]>` styling context) before
171
+ // the outlet entry that begins the staging they must park against.
172
+ const remountTriggers = [];
173
+ const ordinary = [];
174
+ for (const aff of affected) {
175
+ const isRemountTrigger =
176
+ aff.type === 'attribute' &&
177
+ (aff.attrName === 'src' || aff.attrName === 'key') &&
178
+ isComponentWrapper(aff.element);
179
+ (isRemountTrigger ? remountTriggers : ordinary).push(aff);
180
+ }
181
+
182
+ const processEntry = (aff) => {
183
+ if (underOutgoingRoot(aff)) return;
184
+
185
+ // Torn down mid-pass: the entry's target was connected when the walk
186
+ // collected it but left the document before this entry processed — an
187
+ // earlier entry's teardown (an iteration emptying, a branch flip)
188
+ // removed it. Same ordering hole as the outgoing-remount skip above.
189
+ // Processing it would write to dead DOM and re-register a subscriber
190
+ // the teardown just pruned, pinning the removed subtree. Entries that
191
+ // were ALREADY detached at collection (initializeBlock hydrating a
192
+ // fresh row inside its parse container) pass through untouched.
193
+ if (aff.wasConnected) {
194
+ const target =
195
+ aff.textNode ?? aff.element ?? aff.node?.meta?.startComment;
196
+ if (target && !target.isConnected) return;
197
+ }
198
+
10
199
  // Use scoped state if provided (from iteration instances)
11
200
  const effectiveState = aff.scopedState || state;
12
201
 
@@ -30,52 +219,14 @@ export default (affected, state, manifest = {}, oldState = {}) => {
30
219
 
31
220
  // Handle name bindings (e.g., <icon @[section.icon]>)
32
221
  if (aff.type === 'nameBinding') {
33
- const { nameBinding, matchInner, element } = aff;
34
- try {
35
- // HTML lowercases attribute names, so we need case-insensitive lookup.
36
- // Try exact match first, then walk the path case-insensitively if the
37
- // exact lookup returned nothing — this recovers camelCase property
38
- // names in dotted paths like `<icon @[fx.convertsIcon]>` (arrives at
39
- // runtime as `@[fx.convertsicon]`).
40
- let attrName = evalInScope(matchInner, effectiveState, element);
41
-
42
- if (!attrName) {
43
- attrName = resolveCaseInsensitivePath(effectiveState, matchInner);
44
- }
45
-
46
- // Track multiple name bindings per element (need a map of binding -> evaluated attr)
47
- if (!element._vibeNameBindings) {
48
- element._vibeNameBindings = new Map();
49
- }
50
-
51
- // Remove the old evaluated attribute for this specific binding
52
- const oldAttrName = element._vibeNameBindings.get(nameBinding);
53
- if (oldAttrName) {
54
- element.removeAttribute(oldAttrName);
55
- }
56
-
57
- // Remove the binding attribute itself
58
- if (element.hasAttribute(nameBinding)) {
59
- element.removeAttribute(nameBinding);
60
- }
61
-
62
- // Set the new attribute (empty value for boolean-like attributes)
63
- if (attrName) {
64
- element.setAttribute(attrName, '');
65
- element._vibeNameBindings.set(nameBinding, attrName);
66
- } else {
67
- element._vibeNameBindings.delete(nameBinding);
68
- }
69
-
70
- // A binding relocated into `data-vibe-namebind` (clone path) has served its
71
- // transport purpose once the real attribute is set — drop it so the rendered
72
- // DOM matches the batch path, which never emits it.
73
- if (element.hasAttribute('data-vibe-namebind')) {
74
- element.removeAttribute('data-vibe-namebind');
75
- }
76
- } catch (e) {
77
- console.error('Error hydrating name binding:', e);
222
+ const parkRoot = parkRootFor(aff.element);
223
+ if (parkRoot) {
224
+ parkBinding(parkRoot, aff.element, 'nb:' + aff.nameBinding, () =>
225
+ applyNameBinding(aff, effectiveState),
226
+ );
227
+ return;
78
228
  }
229
+ tracked(aff, effectiveState, () => applyNameBinding(aff, effectiveState));
79
230
  return;
80
231
  }
81
232
 
@@ -91,71 +242,60 @@ export default (affected, state, manifest = {}, oldState = {}) => {
91
242
  // reparse rebuilds the knowledge from that attribute. A state change
92
243
  // landing inside the swap window still resolves through the
93
244
  // replacement chain.
245
+ // Keyed component (`<component src="@[page.src]" key="@[page.path]">`):
246
+ // a key change is a declared identity change — remount the mounted
247
+ // component even when the src is unchanged (param→param navigation on
248
+ // the same route). The first resolution just records the initial key;
249
+ // the mount itself is owned by the normal component pass.
250
+ if (attrName === 'key' && isComponentWrapper(element)) {
251
+ const live = liveComponentWrapper(element);
252
+ const newKey = tracked(aff, effectiveState, () =>
253
+ attrValue.replace(BINDING_REGEX, (_, expr) =>
254
+ evalInScope(expr, effectiveState, live) ?? '',
255
+ ),
256
+ );
257
+ live._vibeKeyBinding = attrValue;
258
+ const prevKey = live._vibeMountedKey;
259
+ live._vibeMountedKey = newKey;
260
+ if (prevKey !== undefined && newKey !== prevKey) forceRemount(live);
261
+ if (live._vibeOutgoing) (outgoingRoots ??= []).push(live);
262
+ return;
263
+ }
264
+
94
265
  if (attrName === 'src' && isComponentWrapper(element)) {
95
266
  const live = liveComponentWrapper(element);
96
- const newSrc = attrValue.replace(BINDING_REGEX, (_, expr) =>
97
- evalInScope(expr, effectiveState, live),
267
+ const newSrc = tracked(aff, effectiveState, () =>
268
+ attrValue.replace(BINDING_REGEX, (_, expr) =>
269
+ evalInScope(expr, effectiveState, live) ?? '',
270
+ ),
98
271
  );
99
272
  live._vibeSrcBinding = attrValue;
100
- remountComponent(live, newSrc);
273
+ if (newSrc) {
274
+ remountComponent(live, newSrc);
275
+ if (live._vibeOutgoing) (outgoingRoots ??= []).push(live);
276
+ } else if (live.hasAttribute('src')) {
277
+ // Unresolved src mounts nothing (a no-match deep link leaves
278
+ // $.page.src unset): move the binding onto data-vibe-src — the
279
+ // established transport parse.js already reads — and drop the
280
+ // fetchable src, so component processing and cleanup treat the
281
+ // outlet as settled instead of fetching a stringified binding.
282
+ live.setAttribute('data-vibe-src', attrValue);
283
+ live.removeAttribute('src');
284
+ }
101
285
  return;
102
286
  }
103
287
 
104
- // Check if this is a pure binding (e.g., value="@[inputValue]")
105
- const isPureBinding = attrValue.match(PURE_BINDING_REGEX);
106
- const isDomProperty = DOM_PROPERTIES.includes(attrName);
107
- // Value attrs keep their string value; everything else is boolean-like (removed when falsy)
108
- const isValueAttr =
109
- VALUE_ATTRS.includes(attrName) ||
110
- attrName.startsWith('data-') ||
111
- attrName.startsWith('aria-') ||
112
- attrName.startsWith('on');
113
-
114
- if (isDomProperty && isPureBinding) {
115
- // For DOM properties (value, checked, selected), set BOTH property AND attribute
116
- // Property: Fast runtime updates, what the user sees
117
- // Attribute: Truthful DOM representation, enables compiler snapshots via outerHTML
118
- const expr = isPureBinding[1];
119
- const value = evalInScope(expr, effectiveState, element);
120
- if (element[attrName] !== value) element[attrName] = value;
121
- if (attrName === 'value') {
122
- if (value !== undefined && value !== null) {
123
- const str = String(value);
124
- if (element.getAttribute(attrName) !== str) element.setAttribute(attrName, str);
125
- }
126
- } else if (value) {
127
- // checked/selected are boolean — the truthful attribute form is
128
- // presence (empty) when truthy, absence when falsy. Stringifying
129
- // would leave checked="false", which is "checked" to CSS and HTML.
130
- if (element.getAttribute(attrName) !== '') element.setAttribute(attrName, '');
131
- } else if (element.hasAttribute(attrName)) {
132
- element.removeAttribute(attrName);
133
- }
134
- } else if (!isValueAttr && isPureBinding) {
135
- // Boolean-like attributes: add or remove based on truthiness.
136
- // Compare both presence AND value — initial hydration starts with
137
- // the raw `@[...]` binding text as the attribute value, so
138
- // `hasAttribute` alone isn't enough to know the canonical state is
139
- // already set.
140
- const expr = isPureBinding[1];
141
- const value = evalInScope(expr, effectiveState, element);
142
- if (value) {
143
- if (element.getAttribute(attrName) !== '') {
144
- element.setAttribute(attrName, '');
145
- }
146
- } else if (element.hasAttribute(attrName)) {
147
- element.removeAttribute(attrName);
148
- }
149
- } else {
150
- // Value attribute - replace bindings with values
151
- const newValue = attrValue.replace(BINDING_REGEX, (_, expr) => {
152
- return evalInScope(expr, effectiveState, element);
153
- });
154
- if (element.getAttribute(attrName) !== newValue) {
155
- element.setAttribute(attrName, newValue);
156
- }
288
+ const parkRoot = parkRootFor(element);
289
+ if (parkRoot) {
290
+ parkBinding(parkRoot, element, 'attr:' + attrName, () =>
291
+ applyAttributeBinding(aff, effectiveState),
292
+ );
293
+ return;
157
294
  }
158
- } catch (e) {}
295
+ tracked(aff, effectiveState, () => applyAttributeBinding(aff, effectiveState));
296
+ } catch (e) {
297
+ reportEvalError(aff.attrValue ?? aff.matchInner, aff.element, e);
298
+ }
159
299
  return;
160
300
  }
161
301
 
@@ -165,7 +305,9 @@ export default (affected, state, manifest = {}, oldState = {}) => {
165
305
  // This prevents undefined store properties to throw an error
166
306
  try {
167
307
  // Evaluate the expression with state as context
168
- const evaluated = evalInScope(matchInner, effectiveState, element);
308
+ const evaluated = tracked(aff, effectiveState, () =>
309
+ evalInScope(matchInner, effectiveState, element),
310
+ );
169
311
 
170
312
  // Raw-HTML render: `$.unsafe(str)` returns a RawHtml marker. When the
171
313
  // binding is the sole content of its element (`<p>@[$.unsafe(x)]</p>`),
@@ -185,10 +327,18 @@ export default (affected, state, manifest = {}, oldState = {}) => {
185
327
  element._vibeRawHtmlValue = html;
186
328
  }
187
329
  element._vibeRawHtml = true;
330
+ // The injection consumed the entry's original text node. Re-anchor
331
+ // the subscriber on the ELEMENT (which survives every injection) —
332
+ // otherwise the dispatch engine prunes it as dead after the first
333
+ // render and the binding silently stops re-rendering.
334
+ if (aff.binding?._sub) aff.binding._sub.anchor = element;
188
335
  return;
189
336
  }
190
337
 
191
- const toReplace = input.replaceAll(matchOuter, evaluated).trim();
338
+ // Function replacer: a string replacement would run GetSubstitution on
339
+ // the VALUE — `$$` collapses, `$&` re-inserts the binding text into the
340
+ // DOM (which the settle gates then read as an unhydrated binding).
341
+ const toReplace = input.replaceAll(matchOuter, () => evaluated).trim();
192
342
 
193
343
  affected.forEach((innerAff) => {
194
344
  if (innerAff.element === element) {
@@ -207,11 +357,28 @@ export default (affected, state, manifest = {}, oldState = {}) => {
207
357
  // value actually changed. Reactivity coverage is unchanged: state changes
208
358
  // that produce a new value still apply; state changes that don't are now
209
359
  // proper no-ops at the DOM layer.
210
- if (textNode && textNode.nodeType === 3) {
360
+ if (element._vibeRawHtml && !(textNode && textNode.isConnected)) {
361
+ // unsafe → escaped transition: an earlier injection consumed the
362
+ // entry's text node, so the element itself takes the escaped text —
363
+ // and the entry ADOPTS the fresh node, so the NEXT escaped update
364
+ // writes live DOM instead of the consumed original.
365
+ element.textContent = toReplace;
366
+ element._vibeRawHtml = false;
367
+ element._vibeRawHtmlValue = undefined;
368
+ if (textNode) {
369
+ if (!element.firstChild) element.appendChild(document.createTextNode(''));
370
+ aff.textNode = element.firstChild;
371
+ }
372
+ } else if (textNode && textNode.nodeType === 3) {
211
373
  if (textNode.textContent !== toReplace) textNode.textContent = toReplace;
212
374
  } else if (element.textContent !== toReplace) {
213
375
  element.textContent = toReplace;
214
376
  }
215
- } catch (e) {}
216
- });
377
+ } catch (e) {
378
+ reportEvalError(matchInner ?? input, element, e);
379
+ }
380
+ };
381
+
382
+ remountTriggers.forEach(processEntry);
383
+ ordinary.forEach(processEntry);
217
384
  };