@schukai/monster 4.153.1 → 4.154.1

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 (41) hide show
  1. package/package.json +1 -1
  2. package/source/components/form/message-state-button.mjs +1 -1
  3. package/source/components/form/select-link.mjs +12 -1
  4. package/source/components/state/log.mjs +3 -7
  5. package/source/components/state/thread.mjs +3 -7
  6. package/source/components/time/day.mjs +0 -19
  7. package/source/components/time/month-calendar.mjs +5 -1
  8. package/source/data/buildmap.mjs +3 -3
  9. package/source/data/datasource.mjs +3 -10
  10. package/source/data/diff.mjs +13 -12
  11. package/source/data/extend.mjs +23 -1
  12. package/source/data/pathfinder.mjs +186 -19
  13. package/source/dom/customelement.mjs +273 -30
  14. package/source/dom/updater.mjs +6 -2
  15. package/source/types/base.mjs +7 -6
  16. package/source/types/basewithoptions.mjs +1 -8
  17. package/source/types/internal.mjs +4 -10
  18. package/source/types/observerlist.mjs +9 -17
  19. package/source/types/proxyobserver.mjs +19 -2
  20. package/source/types/tokenlist.mjs +13 -8
  21. package/source/util/clone.mjs +44 -14
  22. package/test/cases/browser-bundle.mjs +75 -0
  23. package/test/cases/components/datatable/drag-scroll.mjs +41 -9
  24. package/test/cases/components/navigation/site-navigation.mjs +10 -1
  25. package/test/cases/components/time/month-calendar.mjs +115 -0
  26. package/test/cases/data/diff.mjs +86 -0
  27. package/test/cases/data/extend.mjs +52 -0
  28. package/test/cases/data/pathfinder-resolution-baseline.mjs +322 -0
  29. package/test/cases/data/pathfinder.mjs +129 -0
  30. package/test/cases/dom/customelement-rebind-baseline.mjs +323 -0
  31. package/test/cases/dom/customelement-teardown.mjs +408 -0
  32. package/test/cases/dom/customelement.mjs +354 -0
  33. package/test/cases/dom/dimension.mjs +34 -10
  34. package/test/cases/dom/updater-remove.mjs +198 -0
  35. package/test/cases/packaging.mjs +213 -0
  36. package/test/cases/types/base.mjs +50 -0
  37. package/test/cases/types/observerlist.mjs +103 -1
  38. package/test/cases/types/proxyobserver.mjs +114 -0
  39. package/test/cases/types/regex.mjs +4 -0
  40. package/test/cases/types/tokenlist.mjs +64 -1
  41. package/test/cases/util/clone.mjs +115 -0
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+
3
+ import { expect } from "chai";
4
+ import { existsSync, readFileSync, statSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ /**
9
+ * Packaging regressions for issue #577.
10
+ *
11
+ * Two classes of mistake are guarded here. An exports map that does not cover
12
+ * an established import path turns a patch release into a breaking change, and
13
+ * a field added to package.json never reaches the npm package unless the
14
+ * whitelist in nix/scripts/build.nix carries it too, because the published
15
+ * manifest is reassembled there from scratch.
16
+ *
17
+ * This file reads the repository, so it is listed in nodeOnlyCases in
18
+ * test/web/current-bundle.mjs and does not run in the browser bundle.
19
+ */
20
+ const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
21
+
22
+ function readPackageJson() {
23
+ return JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf8"));
24
+ }
25
+
26
+ function readBuildNix() {
27
+ return readFileSync(join(projectRoot, "nix/scripts/build.nix"), "utf8");
28
+ }
29
+
30
+ describe("Packaging issue #577", function () {
31
+ describe("exports map", function () {
32
+ it("should resolve every literal export target to an existing file", function () {
33
+ const pkg = readPackageJson();
34
+ expect(pkg.exports, "exports map is missing").to.be.an("object");
35
+
36
+ for (const [subpath, target] of Object.entries(pkg.exports)) {
37
+ if (typeof target !== "string" || target.includes("*")) {
38
+ continue;
39
+ }
40
+
41
+ const absolute = join(projectRoot, target);
42
+ expect(
43
+ existsSync(absolute),
44
+ `${subpath} points at a missing file: ${target}`,
45
+ ).to.be.true;
46
+ }
47
+ });
48
+
49
+ it("should expose the package entry point", function () {
50
+ const pkg = readPackageJson();
51
+ const entry = pkg.exports["."];
52
+
53
+ expect(entry, "no entry point exported").to.not.equal(undefined);
54
+
55
+ const target = typeof entry === "string" ? entry : entry.default;
56
+ expect(target).to.equal(`./${pkg.main}`);
57
+ expect(existsSync(join(projectRoot, target))).to.be.true;
58
+ });
59
+
60
+ it("should keep every published top level path reachable", function () {
61
+ const pkg = readPackageJson();
62
+ const patterns = Object.keys(pkg.exports);
63
+
64
+ function covered(entry) {
65
+ const head = entry.split("/")[0];
66
+ return patterns.some(
67
+ (pattern) =>
68
+ pattern === `./${entry}` ||
69
+ pattern === `./${head}/*` ||
70
+ pattern.startsWith(`./${head}/`),
71
+ );
72
+ }
73
+
74
+ // Adding an exports map restricts what consumers may import. Anything
75
+ // listed in files was importable before and has to stay importable.
76
+ for (const entry of pkg.files) {
77
+ expect(covered(entry), `files entry "${entry}" is not covered by exports`)
78
+ .to.be.true;
79
+ }
80
+
81
+ // npm publishes these regardless of files, so they were reachable too
82
+ for (const entry of ["README.md", "LICENSE"]) {
83
+ expect(
84
+ covered(entry),
85
+ `"${entry}" ships in the package but is not covered by exports`,
86
+ ).to.be.true;
87
+ }
88
+ });
89
+
90
+ it("should resolve established deep import paths through the patterns", function () {
91
+ const pkg = readPackageJson();
92
+
93
+ const established = [
94
+ "source/monster.mjs",
95
+ "source/dom/customelement.mjs",
96
+ "source/dom/updater.mjs",
97
+ "source/data/pathfinder.mjs",
98
+ "source/types/tokenlist.mjs",
99
+ "source/util/clone.mjs",
100
+ "source/components/form/select.mjs",
101
+ "test/util/jsdom.mjs",
102
+ ];
103
+
104
+ for (const path of established) {
105
+ const head = path.split("/")[0];
106
+ const pattern = pkg.exports[`./${head}/*`];
107
+
108
+ expect(pattern, `no pattern export for ./${head}/*`).to.be.a("string");
109
+
110
+ const target = pattern.replace("*", path.slice(head.length + 1));
111
+ expect(
112
+ existsSync(join(projectRoot, target)),
113
+ `${path} does not resolve through ${pattern}`,
114
+ ).to.be.true;
115
+ }
116
+ });
117
+ });
118
+
119
+ describe("side effect metadata", function () {
120
+ it("should not mark component modules as side effect free", function () {
121
+ const pkg = readPackageJson();
122
+
123
+ expect(pkg.sideEffects, "sideEffects is missing").to.be.an("array");
124
+
125
+ // every component module calls registerCustomElement() at module scope,
126
+ // so a bundler must never drop one
127
+ const covered = pkg.sideEffects.some(
128
+ (pattern) =>
129
+ pattern === "./source/components/**" ||
130
+ pattern === "./source/**" ||
131
+ pattern === "./source/components/*",
132
+ );
133
+
134
+ expect(covered, "component modules are not declared side effectful").to.be
135
+ .true;
136
+ expect(pkg.sideEffects).to.include("./source/monster.mjs");
137
+ });
138
+ });
139
+
140
+ describe("published manifest", function () {
141
+ it("should carry every publish relevant field into the build whitelist", function () {
142
+ const pkg = readPackageJson();
143
+ const buildNix = readBuildNix();
144
+
145
+ // fields that only matter for development stay out of the package
146
+ const developmentOnly = new Set([
147
+ "devDependencies",
148
+ "main",
149
+ "module",
150
+ "type",
151
+ ]);
152
+
153
+ for (const field of Object.keys(pkg)) {
154
+ if (developmentOnly.has(field)) {
155
+ continue;
156
+ }
157
+
158
+ expect(
159
+ buildNix.includes(`${field} =`),
160
+ `package.json field "${field}" is missing from releasedPackageJson in nix/scripts/build.nix, so it never reaches the npm package`,
161
+ ).to.be.true;
162
+ }
163
+ });
164
+ });
165
+
166
+ describe("generated barrel", function () {
167
+ it("should export the core symbols", function () {
168
+ const barrel = readFileSync(
169
+ join(projectRoot, "source/monster.mjs"),
170
+ "utf8",
171
+ );
172
+
173
+ const core = [
174
+ "./dom/customelement.mjs",
175
+ "./dom/customcontrol.mjs",
176
+ "./dom/updater.mjs",
177
+ "./data/pathfinder.mjs",
178
+ "./data/diff.mjs",
179
+ "./data/extend.mjs",
180
+ "./types/base.mjs",
181
+ "./types/proxyobserver.mjs",
182
+ "./types/observer.mjs",
183
+ "./types/tokenlist.mjs",
184
+ "./util/clone.mjs",
185
+ ];
186
+
187
+ for (const module of core) {
188
+ expect(
189
+ barrel.includes(`from "${module}"`),
190
+ `the generated barrel no longer re-exports ${module}`,
191
+ ).to.be.true;
192
+ }
193
+ });
194
+
195
+ it("should reference only existing modules", function () {
196
+ const barrel = readFileSync(
197
+ join(projectRoot, "source/monster.mjs"),
198
+ "utf8",
199
+ );
200
+
201
+ const matches = [...barrel.matchAll(/from "(\.\/[^"]+)"/g)];
202
+ expect(matches.length).to.be.greaterThan(100);
203
+
204
+ for (const match of matches) {
205
+ const target = join(projectRoot, "source", match[1]);
206
+ expect(
207
+ existsSync(target) && statSync(target).isFile(),
208
+ `the barrel references a missing module: ${match[1]}`,
209
+ ).to.be.true;
210
+ }
211
+ });
212
+ });
213
+ });
@@ -25,6 +25,26 @@ class Subclass extends BaseDifferentRealm {
25
25
  }
26
26
  }
27
27
 
28
+ // an own instanceSymbol whose getter yields undefined, which is the only way to
29
+ // reach the traversal branch that lost its receiver, see issue #574
30
+ class UndefinedSymbolBelowBase extends Base {
31
+ static get [instanceSymbol]() {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ class UnrelatedWithUndefinedSymbol extends Object {
37
+ static get [instanceSymbol]() {
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ class DeepUndefinedSymbol extends UndefinedSymbolBelowBase {
43
+ static get [instanceSymbol]() {
44
+ return undefined;
45
+ }
46
+ }
47
+
28
48
  describe('Base', function () {
29
49
 
30
50
  describe('new Base', function () {
@@ -60,4 +80,34 @@ describe('Base', function () {
60
80
 
61
81
  })
62
82
 
83
+ describe('instance symbol traversal issue #574', function () {
84
+
85
+ // net: traversal results that are already correct
86
+ it('should keep reporting a subclass with its own symbol as an instance', function () {
87
+ expect(new Subclass).to.be.instanceOf(Base);
88
+ });
89
+
90
+ it('should not report a plain object or a foreign class as an instance', function () {
91
+ expect({}).to.not.be.instanceOf(Base);
92
+ expect(new (class Foreign {})).to.not.be.instanceOf(Base);
93
+ });
94
+
95
+ // regression: the branch for an undefined symbol dropped its receiver, so
96
+ // `this.hasOwnProperty` threw a TypeError in strict mode module code
97
+ it('should not throw when a class in the chain yields an undefined symbol', function () {
98
+ expect(() => new UndefinedSymbolBelowBase instanceof Base).to.not.throw();
99
+ expect(() => new UnrelatedWithUndefinedSymbol instanceof Base).to.not.throw();
100
+ });
101
+
102
+ it('should keep walking the chain past an undefined symbol', function () {
103
+ expect(new UndefinedSymbolBelowBase).to.be.instanceOf(Base);
104
+ expect(new DeepUndefinedSymbol).to.be.instanceOf(Base);
105
+ });
106
+
107
+ it('should still report false for an unrelated class with an undefined symbol', function () {
108
+ expect(new UnrelatedWithUndefinedSymbol).to.not.be.instanceOf(Base);
109
+ });
110
+
111
+ })
112
+
63
113
  })
@@ -103,5 +103,107 @@ describe('ObserverList', function () {
103
103
  });
104
104
 
105
105
 
106
- });
106
+ describe('.detach() removes every occurrence issue #572', function () {
107
+
108
+ // net: behaviour that is already correct and must stay correct
109
+ it('should remove a single attached observer', function () {
110
+ let observerList = new ObserverList()
111
+ let o = new Observer(() => {
112
+ })
113
+
114
+ observerList.attach(o)
115
+ expect(observerList.contains(o)).to.be.true;
116
+
117
+ observerList.detach(o)
118
+ expect(observerList.contains(o)).to.be.false;
119
+ expect(observerList.observers.length).to.equal(0);
120
+ });
121
+
122
+ it('should keep other observers when one is detached', function () {
123
+ let observerList = new ObserverList()
124
+ let o1 = new Observer(() => {
125
+ })
126
+ let o2 = new Observer(() => {
127
+ })
128
+
129
+ observerList.attach(o1).attach(o2)
130
+ observerList.detach(o1)
131
+
132
+ expect(observerList.contains(o1)).to.be.false;
133
+ expect(observerList.contains(o2)).to.be.true;
134
+ expect(observerList.observers.length).to.equal(1);
135
+ });
136
+
137
+ it('should ignore detaching an observer that was never attached', function () {
138
+ let observerList = new ObserverList()
139
+ let attached = new Observer(() => {
140
+ })
141
+ let foreign = new Observer(() => {
142
+ })
143
+
144
+ observerList.attach(attached)
145
+ observerList.detach(foreign)
146
+
147
+ expect(observerList.contains(attached)).to.be.true;
148
+ expect(observerList.observers.length).to.equal(1);
149
+ });
107
150
 
151
+ it('should remove every occurrence when the duplicates are not adjacent', function () {
152
+ let observerList = new ObserverList()
153
+ let target = new Observer(() => {
154
+ })
155
+ let other = new Observer(() => {
156
+ })
157
+
158
+ observerList.attach(target).attach(other).attach(target)
159
+ expect(observerList.observers.length).to.equal(3);
160
+
161
+ observerList.detach(target)
162
+
163
+ expect(observerList.contains(target)).to.be.false;
164
+ expect(observerList.contains(other)).to.be.true;
165
+ expect(observerList.observers.length).to.equal(1);
166
+ });
167
+
168
+ // regression: forward splicing with a cached length skips the entry that
169
+ // moves into the freed slot, so adjacent duplicates survive detaching
170
+ it('should remove a doubly attached observer completely', function () {
171
+ let observerList = new ObserverList()
172
+ let o = new Observer(() => {
173
+ })
174
+
175
+ observerList.attach(o).attach(o)
176
+ expect(observerList.observers.length).to.equal(2);
177
+
178
+ observerList.detach(o)
179
+
180
+ expect(observerList.contains(o)).to.be.false;
181
+ expect(observerList.observers.length).to.equal(0);
182
+ });
183
+
184
+ it('should stop notifying a doubly attached observer after detaching', function (done) {
185
+ let observerList = new ObserverList()
186
+ let calls = 0
187
+ let o = new Observer(() => {
188
+ calls++
189
+ })
190
+
191
+ observerList.attach(o).attach(o)
192
+ observerList.detach(o)
193
+
194
+ observerList.notify({}).then(() => {
195
+ setTimeout(() => {
196
+ try {
197
+ expect(calls).to.equal(0);
198
+ done();
199
+ } catch (e) {
200
+ done(e);
201
+ }
202
+ }, 10);
203
+ }).catch(done);
204
+ });
205
+
206
+ });
207
+
208
+
209
+ });
@@ -419,4 +419,118 @@ describe('ProxyObserver', function () {
419
419
  });
420
420
  });
421
421
 
422
+ describe('numeric identity issue #571', function () {
423
+
424
+ // net: assigning an unchanged primitive must not notify, a changed one must
425
+ it('should not notify when an identical number is assigned', async function () {
426
+ const proxy = new ProxyObserver({a: 1});
427
+ let notifications = 0;
428
+ proxy.attachObserver(new Observer(function () {
429
+ notifications++;
430
+ }));
431
+
432
+ proxy.getSubject().a = 1;
433
+ await new Promise((resolve) => setTimeout(resolve, 0));
434
+
435
+ expect(notifications).to.equal(0);
436
+ });
437
+
438
+ it('should notify once when a number actually changes', async function () {
439
+ const proxy = new ProxyObserver({a: 1});
440
+ let notifications = 0;
441
+ proxy.attachObserver(new Observer(function () {
442
+ notifications++;
443
+ }));
444
+
445
+ proxy.getSubject().a = 2;
446
+ await new Promise((resolve) => setTimeout(resolve, 0));
447
+
448
+ expect(notifications).to.equal(1);
449
+ expect(proxy.getRealSubject().a).to.equal(2);
450
+ });
451
+
452
+ it('should keep notifying when NaN is replaced by a number', async function () {
453
+ const proxy = new ProxyObserver({a: NaN});
454
+ let notifications = 0;
455
+ proxy.attachObserver(new Observer(function () {
456
+ notifications++;
457
+ }));
458
+
459
+ proxy.getSubject().a = 5;
460
+ await new Promise((resolve) => setTimeout(resolve, 0));
461
+
462
+ expect(notifications).to.equal(1);
463
+ expect(proxy.getRealSubject().a).to.equal(5);
464
+ });
465
+
466
+ it('should keep notifying when a number is replaced by NaN', async function () {
467
+ const proxy = new ProxyObserver({a: 5});
468
+ let notifications = 0;
469
+ proxy.attachObserver(new Observer(function () {
470
+ notifications++;
471
+ }));
472
+
473
+ proxy.getSubject().a = NaN;
474
+ await new Promise((resolve) => setTimeout(resolve, 0));
475
+
476
+ expect(notifications).to.equal(1);
477
+ expect(Number.isNaN(proxy.getRealSubject().a)).to.be.true;
478
+ });
479
+
480
+ // regression: an identical NaN is not a change
481
+ it('should not notify when an identical NaN is assigned', async function () {
482
+ const proxy = new ProxyObserver({a: NaN});
483
+ let notifications = 0;
484
+ proxy.attachObserver(new Observer(function () {
485
+ notifications++;
486
+ }));
487
+
488
+ proxy.getSubject().a = NaN;
489
+ await new Promise((resolve) => setTimeout(resolve, 0));
490
+
491
+ expect(notifications).to.equal(0);
492
+ });
493
+
494
+ it('should not notify when an identical NaN is assigned on a nested path', async function () {
495
+ const proxy = new ProxyObserver({options: {size: NaN}});
496
+ let notifications = 0;
497
+ proxy.attachObserver(new Observer(function () {
498
+ notifications++;
499
+ }));
500
+
501
+ proxy.getSubject().options.size = NaN;
502
+ await new Promise((resolve) => setTimeout(resolve, 0));
503
+
504
+ expect(notifications).to.equal(0);
505
+ });
506
+
507
+ it('should not notify when an identical NaN is assigned to an array entry', async function () {
508
+ const proxy = new ProxyObserver({values: [NaN, 1]});
509
+ let notifications = 0;
510
+ proxy.attachObserver(new Observer(function () {
511
+ notifications++;
512
+ }));
513
+
514
+ proxy.getSubject().values[0] = NaN;
515
+ await new Promise((resolve) => setTimeout(resolve, 0));
516
+
517
+ expect(notifications).to.equal(0);
518
+ });
519
+
520
+ it('should not notify repeatedly when the same NaN is written many times', async function () {
521
+ const proxy = new ProxyObserver({a: NaN});
522
+ let notifications = 0;
523
+ proxy.attachObserver(new Observer(function () {
524
+ notifications++;
525
+ }));
526
+
527
+ for (let i = 0; i < 5; i++) {
528
+ proxy.getSubject().a = NaN;
529
+ }
530
+ await new Promise((resolve) => setTimeout(resolve, 0));
531
+
532
+ expect(notifications).to.equal(0);
533
+ });
534
+ });
535
+
422
536
  })
@@ -8,6 +8,10 @@ describe('escapeString', function () {
8
8
 
9
9
  before(function (done) {
10
10
 
11
+ // A cold ESM load of the library plus the jsdom bootstrap can exceed the
12
+ // 2000ms default, which made this hook fail depending on file order.
13
+ this.timeout(20000);
14
+
11
15
  let promises = []
12
16
  promises.push(initJSDOM());
13
17
  if(!globalThis['crypto']) {
@@ -257,5 +257,68 @@ describe('TokenList', function () {
257
257
  });
258
258
 
259
259
 
260
- });
260
+ describe('contains all tokens issue #572', function () {
261
+
262
+ // net: behaviour that is already correct and must stay correct
263
+ it('should contain a multi token string when every token is present', function () {
264
+ let list = new TokenList("start middle end");
265
+ expect(list.contains('start middle')).to.be.true;
266
+ expect(list.contains('start middle end')).to.be.true;
267
+ expect(list.contains('end start')).to.be.true;
268
+ });
269
+
270
+ it('should ignore surrounding and repeated whitespace', function () {
271
+ let list = new TokenList("start middle end");
272
+ expect(list.contains(' start middle ')).to.be.true;
273
+ expect(list.contains(' end ')).to.be.true;
274
+ });
275
+
276
+ it('should not contain an empty or whitespace only value', function () {
277
+ let list = new TokenList("start middle end");
278
+ expect(list.contains('')).to.be.false;
279
+ expect(list.contains(' ')).to.be.false;
280
+ expect(list.contains([])).to.be.false;
281
+ });
282
+
283
+ it('should keep the iterable branch requiring every token', function () {
284
+ let list = new TokenList("start middle end");
285
+ expect(list.contains(['start', 'middle'])).to.be.true;
286
+ expect(list.contains(['start', 'xyz'])).to.be.false;
287
+ });
288
+
289
+ // regression: a multi token string needs every token, not just one
290
+ it('should not contain a multi token string when one token is missing', function () {
291
+ let list = new TokenList("start middle end");
292
+ expect(list.contains('start xyz')).to.be.false;
293
+ });
261
294
 
295
+ it('should not contain a multi token string when the missing token comes first', function () {
296
+ let list = new TokenList("start middle end");
297
+ expect(list.contains('xyz start')).to.be.false;
298
+ });
299
+
300
+ it('should not contain a multi token string when only two of three tokens match', function () {
301
+ let list = new TokenList("start middle end");
302
+ expect(list.contains('start middle xyz')).to.be.false;
303
+ });
304
+
305
+ it('should answer identically for a string and the equivalent array', function () {
306
+ let list = new TokenList("start middle end");
307
+ let cases = [
308
+ ['start middle', ['start', 'middle']],
309
+ ['start xyz', ['start', 'xyz']],
310
+ ['xyz abc', ['xyz', 'abc']],
311
+ ['end', ['end']],
312
+ ];
313
+
314
+ for (let [asString, asArray] of cases) {
315
+ expect(list.contains(asString)).to.equal(
316
+ list.contains(asArray),
317
+ 'mismatch for ' + JSON.stringify(asString),
318
+ );
319
+ }
320
+ });
321
+ });
322
+
323
+
324
+ });