@schukai/monster 4.153.0 → 4.154.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.
- package/package.json +1 -1
- package/source/components/form/message-state-button.mjs +1 -1
- package/source/components/form/select-link.mjs +12 -1
- package/source/components/state/log.mjs +3 -7
- package/source/components/state/thread.mjs +3 -7
- package/source/components/time/day.mjs +0 -19
- package/source/components/time/month-calendar.mjs +5 -1
- package/source/data/buildmap.mjs +6 -5
- package/source/data/buildtree.mjs +0 -8
- package/source/data/datasource.mjs +3 -10
- package/source/data/diff.mjs +13 -12
- package/source/data/extend.mjs +23 -1
- package/source/data/pathfinder.mjs +228 -32
- package/source/data/transformer.mjs +2 -2
- package/source/dom/customcontrol.mjs +35 -9
- package/source/dom/customelement.mjs +293 -31
- package/source/dom/slotted.mjs +46 -31
- package/source/dom/template.mjs +4 -1
- package/source/dom/updater.mjs +57 -17
- package/source/types/base.mjs +7 -6
- package/source/types/basewithoptions.mjs +1 -8
- package/source/types/internal.mjs +4 -10
- package/source/types/node.mjs +1 -3
- package/source/types/observerlist.mjs +9 -17
- package/source/types/proxyobserver.mjs +51 -12
- package/source/types/tokenlist.mjs +13 -8
- package/source/util/clone.mjs +44 -14
- package/test/cases/browser-bundle.mjs +75 -0
- package/test/cases/components/time/month-calendar.mjs +115 -0
- package/test/cases/data/buildmap.mjs +14 -0
- package/test/cases/data/buildtree.mjs +49 -0
- package/test/cases/data/diff.mjs +86 -0
- package/test/cases/data/extend.mjs +52 -0
- package/test/cases/data/pathfinder-resolution-baseline.mjs +322 -0
- package/test/cases/data/pathfinder.mjs +163 -0
- package/test/cases/data/transformer.mjs +26 -0
- package/test/cases/dom/customcontrol.mjs +85 -0
- package/test/cases/dom/customelement-rebind-baseline.mjs +323 -0
- package/test/cases/dom/customelement-teardown.mjs +408 -0
- package/test/cases/dom/customelement.mjs +408 -0
- package/test/cases/dom/dimension.mjs +34 -10
- package/test/cases/dom/slotted-nodes.mjs +48 -1
- package/test/cases/dom/template.mjs +11 -0
- package/test/cases/dom/updater-remove.mjs +198 -0
- package/test/cases/dom/updater.mjs +118 -0
- package/test/cases/packaging.mjs +213 -0
- package/test/cases/types/base.mjs +50 -0
- package/test/cases/types/observerlist.mjs +103 -1
- package/test/cases/types/proxyobserver.mjs +189 -0
- package/test/cases/types/regex.mjs +4 -0
- package/test/cases/types/tokenlist.mjs +64 -1
- package/test/cases/util/clone.mjs +115 -0
package/source/dom/updater.mjs
CHANGED
|
@@ -651,7 +651,7 @@ function retrieveAndSetValue(element) {
|
|
|
651
651
|
|
|
652
652
|
let options = element?.selectedOptions;
|
|
653
653
|
if (options === undefined)
|
|
654
|
-
options = element.querySelectorAll("
|
|
654
|
+
options = element.querySelectorAll("option:checked");
|
|
655
655
|
value = Array.from(options).map(({ value }) => value);
|
|
656
656
|
|
|
657
657
|
break;
|
|
@@ -803,17 +803,34 @@ function parseIntArray(val) {
|
|
|
803
803
|
* @private
|
|
804
804
|
*/
|
|
805
805
|
function retrieveFromBindings() {
|
|
806
|
-
if (
|
|
806
|
+
if (
|
|
807
|
+
this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`) &&
|
|
808
|
+
!isUncheckedRadio(this[internalSymbol].element)
|
|
809
|
+
) {
|
|
807
810
|
retrieveAndSetValue.call(this, this[internalSymbol].element);
|
|
808
811
|
}
|
|
809
812
|
|
|
810
813
|
for (const [, element] of this[internalSymbol].element
|
|
811
814
|
.querySelectorAll(`[${ATTRIBUTE_UPDATER_BIND}]`)
|
|
812
815
|
.entries()) {
|
|
816
|
+
if (isUncheckedRadio(element)) continue;
|
|
813
817
|
retrieveAndSetValue.call(this, element);
|
|
814
818
|
}
|
|
815
819
|
}
|
|
816
820
|
|
|
821
|
+
/**
|
|
822
|
+
* @param {HTMLElement} element
|
|
823
|
+
* @return {boolean}
|
|
824
|
+
* @private
|
|
825
|
+
*/
|
|
826
|
+
function isUncheckedRadio(element) {
|
|
827
|
+
return (
|
|
828
|
+
element instanceof HTMLInputElement &&
|
|
829
|
+
element.type === "radio" &&
|
|
830
|
+
element.checked === false
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
817
834
|
/**
|
|
818
835
|
* @private
|
|
819
836
|
* @license AGPLv3
|
|
@@ -830,8 +847,12 @@ function removeElement(_change, renderContext) {
|
|
|
830
847
|
}
|
|
831
848
|
|
|
832
849
|
let changed = false;
|
|
850
|
+
// No :scope prefix: querySelectorAll() on an element already returns only its
|
|
851
|
+
// descendants, so the prefix carries no meaning here, while :scope does not
|
|
852
|
+
// resolve for shadow root content under jsdom, which made this a silent no-op
|
|
853
|
+
// for every component that renders into a shadow root.
|
|
833
854
|
for (const [, element] of this[internalSymbol].element
|
|
834
|
-
.querySelectorAll(
|
|
855
|
+
.querySelectorAll(`[${ATTRIBUTE_UPDATER_REMOVE}]`)
|
|
835
856
|
.entries()) {
|
|
836
857
|
teardownManagedSubtree(element);
|
|
837
858
|
element.parentNode.removeChild(element);
|
|
@@ -1201,7 +1222,10 @@ function getSlots(container, cache) {
|
|
|
1201
1222
|
return cache.get(container);
|
|
1202
1223
|
}
|
|
1203
1224
|
|
|
1204
|
-
const slots = container.querySelectorAll("slot");
|
|
1225
|
+
const slots = Array.from(container.querySelectorAll("slot"));
|
|
1226
|
+
if (container.localName === "slot") {
|
|
1227
|
+
slots.unshift(container);
|
|
1228
|
+
}
|
|
1205
1229
|
cache.set(container, slots);
|
|
1206
1230
|
return slots;
|
|
1207
1231
|
}
|
|
@@ -1565,6 +1589,9 @@ function replaceKeyedPatchedChildren(
|
|
|
1565
1589
|
const nextNode = preparePatchedNode(reused, renderedValue);
|
|
1566
1590
|
nextNode[patchNodeKeySymbol] = normalizedKey;
|
|
1567
1591
|
if (nextNode !== reused && reused.parentNode === element) {
|
|
1592
|
+
if (reused instanceof HTMLElement) {
|
|
1593
|
+
teardownManagedSubtree(reused);
|
|
1594
|
+
}
|
|
1568
1595
|
element.replaceChild(nextNode, reused);
|
|
1569
1596
|
}
|
|
1570
1597
|
nextNodes.push(nextNode);
|
|
@@ -1591,15 +1618,11 @@ function replaceKeyedPatchedChildren(
|
|
|
1591
1618
|
}
|
|
1592
1619
|
}
|
|
1593
1620
|
|
|
1594
|
-
for (
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
if (element.lastChild !== node) {
|
|
1601
|
-
element.appendChild(node);
|
|
1602
|
-
}
|
|
1621
|
+
for (let index = 0; index < nextNodes.length; index++) {
|
|
1622
|
+
const node = nextNodes[index];
|
|
1623
|
+
const current = element.childNodes[index] ?? null;
|
|
1624
|
+
if (current === node) continue;
|
|
1625
|
+
element.insertBefore(node, current);
|
|
1603
1626
|
}
|
|
1604
1627
|
}
|
|
1605
1628
|
|
|
@@ -1851,8 +1874,7 @@ function runUpdateAttributes(
|
|
|
1851
1874
|
if (
|
|
1852
1875
|
!element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
|
|
1853
1876
|
!attributes.includes(`path:${current}`) &&
|
|
1854
|
-
!attributes
|
|
1855
|
-
!attributes.startsWith("i18n:")
|
|
1877
|
+
!hasUnconditionalBinding(attributes)
|
|
1856
1878
|
) {
|
|
1857
1879
|
continue;
|
|
1858
1880
|
}
|
|
@@ -1933,8 +1955,7 @@ function runUpdateProperties(
|
|
|
1933
1955
|
if (
|
|
1934
1956
|
!element.hasAttribute(ATTRIBUTE_UPDATER_SELECT_THIS) &&
|
|
1935
1957
|
!properties.includes(`path:${current}`) &&
|
|
1936
|
-
!properties
|
|
1937
|
-
!properties.startsWith("i18n:")
|
|
1958
|
+
!hasUnconditionalBinding(properties)
|
|
1938
1959
|
) {
|
|
1939
1960
|
continue;
|
|
1940
1961
|
}
|
|
@@ -1981,6 +2002,9 @@ function runUpdateProperties(
|
|
|
1981
2002
|
|
|
1982
2003
|
function handleInputControlAttributeUpdate(element, name, value) {
|
|
1983
2004
|
if (element instanceof HTMLSelectElement) {
|
|
2005
|
+
if (name !== "value") {
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
1984
2008
|
switch (element.type) {
|
|
1985
2009
|
case "select-multiple":
|
|
1986
2010
|
const selectedValues = isArray(value)
|
|
@@ -2054,6 +2078,22 @@ function handleInputControlAttributeUpdate(element, name, value) {
|
|
|
2054
2078
|
}
|
|
2055
2079
|
}
|
|
2056
2080
|
|
|
2081
|
+
/**
|
|
2082
|
+
* Check whether an attribute/property definition contains a command that is
|
|
2083
|
+
* independent from the changed subject path.
|
|
2084
|
+
* @param {string} definitions
|
|
2085
|
+
* @return {boolean}
|
|
2086
|
+
* @private
|
|
2087
|
+
*/
|
|
2088
|
+
function hasUnconditionalBinding(definitions) {
|
|
2089
|
+
return definitions.split(",").some((definition) => {
|
|
2090
|
+
const separator = definition.indexOf(" ");
|
|
2091
|
+
if (separator < 0) return false;
|
|
2092
|
+
const command = trimSpaces(definition.substring(separator + 1));
|
|
2093
|
+
return command.startsWith("static:") || command.startsWith("i18n:");
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2057
2097
|
/**
|
|
2058
2098
|
* @private
|
|
2059
2099
|
* @param {HTMLElement|*} element
|
package/source/types/base.mjs
CHANGED
|
@@ -115,21 +115,22 @@ function checkInstanceSymbol(obj) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
if (proto.hasOwnProperty(instanceSymbol) !== true) {
|
|
118
|
-
return checkInstanceSymbol.apply(this, [obj
|
|
118
|
+
return checkInstanceSymbol.apply(this, [Object.getPrototypeOf(obj)]);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
const symbol = proto[instanceSymbol];
|
|
122
122
|
if (symbol === undefined) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return false;
|
|
123
|
+
const parent = Object.getPrototypeOf(obj);
|
|
124
|
+
if (parent) {
|
|
125
|
+
return checkInstanceSymbol.apply(this, [parent]);
|
|
127
126
|
}
|
|
127
|
+
|
|
128
|
+
return false;
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
if (symbol === this[instanceSymbol]) {
|
|
131
132
|
return true;
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
return checkInstanceSymbol.apply(this, [obj
|
|
135
|
+
return checkInstanceSymbol.apply(this, [Object.getPrototypeOf(obj)]);
|
|
135
136
|
}
|
|
@@ -80,13 +80,6 @@ class BaseWithOptions extends Base {
|
|
|
80
80
|
* @since 1.10.0
|
|
81
81
|
*/
|
|
82
82
|
getOption(path, defaultValue) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
value = new Pathfinder(this[internalSymbol]).getVia(path);
|
|
87
|
-
} catch (e) {}
|
|
88
|
-
|
|
89
|
-
if (value === undefined) return defaultValue;
|
|
90
|
-
return value;
|
|
83
|
+
return new Pathfinder(this[internalSymbol]).getVia(path, defaultValue);
|
|
91
84
|
}
|
|
92
85
|
}
|
|
@@ -126,16 +126,10 @@ function equipWithInternal() {
|
|
|
126
126
|
* @return {*}
|
|
127
127
|
*/
|
|
128
128
|
this["getInternal"] = (path, defaultValue) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
path,
|
|
134
|
-
);
|
|
135
|
-
} catch (e) {}
|
|
136
|
-
|
|
137
|
-
if (value === undefined) return defaultValue;
|
|
138
|
-
return value;
|
|
129
|
+
return new Pathfinder(this[internalSymbol].getRealSubject()).getVia(
|
|
130
|
+
path,
|
|
131
|
+
defaultValue,
|
|
132
|
+
);
|
|
139
133
|
};
|
|
140
134
|
}
|
|
141
135
|
|
package/source/types/node.mjs
CHANGED
|
@@ -199,8 +199,6 @@ class Node extends Base {
|
|
|
199
199
|
* @return {setChildLevelAndParent}
|
|
200
200
|
*/
|
|
201
201
|
function setChildLevelAndParent(node, operand, visitedNodes) {
|
|
202
|
-
const self = this;
|
|
203
|
-
|
|
204
202
|
if (visitedNodes.has(node)) {
|
|
205
203
|
throw new Error(
|
|
206
204
|
"the node has already been visited and cannot be traversed again",
|
|
@@ -217,7 +215,7 @@ function setChildLevelAndParent(node, operand, visitedNodes) {
|
|
|
217
215
|
child[treeStructureSymbol].parent = node;
|
|
218
216
|
child[treeStructureSymbol].level =
|
|
219
217
|
node[treeStructureSymbol].level + operand;
|
|
220
|
-
setChildLevelAndParent.call(
|
|
218
|
+
setChildLevelAndParent.call(node, child, operand, visitedNodes);
|
|
221
219
|
});
|
|
222
220
|
return this;
|
|
223
221
|
}
|
|
@@ -55,9 +55,9 @@ class ObserverList extends Base {
|
|
|
55
55
|
detach(observer) {
|
|
56
56
|
validateInstance(observer, Observer);
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
for (; i
|
|
58
|
+
// Iterate backwards: splicing while moving forward skips the entry that
|
|
59
|
+
// takes the freed slot, which left adjacent duplicates attached.
|
|
60
|
+
for (let i = this.observers.length - 1; i >= 0; i--) {
|
|
61
61
|
if (this.observers[i] === observer) {
|
|
62
62
|
this.observers.splice(i, 1);
|
|
63
63
|
}
|
|
@@ -74,14 +74,8 @@ class ObserverList extends Base {
|
|
|
74
74
|
*/
|
|
75
75
|
contains(observer) {
|
|
76
76
|
validateInstance(observer, Observer);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (; i < l; i++) {
|
|
80
|
-
if (this.observers[i] === observer) {
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
77
|
+
|
|
78
|
+
return this.observers.includes(observer);
|
|
85
79
|
}
|
|
86
80
|
|
|
87
81
|
/**
|
|
@@ -90,14 +84,12 @@ class ObserverList extends Base {
|
|
|
90
84
|
* @return {Promise}
|
|
91
85
|
*/
|
|
92
86
|
notify(subject) {
|
|
93
|
-
const
|
|
87
|
+
const promises = [];
|
|
94
88
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
for (; i < l; i++) {
|
|
98
|
-
pomises.push(this.observers[i].update(subject));
|
|
89
|
+
for (const observer of this.observers) {
|
|
90
|
+
promises.push(observer.update(subject));
|
|
99
91
|
}
|
|
100
92
|
|
|
101
|
-
return Promise.all(
|
|
93
|
+
return Promise.all(promises);
|
|
102
94
|
}
|
|
103
95
|
}
|
|
@@ -156,6 +156,15 @@ function getHandler() {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
const value = Reflect.get(target, key, receiver);
|
|
159
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
|
|
160
|
+
if (
|
|
161
|
+
descriptor &&
|
|
162
|
+
descriptor.configurable === false &&
|
|
163
|
+
"value" in descriptor &&
|
|
164
|
+
descriptor.writable === false
|
|
165
|
+
) {
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
159
168
|
|
|
160
169
|
if (typeof key === "symbol") {
|
|
161
170
|
return value;
|
|
@@ -209,7 +218,7 @@ function getHandler() {
|
|
|
209
218
|
current = proxy.proxyMap.get(current);
|
|
210
219
|
}
|
|
211
220
|
|
|
212
|
-
if (current
|
|
221
|
+
if (isSameValue(current, value)) {
|
|
213
222
|
return true;
|
|
214
223
|
}
|
|
215
224
|
|
|
@@ -223,7 +232,7 @@ function getHandler() {
|
|
|
223
232
|
if (proxy.proxyMap.has(next)) {
|
|
224
233
|
next = proxy.proxyMap.get(next);
|
|
225
234
|
}
|
|
226
|
-
if (next
|
|
235
|
+
if (!isSameValue(next, current)) {
|
|
227
236
|
proxy.observers.notify(proxy);
|
|
228
237
|
}
|
|
229
238
|
}
|
|
@@ -233,16 +242,12 @@ function getHandler() {
|
|
|
233
242
|
|
|
234
243
|
// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-delete-p
|
|
235
244
|
deleteProperty: function (target, key) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
proxy.observers.notify(proxy);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return true;
|
|
245
|
+
const hadOwnProperty = Object.hasOwn(target, key);
|
|
246
|
+
const result = Reflect.deleteProperty(target, key);
|
|
247
|
+
if (result && hadOwnProperty && typeof key !== "symbol") {
|
|
248
|
+
proxy.observers.notify(proxy);
|
|
244
249
|
}
|
|
245
|
-
return
|
|
250
|
+
return result;
|
|
246
251
|
},
|
|
247
252
|
|
|
248
253
|
// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc
|
|
@@ -269,6 +274,23 @@ function getHandler() {
|
|
|
269
274
|
return handler;
|
|
270
275
|
}
|
|
271
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Two NaN values are the same value, so assigning NaN over NaN is not a change.
|
|
279
|
+
* Object.is() is not used here because it would additionally treat 0 and -0 as
|
|
280
|
+
* a change and notify for assignments that do not alter the subject.
|
|
281
|
+
* @param {*} a
|
|
282
|
+
* @param {*} b
|
|
283
|
+
* @return {boolean}
|
|
284
|
+
* @private
|
|
285
|
+
*/
|
|
286
|
+
function isSameValue(a, b) {
|
|
287
|
+
if (a === b) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return Number.isNaN(a) && Number.isNaN(b);
|
|
292
|
+
}
|
|
293
|
+
|
|
272
294
|
/**
|
|
273
295
|
* Native objects with internal slots cannot be used through a transparent Proxy
|
|
274
296
|
* because their prototype methods require the original receiver.
|
|
@@ -277,5 +299,22 @@ function getHandler() {
|
|
|
277
299
|
* @private
|
|
278
300
|
*/
|
|
279
301
|
function isNativeObjectWithInternalSlots(value) {
|
|
280
|
-
|
|
302
|
+
if (
|
|
303
|
+
value instanceof Date ||
|
|
304
|
+
value instanceof RegExp ||
|
|
305
|
+
value instanceof Map ||
|
|
306
|
+
value instanceof Set ||
|
|
307
|
+
value instanceof WeakMap ||
|
|
308
|
+
value instanceof WeakSet ||
|
|
309
|
+
value instanceof Promise ||
|
|
310
|
+
value instanceof ArrayBuffer ||
|
|
311
|
+
ArrayBuffer.isView(value)
|
|
312
|
+
) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
typeof SharedArrayBuffer === "function" &&
|
|
318
|
+
value instanceof SharedArrayBuffer
|
|
319
|
+
);
|
|
281
320
|
}
|
|
@@ -84,13 +84,18 @@ class TokenList extends Base {
|
|
|
84
84
|
*/
|
|
85
85
|
contains(value) {
|
|
86
86
|
if (isString(value)) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
// Repeated or surrounding whitespace produces empty segments, which are
|
|
88
|
+
// not tokens. A value without any token contains nothing.
|
|
89
|
+
const tokens = value
|
|
90
|
+
.split(" ")
|
|
91
|
+
.map((token) => token.trim())
|
|
92
|
+
.filter((token) => token !== "");
|
|
93
|
+
|
|
94
|
+
if (tokens.length === 0) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return tokens.every((token) => this.tokens.has(token));
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
if (isIterable(value)) {
|
|
@@ -100,7 +105,7 @@ class TokenList extends Base {
|
|
|
100
105
|
if (this.tokens.has(token.trim()) === false) return false;
|
|
101
106
|
counter++;
|
|
102
107
|
}
|
|
103
|
-
return counter > 0
|
|
108
|
+
return counter > 0;
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
return false;
|
package/source/util/clone.mjs
CHANGED
|
@@ -42,6 +42,16 @@ export { clone };
|
|
|
42
42
|
* @throws {Error} unable to clone obj! its type isn't supported.
|
|
43
43
|
*/
|
|
44
44
|
function clone(obj) {
|
|
45
|
+
return cloneValue(obj, new WeakMap());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @private
|
|
50
|
+
* @param {*} obj
|
|
51
|
+
* @param {WeakMap} seen already cloned objects, keyed by source
|
|
52
|
+
* @return {*}
|
|
53
|
+
*/
|
|
54
|
+
function cloneValue(obj, seen) {
|
|
45
55
|
// typeof null results in 'object'. https://2ality.com/2013/10/typeof-null.html
|
|
46
56
|
if (null === obj) {
|
|
47
57
|
return obj;
|
|
@@ -59,9 +69,14 @@ function clone(obj) {
|
|
|
59
69
|
|
|
60
70
|
// Handle Array
|
|
61
71
|
if (isArray(obj)) {
|
|
72
|
+
if (seen.has(obj)) {
|
|
73
|
+
return seen.get(obj);
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
const copy = [];
|
|
77
|
+
seen.set(obj, copy);
|
|
63
78
|
for (let i = 0, len = obj.length; i < len; i++) {
|
|
64
|
-
copy[i] =
|
|
79
|
+
copy[i] = cloneValue(obj[i], seen);
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
return copy;
|
|
@@ -87,18 +102,28 @@ function clone(obj) {
|
|
|
87
102
|
|
|
88
103
|
// Handle Map
|
|
89
104
|
if (obj instanceof Map) {
|
|
105
|
+
if (seen.has(obj)) {
|
|
106
|
+
return seen.get(obj);
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
const copy = new Map();
|
|
110
|
+
seen.set(obj, copy);
|
|
91
111
|
for (const [key, value] of obj.entries()) {
|
|
92
|
-
copy.set(
|
|
112
|
+
copy.set(cloneValue(key, seen), cloneValue(value, seen));
|
|
93
113
|
}
|
|
94
114
|
return copy;
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
// Handle Set
|
|
98
118
|
if (obj instanceof Set) {
|
|
119
|
+
if (seen.has(obj)) {
|
|
120
|
+
return seen.get(obj);
|
|
121
|
+
}
|
|
122
|
+
|
|
99
123
|
const copy = new Set();
|
|
124
|
+
seen.set(obj, copy);
|
|
100
125
|
for (const value of obj.values()) {
|
|
101
|
-
copy.add(
|
|
126
|
+
copy.add(cloneValue(value, seen));
|
|
102
127
|
}
|
|
103
128
|
return copy;
|
|
104
129
|
}
|
|
@@ -119,7 +144,7 @@ function clone(obj) {
|
|
|
119
144
|
if (typeof navigator !== "undefined" && obj === navigator) return obj;
|
|
120
145
|
if (typeof JSON !== "undefined" && obj === JSON) return obj;
|
|
121
146
|
if (isProxy(obj)) return obj; // Handle Proxy-Object
|
|
122
|
-
return cloneObject(obj);
|
|
147
|
+
return cloneObject(obj, seen);
|
|
123
148
|
}
|
|
124
149
|
|
|
125
150
|
throw new Error("unable to clone obj! its type isn't supported.");
|
|
@@ -128,12 +153,17 @@ function clone(obj) {
|
|
|
128
153
|
/**
|
|
129
154
|
*
|
|
130
155
|
* @param {object} obj
|
|
156
|
+
* @param {WeakMap} seen already cloned objects, keyed by source
|
|
131
157
|
* @return {object}
|
|
132
158
|
* @private
|
|
133
159
|
*/
|
|
134
|
-
function cloneObject(obj) {
|
|
160
|
+
function cloneObject(obj, seen) {
|
|
135
161
|
validateObject(obj);
|
|
136
162
|
|
|
163
|
+
if (seen.has(obj)) {
|
|
164
|
+
return seen.get(obj);
|
|
165
|
+
}
|
|
166
|
+
|
|
137
167
|
const fkt = obj?.["constructor"];
|
|
138
168
|
const sourcePrototype = Object.getPrototypeOf(obj);
|
|
139
169
|
|
|
@@ -145,19 +175,19 @@ function cloneObject(obj) {
|
|
|
145
175
|
Object.prototype.hasOwnProperty.call(prototype, "getClone") &&
|
|
146
176
|
typeOf(obj.getClone) === "function"
|
|
147
177
|
) {
|
|
178
|
+
// deliberately before the memo is written: the object decides for
|
|
179
|
+
// itself what its copy is, and that result must not be replaced by
|
|
180
|
+
// a half built copy when the object is reached again inside a cycle.
|
|
148
181
|
return obj.getClone();
|
|
149
182
|
}
|
|
150
183
|
}
|
|
151
184
|
}
|
|
152
185
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
) {
|
|
159
|
-
copy = new fkt();
|
|
160
|
-
}
|
|
186
|
+
// The constructor is not invoked. Calling it would run arbitrary code and
|
|
187
|
+
// fail for every class whose constructor requires arguments. Classes that
|
|
188
|
+
// need constructor work expose getClone() instead.
|
|
189
|
+
const copy = Object.create(sourcePrototype);
|
|
190
|
+
seen.set(obj, copy);
|
|
161
191
|
|
|
162
192
|
for (const key in obj) {
|
|
163
193
|
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
@@ -169,7 +199,7 @@ function cloneObject(obj) {
|
|
|
169
199
|
continue;
|
|
170
200
|
}
|
|
171
201
|
|
|
172
|
-
copy[key] =
|
|
202
|
+
copy[key] = cloneValue(obj[key], seen);
|
|
173
203
|
}
|
|
174
204
|
|
|
175
205
|
return copy;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { expect } from "chai";
|
|
4
|
+
import { buildCurrentTests } from "../web/current-bundle.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Guards the browser test bundle for issue #577.
|
|
8
|
+
*
|
|
9
|
+
* The browser tier bundles the cases as bare `import "<file>"` statements. A
|
|
10
|
+
* `sideEffects` list in package.json is a restriction: every path it does not
|
|
11
|
+
* match counts as side effect free, and esbuild then drops those bare imports.
|
|
12
|
+
* With `./test/**` missing from the list the bundle collapsed from about 9 MB
|
|
13
|
+
* to 15 bytes, and all three browser jobs reported `0 passing, 0 failing,
|
|
14
|
+
* 0 pending` rather than failing loudly.
|
|
15
|
+
*
|
|
16
|
+
* This file runs esbuild over the repository, so it is listed in nodeOnlyCases
|
|
17
|
+
* in test/web/current-bundle.mjs and does not run in the browser bundle itself.
|
|
18
|
+
*/
|
|
19
|
+
describe("Browser test bundle issue #577", function () {
|
|
20
|
+
this.timeout(60000);
|
|
21
|
+
|
|
22
|
+
let bundle;
|
|
23
|
+
|
|
24
|
+
before(async function () {
|
|
25
|
+
bundle = await buildCurrentTests();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should collect the case files", function () {
|
|
29
|
+
expect(bundle.files).to.be.an("array");
|
|
30
|
+
expect(
|
|
31
|
+
bundle.files.length,
|
|
32
|
+
"far fewer case files than the repository contains",
|
|
33
|
+
).to.be.above(150);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should produce a bundle that actually contains the cases", function () {
|
|
37
|
+
// an emptied bundle is a few bytes, a complete one is megabytes
|
|
38
|
+
expect(
|
|
39
|
+
bundle.code.length,
|
|
40
|
+
"the bundled browser suite is far too small to hold the test cases",
|
|
41
|
+
).to.be.above(1_000_000);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should keep the suites of the bundled files", function () {
|
|
45
|
+
// every case file registers suites at module scope, so dropping the bare
|
|
46
|
+
// imports removes these calls even though the file list stays complete
|
|
47
|
+
const suites = [...bundle.code.matchAll(/\bdescribe\(/g)].length;
|
|
48
|
+
|
|
49
|
+
expect(suites, "the bundle registers almost no suites").to.be.above(300);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should contain content from several distinct case files", function () {
|
|
53
|
+
for (const marker of [
|
|
54
|
+
"Pathfinder resolution baseline",
|
|
55
|
+
"CustomElement rebind baseline",
|
|
56
|
+
"Updater remove attribute",
|
|
57
|
+
"MonthCalendar date configuration",
|
|
58
|
+
"contains all tokens",
|
|
59
|
+
]) {
|
|
60
|
+
expect(
|
|
61
|
+
bundle.code.includes(marker),
|
|
62
|
+
`the bundle does not contain "${marker}", so cases are missing`,
|
|
63
|
+
).to.be.true;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should not bundle the node only cases", function () {
|
|
68
|
+
for (const nodeOnly of ["packaging.mjs", "browser-bundle.mjs"]) {
|
|
69
|
+
expect(
|
|
70
|
+
bundle.files.some((file) => file.endsWith(`test/cases/${nodeOnly}`)),
|
|
71
|
+
`${nodeOnly} must stay out of the browser bundle`,
|
|
72
|
+
).to.be.false;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|