web-toolkit-x 0.0.1 → 0.71.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 (46) hide show
  1. package/AUTHORS +3 -0
  2. package/LICENSE +1 -1
  3. package/NOTICE +7 -0
  4. package/README.md +169 -6
  5. package/dist/release/global.d.ts +10 -0
  6. package/dist/release/index.d.ts +650 -0
  7. package/dist/release/index.d.ts.map +1 -0
  8. package/dist/release/index.js +2837 -0
  9. package/dist/release/index.js.map +1 -0
  10. package/dist/release/toolkit-0.71.0.js +2840 -0
  11. package/dist/release/toolkit-0.71.0.js.map +1 -0
  12. package/package.json +75 -18
  13. package/src/core/bragi.ts +122 -0
  14. package/src/core/browser.ts +555 -0
  15. package/src/core/child-nodes.ts +77 -0
  16. package/src/core/comment.ts +24 -0
  17. package/src/core/component.ts +190 -0
  18. package/src/core/created-roots.ts +38 -0
  19. package/src/core/custom-element.ts +191 -0
  20. package/src/core/description.ts +241 -0
  21. package/src/core/diff.ts +590 -0
  22. package/src/core/dispatcher.ts +221 -0
  23. package/src/core/dom.ts +124 -0
  24. package/src/core/draft.ts +609 -0
  25. package/src/core/lifecycle.ts +301 -0
  26. package/src/core/nodes.ts +13 -0
  27. package/src/core/patch.ts +412 -0
  28. package/src/core/plugins.ts +231 -0
  29. package/src/core/reconciler.ts +185 -0
  30. package/src/core/renderer.ts +75 -0
  31. package/src/core/runtime.ts +36 -0
  32. package/src/core/sandbox.ts +128 -0
  33. package/src/core/service.ts +39 -0
  34. package/src/core/template.ts +472 -0
  35. package/src/core/text.ts +24 -0
  36. package/src/core/toolkit.ts +220 -0
  37. package/src/core/utils.ts +148 -0
  38. package/src/core/virtual-dom.ts +118 -0
  39. package/src/core/virtual-element.ts +72 -0
  40. package/src/core/virtual-node.ts +87 -0
  41. package/src/core/web-component.ts +285 -0
  42. package/src/global.d.ts +10 -0
  43. package/src/index.ts +62 -0
  44. package/src/plugins/logger.ts +38 -0
  45. package/src/release.ts +3 -0
  46. package/index.js +0 -1
@@ -0,0 +1,590 @@
1
+ import type {
2
+ ComponentDescription,
3
+ ElementDescription,
4
+ Listener,
5
+ NodeDescription,
6
+ } from './description.js'
7
+ import CreatedRoots from './created-roots.js'
8
+ import Lifecycle, { throwErrors } from './lifecycle.js'
9
+ import type {
10
+ Component,
11
+ ParentVirtualNode,
12
+ VirtualElement,
13
+ VirtualNode,
14
+ WebComponent,
15
+ } from './nodes.js'
16
+ import Patch from './patch.js'
17
+ import Reconciler from './reconciler.js'
18
+ import type { State } from './dispatcher.js'
19
+ import Template from './template.js'
20
+ import { runtime } from './runtime.js'
21
+ import VirtualDOM from './virtual-dom.js'
22
+
23
+ /*
24
+ * Returns the keys added to, removed from and changed in the next object,
25
+ * in the order of the objects.
26
+ */
27
+ const compareKeys = <T>(
28
+ current: Record<string, T>,
29
+ next: Record<string, T>,
30
+ isEqual: (value: T, nextValue: T) => boolean = (value, nextValue) =>
31
+ value === nextValue,
32
+ ) => {
33
+ const keys = Object.keys(current)
34
+ const nextKeys = Object.keys(next)
35
+ return {
36
+ added: nextKeys.filter(key => !keys.includes(key)),
37
+ removed: keys.filter(key => !nextKeys.includes(key)),
38
+ changed: keys.filter(
39
+ key => nextKeys.includes(key) && !isEqual(current[key]!, next[key]!),
40
+ ),
41
+ }
42
+ }
43
+
44
+ class Diff {
45
+ declare root: WebComponent
46
+ declare patches: Patch[]
47
+
48
+ /**
49
+ * Creates a new instance bound to a root component
50
+ * with an empty list of patches.
51
+ */
52
+ constructor(
53
+ root: WebComponent,
54
+ currentState: State | undefined,
55
+ nextState: State,
56
+ ) {
57
+ this.root = root
58
+ this.patches = []
59
+ // nothing is applied when the calculation fails, e.g. when rendering
60
+ CreatedRoots.destroyingRootsOnError(() =>
61
+ this.calculate(currentState, nextState),
62
+ )
63
+ }
64
+
65
+ /**
66
+ * Adds the patch to the underlying list.
67
+ */
68
+ addPatch(patch: Patch) {
69
+ return this.patches.push(patch)
70
+ }
71
+
72
+ /**
73
+ * Applies all the patches onto the bound root node.
74
+ */
75
+ apply(): Patch[] {
76
+ if (this.patches.length) {
77
+ // the update is completed when hooks or patches throw, as stopping
78
+ // would leave the DOM, the virtual nodes and the hooks called out of
79
+ // sync, and the errors are thrown once it is applied
80
+ const errors: unknown[] = []
81
+ Lifecycle.collectingErrors(errors, () =>
82
+ Lifecycle.beforeUpdate(this.patches),
83
+ )
84
+ // a nested root failing to render its props keeps its description,
85
+ // to render them on the next update
86
+ const failedRoots = new Set<VirtualNode>()
87
+ const applied: Patch[] = []
88
+ for (const patch of this.patches) {
89
+ if (
90
+ patch.type === Patch.Type.UPDATE_NODE &&
91
+ failedRoots.has(patch.node)
92
+ ) {
93
+ continue
94
+ }
95
+ try {
96
+ Patch.apply(patch)
97
+ } catch (error) {
98
+ errors.push(error)
99
+ if (patch.type === Patch.Type.UPDATE_ROOT) {
100
+ failedRoots.add(patch.root)
101
+ }
102
+ continue
103
+ }
104
+ applied.push(patch)
105
+ }
106
+ Lifecycle.collectingErrors(errors, () => Lifecycle.afterUpdate(applied))
107
+ throwErrors(errors, 'Errors applying the update')
108
+ }
109
+ return this.patches
110
+ }
111
+
112
+ /**
113
+ * Calculates and returns all patches needed for transformation
114
+ * of the rendered DOM fragment from one state to another.
115
+ */
116
+ calculate(currentState: State | undefined, nextState: State) {
117
+ if (!currentState) {
118
+ this.addPatch(Patch.initRootComponent(this.root))
119
+ }
120
+
121
+ if (Diff.deepEqual(currentState, nextState)) {
122
+ return
123
+ }
124
+
125
+ const template: unknown[] = [this.root.constructor, nextState]
126
+ if (this.root.description.children) {
127
+ template.push(
128
+ ...this.root.description.children.map(child => child.asTemplate),
129
+ )
130
+ }
131
+
132
+ const description = Template.describe(template) as ComponentDescription
133
+
134
+ // the root is rendered initially even when the description is equal
135
+ if (
136
+ !this.root.isInitialized ||
137
+ !Diff.deepEqual(this.root.description, description)
138
+ ) {
139
+ this.componentPatches(this.root, description)
140
+ }
141
+ if (this.root.description.attrs || description.attrs) {
142
+ this.attributePatches(
143
+ this.root.description.attrs,
144
+ description.attrs,
145
+ this.root,
146
+ true,
147
+ )
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Renders the descendants with normalized props and children passed
153
+ * from the parent component.
154
+ *
155
+ * Calculates the patches needed for transformation of a component
156
+ * to match the given description.
157
+ */
158
+ componentPatches(component: Component, description: ComponentDescription) {
159
+ const nodeDescription = component.renderDescription(
160
+ description.props,
161
+ description.childrenAsTemplates,
162
+ )
163
+ this.componentContentPatches(nodeDescription, component)
164
+
165
+ this.addPatch(Patch.updateNode(component, description))
166
+ }
167
+
168
+ componentContentPatches(
169
+ description: NodeDescription | null,
170
+ parent: Component,
171
+ ) {
172
+ const content = parent.content
173
+
174
+ if (!content && !description) {
175
+ return
176
+ }
177
+
178
+ // content and description are either both present or both missing
179
+ if (!content || !description) {
180
+ throw new Error('Invalid component state!')
181
+ }
182
+
183
+ // update
184
+ if (content.description.isCompatible(description)) {
185
+ this.childPatches(content, description)
186
+ return
187
+ }
188
+
189
+ // replace
190
+ const node = VirtualDOM.createFromDescription(
191
+ description,
192
+ parent,
193
+ this.root,
194
+ )
195
+ this.addPatch(Patch.setContent(node!, parent))
196
+ }
197
+
198
+ /**
199
+ * Calculates patches for transformation of specified child node
200
+ * to match given description.
201
+ */
202
+ childPatches(child: VirtualNode, description: NodeDescription): void {
203
+ // the only comparison of the descriptions, as they are compared deeply
204
+ if (Diff.deepEqual(child.description, description)) {
205
+ return
206
+ }
207
+ if (child.isComponent()) {
208
+ if (child.isRoot()) {
209
+ // the child nodes of a Web Component are rendered in the light DOM
210
+ this.childrenPatches(child.childNodes, description.children, child)
211
+ // the root renders its own content when the patches are applied,
212
+ // before the node is updated, as it compares its description
213
+ this.addPatch(
214
+ Patch.updateRoot(child, description as ComponentDescription),
215
+ )
216
+ this.addPatch(Patch.updateNode(child, description))
217
+ return
218
+ }
219
+ return this.componentPatches(child, description as ComponentDescription)
220
+ }
221
+ if (child.isElement()) {
222
+ return this.elementPatches(child, description as ElementDescription)
223
+ }
224
+ throw new Error(`Unsupported node type: ${child.nodeType}`)
225
+ }
226
+
227
+ /**
228
+ * Calculates patches for transformation of an element to match given
229
+ * description.
230
+ */
231
+ elementPatches(element: VirtualElement, description: ElementDescription) {
232
+ this.classNamePatches(element.description.class, description.class, element)
233
+ this.stylePatches(element.description.style, description.style, element)
234
+ this.attributePatches(element.description.attrs, description.attrs, element)
235
+ this.listenerPatches(
236
+ element.description.listeners,
237
+ description.listeners,
238
+ element,
239
+ )
240
+ this.datasetPatches(
241
+ element.description.dataset,
242
+ description.dataset,
243
+ element,
244
+ )
245
+ this.propertiesPatches(
246
+ element.description.properties,
247
+ description.properties,
248
+ element,
249
+ )
250
+
251
+ if (element.description.custom || description.custom) {
252
+ this.attributePatches(
253
+ element.description.custom && element.description.custom.attrs,
254
+ description.custom && description.custom.attrs,
255
+ element,
256
+ true,
257
+ )
258
+ this.listenerPatches(
259
+ element.description.custom && element.description.custom.listeners,
260
+ description.custom && description.custom.listeners,
261
+ element,
262
+ true,
263
+ )
264
+ }
265
+
266
+ if (element.children || description.children) {
267
+ this.childrenPatches(element.children, description.children, element)
268
+ }
269
+ this.addPatch(Patch.updateNode(element, description))
270
+ }
271
+
272
+ classNamePatches(current = '', next = '', target: VirtualElement) {
273
+ if (current !== next) {
274
+ this.addPatch(Patch.setClassName(next, target))
275
+ }
276
+ }
277
+
278
+ stylePatches(
279
+ current: Record<string, string> = {},
280
+ next: Record<string, string> = {},
281
+ target: VirtualElement,
282
+ ) {
283
+ const { added, removed, changed } = compareKeys(current, next)
284
+
285
+ for (const prop of added) {
286
+ this.addPatch(Patch.setStyleProperty(prop, next[prop]!, target))
287
+ }
288
+ for (const prop of removed) {
289
+ this.addPatch(Patch.removeStyleProperty(prop, target))
290
+ }
291
+ for (const prop of changed) {
292
+ this.addPatch(Patch.setStyleProperty(prop, next[prop]!, target))
293
+ }
294
+ }
295
+
296
+ attributePatches(
297
+ current: Record<string, string> = {},
298
+ next: Record<string, string> = {},
299
+ target: VirtualElement | WebComponent,
300
+ isCustom = false,
301
+ ) {
302
+ const { added, removed, changed } = compareKeys(current, next)
303
+
304
+ for (const attr of added) {
305
+ this.addPatch(Patch.setAttribute(attr, next[attr]!, target, isCustom))
306
+ }
307
+ for (const attr of removed) {
308
+ this.addPatch(Patch.removeAttribute(attr, target, isCustom))
309
+ }
310
+ for (const attr of changed) {
311
+ this.addPatch(Patch.setAttribute(attr, next[attr]!, target, isCustom))
312
+ }
313
+ }
314
+
315
+ listenerPatches(
316
+ current: Record<string, Listener> = {},
317
+ next: Record<string, Listener> = {},
318
+ target: VirtualElement,
319
+ isCustom = false,
320
+ ) {
321
+ // listeners bound by the sandbox are equal when bound to the same method
322
+ const { added, removed, changed } = compareKeys(
323
+ current,
324
+ next,
325
+ (listener, nextListener) =>
326
+ listener === nextListener ||
327
+ (listener.source !== undefined &&
328
+ listener.source === nextListener.source),
329
+ )
330
+
331
+ for (const event of added) {
332
+ this.addPatch(Patch.addListener(event, next[event]!, target, isCustom))
333
+ }
334
+ for (const event of removed) {
335
+ this.addPatch(
336
+ Patch.removeListener(event, current[event]!, target, isCustom),
337
+ )
338
+ }
339
+ for (const event of changed) {
340
+ this.addPatch(
341
+ Patch.replaceListener(
342
+ event,
343
+ current[event]!,
344
+ next[event]!,
345
+ target,
346
+ isCustom,
347
+ ),
348
+ )
349
+ }
350
+ }
351
+
352
+ datasetPatches(
353
+ current: Record<string, string> = {},
354
+ next: Record<string, string> = {},
355
+ target: VirtualElement,
356
+ ) {
357
+ const { added, removed, changed } = compareKeys(current, next)
358
+
359
+ for (const attr of added) {
360
+ this.addPatch(Patch.setDataAttribute(attr, next[attr]!, target))
361
+ }
362
+ for (const attr of removed) {
363
+ this.addPatch(Patch.removeDataAttribute(attr, target))
364
+ }
365
+ for (const attr of changed) {
366
+ this.addPatch(Patch.setDataAttribute(attr, next[attr]!, target))
367
+ }
368
+ }
369
+
370
+ propertiesPatches(
371
+ current: Record<string, unknown> = {},
372
+ next: Record<string, unknown> = {},
373
+ target: VirtualElement,
374
+ ) {
375
+ const { added, removed, changed } = compareKeys(
376
+ current,
377
+ next,
378
+ (value, nextValue) => Diff.deepEqual(value, nextValue),
379
+ )
380
+
381
+ for (const key of added) {
382
+ this.addPatch(Patch.setProperty(key, next[key], target))
383
+ }
384
+ for (const key of removed) {
385
+ this.addPatch(Patch.deleteProperty(key, target))
386
+ }
387
+ for (const key of changed) {
388
+ this.addPatch(Patch.setProperty(key, next[key], target))
389
+ }
390
+ }
391
+
392
+ childrenPatches(
393
+ sourceNodes: VirtualNode[] = [],
394
+ targetDescriptions: NodeDescription[] = [],
395
+ parent: ParentVirtualNode,
396
+ ) {
397
+ const Move = Reconciler.Move
398
+
399
+ const created = new Set<VirtualNode>()
400
+ const createdNodesMap = new Map<string, VirtualNode>()
401
+
402
+ const createNode = (description: NodeDescription, key: string) => {
403
+ const node = VirtualDOM.createFromDescription(
404
+ description,
405
+ parent,
406
+ this.root,
407
+ )
408
+ created.add(node!)
409
+ createdNodesMap.set(key, node!)
410
+ return node!
411
+ }
412
+
413
+ const from = sourceNodes.map(
414
+ (node, index) => node.key || Diff.createKey(index),
415
+ )
416
+ const to = targetDescriptions.map(
417
+ (description, index) => description.key || Diff.createKey(index),
418
+ )
419
+
420
+ if (runtime().isDebug()) {
421
+ const assertUniqueKeys = (keys: string[]) => {
422
+ if (keys.length) {
423
+ const uniqueKeys = [...new Set(keys)]
424
+ if (uniqueKeys.length !== keys.length) {
425
+ throw new Error(`Non-unique keys detected in: ${keys.join(', ')}`)
426
+ }
427
+ }
428
+ }
429
+ assertUniqueKeys(from)
430
+ assertUniqueKeys(to)
431
+ }
432
+
433
+ // with the same keys in the same order, there are no nodes to move
434
+ if (from.length === to.length && from.every((key, i) => key === to[i])) {
435
+ for (let i = 0; i < sourceNodes.length; i++) {
436
+ this.elementChildPatches(
437
+ sourceNodes[i]!,
438
+ targetDescriptions[i]!,
439
+ parent,
440
+ )
441
+ }
442
+ return
443
+ }
444
+
445
+ // the first of the items with the same key, which only debug rejects
446
+ const mapByKey = <T>(keys: string[], items: T[]) => {
447
+ const map = new Map<string, T>()
448
+ keys.forEach((key, index) => {
449
+ if (!map.has(key)) {
450
+ map.set(key, items[index]!)
451
+ }
452
+ })
453
+ return map
454
+ }
455
+ const sourceNodesMap = mapByKey(from, sourceNodes)
456
+ const targetDescriptionsMap = mapByKey(to, targetDescriptions)
457
+
458
+ const getNode = (key: string, isMove: boolean): VirtualNode => {
459
+ const sourceNode = sourceNodesMap.get(key)
460
+ if (sourceNode) {
461
+ return sourceNode
462
+ }
463
+ if (isMove) {
464
+ return createdNodesMap.get(key)!
465
+ }
466
+ return createNode(targetDescriptionsMap.get(key)!, key)
467
+ }
468
+
469
+ const moves = Reconciler.calculateMoves(from, to)
470
+
471
+ const children = [...sourceNodes]
472
+ for (const move of moves) {
473
+ const node = getNode(move.item, move.name === Move.Name.MOVE)
474
+ switch (move.name) {
475
+ case Move.Name.REMOVE:
476
+ this.addPatch(Patch.removeChild(node, move.at!, parent))
477
+ Move.remove(node, move.at!).make(children)
478
+ continue
479
+ case Move.Name.INSERT:
480
+ this.addPatch(Patch.insertChild(node, move.at!, parent))
481
+ Move.insert(node, move.at!).make(children)
482
+ continue
483
+ case Move.Name.MOVE:
484
+ this.addPatch(Patch.moveChild(node, move.from!, move.to!, parent))
485
+ Move.move(node, move.from!, move.to!).make(children)
486
+ continue
487
+ }
488
+ }
489
+ for (let i = 0; i < children.length; i++) {
490
+ const child = children[i]!
491
+ if (!created.has(child)) {
492
+ const targetDescription = targetDescriptions[i]!
493
+ this.elementChildPatches(child, targetDescription, parent)
494
+ }
495
+ }
496
+ }
497
+
498
+ elementChildPatches(
499
+ child: VirtualNode,
500
+ description: NodeDescription,
501
+ parent: ParentVirtualNode,
502
+ ) {
503
+ if (child.description.isCompatible(description)) {
504
+ this.childPatches(child, description)
505
+ } else {
506
+ const node = VirtualDOM.createFromDescription(
507
+ description,
508
+ parent,
509
+ this.root,
510
+ )
511
+ this.addPatch(Patch.replaceChild(child, node!, parent))
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Returns a normalized type of given item.
517
+ */
518
+ static getType(item: unknown): string {
519
+ const type = typeof item
520
+ if (type !== 'object') {
521
+ return type
522
+ }
523
+ if (item === null) {
524
+ return 'null'
525
+ }
526
+ if (Array.isArray(item)) {
527
+ return 'array'
528
+ }
529
+ return 'object'
530
+ }
531
+
532
+ static createKey(index: number): string {
533
+ return String(index).padStart(8, '0')
534
+ }
535
+
536
+ static deepEqual(current: unknown, next: unknown): boolean {
537
+ if (Object.is(current, next)) {
538
+ return true
539
+ }
540
+ const type = this.getType(current)
541
+ const nextType = this.getType(next)
542
+ if (type !== nextType) {
543
+ return false
544
+ }
545
+ if (type === 'array') {
546
+ const currentArray = current as unknown[]
547
+ const nextArray = next as unknown[]
548
+ if (currentArray.length !== nextArray.length) {
549
+ return false
550
+ }
551
+ for (let i = 0; i < currentArray.length; i++) {
552
+ const equal = this.deepEqual(currentArray[i], nextArray[i])
553
+ if (!equal) {
554
+ return false
555
+ }
556
+ }
557
+ return true
558
+ } else if (type === 'object') {
559
+ const currentObject = current as Record<string, unknown>
560
+ const nextObject = next as Record<string, unknown>
561
+ if (currentObject.constructor !== nextObject.constructor) {
562
+ return false
563
+ }
564
+ // these keep their values in internal slots, not in own keys
565
+ if (current instanceof Date) {
566
+ return Object.is(current.getTime(), (next as Date).getTime())
567
+ }
568
+ if (current instanceof Map || current instanceof Set) {
569
+ return this.deepEqual([...current], [...(next as typeof current)])
570
+ }
571
+ const keys = Object.keys(currentObject)
572
+ if (keys.length !== Object.keys(nextObject).length) {
573
+ return false
574
+ }
575
+ // with as many keys, the same keys when each one is in both objects
576
+ for (const key of keys) {
577
+ if (
578
+ !Object.prototype.propertyIsEnumerable.call(nextObject, key) ||
579
+ !this.deepEqual(currentObject[key], nextObject[key])
580
+ ) {
581
+ return false
582
+ }
583
+ }
584
+ return true
585
+ }
586
+ return false
587
+ }
588
+ }
589
+
590
+ export default Diff