web-toolkit-x 0.0.1 → 0.70.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 (45) hide show
  1. package/AUTHORS +3 -0
  2. package/LICENSE +1 -1
  3. package/NOTICE +7 -0
  4. package/README.md +162 -6
  5. package/dist/release/index.d.ts +650 -0
  6. package/dist/release/index.d.ts.map +1 -0
  7. package/dist/release/index.js +2831 -0
  8. package/dist/release/index.js.map +1 -0
  9. package/dist/release/toolkit-0.70.0.js +2834 -0
  10. package/dist/release/toolkit-0.70.0.js.map +1 -0
  11. package/package.json +71 -18
  12. package/src/core/bragi.ts +122 -0
  13. package/src/core/browser.ts +538 -0
  14. package/src/core/child-nodes.ts +77 -0
  15. package/src/core/comment.ts +24 -0
  16. package/src/core/component.ts +190 -0
  17. package/src/core/created-roots.ts +38 -0
  18. package/src/core/custom-element.ts +191 -0
  19. package/src/core/description.ts +241 -0
  20. package/src/core/diff.ts +590 -0
  21. package/src/core/dispatcher.ts +221 -0
  22. package/src/core/dom.ts +124 -0
  23. package/src/core/draft.ts +609 -0
  24. package/src/core/lifecycle.ts +301 -0
  25. package/src/core/nodes.ts +13 -0
  26. package/src/core/patch.ts +412 -0
  27. package/src/core/plugins.ts +231 -0
  28. package/src/core/reconciler.ts +185 -0
  29. package/src/core/renderer.ts +75 -0
  30. package/src/core/runtime.ts +36 -0
  31. package/src/core/sandbox.ts +128 -0
  32. package/src/core/service.ts +39 -0
  33. package/src/core/template.ts +472 -0
  34. package/src/core/text.ts +24 -0
  35. package/src/core/toolkit.ts +220 -0
  36. package/src/core/utils.ts +148 -0
  37. package/src/core/virtual-dom.ts +118 -0
  38. package/src/core/virtual-element.ts +72 -0
  39. package/src/core/virtual-node.ts +87 -0
  40. package/src/core/web-component.ts +285 -0
  41. package/src/globals.d.ts +10 -0
  42. package/src/index.ts +62 -0
  43. package/src/plugins/logger.ts +38 -0
  44. package/src/release.ts +3 -0
  45. package/index.js +0 -1
@@ -0,0 +1,472 @@
1
+ import type { Template as BragiTemplate } from './bragi.js'
2
+ import Browser from './browser.js'
3
+ import {
4
+ ComponentDescription,
5
+ ElementDescription,
6
+ type Listener,
7
+ type NodeDescription,
8
+ type Props,
9
+ TextDescription,
10
+ } from './description.js'
11
+ import type { ComponentClass } from './nodes.js'
12
+ import { runtime } from './runtime.js'
13
+ import { deepFreeze, lowerDash } from './utils.js'
14
+
15
+ export type ItemType =
16
+ | 'component'
17
+ | 'function'
18
+ | 'null'
19
+ | 'node'
20
+ | 'props'
21
+ | 'unknown'
22
+ | 'string'
23
+ | 'number'
24
+ | 'bigint'
25
+ | 'boolean'
26
+ | 'symbol'
27
+ | 'undefined'
28
+
29
+ const isDefined = <T>(value: T): value is NonNullable<T> =>
30
+ value !== undefined && value !== null
31
+ const isFalsy = (template: unknown) => template === null || template === false
32
+ const isNotEmpty = (object: object) => Boolean(Object.keys(object).length)
33
+
34
+ const Template = {
35
+ /**
36
+ * Creates a normalized Description of given template.
37
+ */
38
+ describe(template: unknown): NodeDescription | null {
39
+ if (isFalsy(template)) {
40
+ return null
41
+ }
42
+
43
+ if (Array.isArray(template) && template.length) {
44
+ let description!: ComponentDescription | ElementDescription
45
+ for (const [item, type, index] of (template as unknown[]).map(
46
+ (item, index): [unknown, ItemType, number] => [
47
+ item,
48
+ this.getItemType(item),
49
+ index,
50
+ ],
51
+ )) {
52
+ if (index === 0) {
53
+ switch (type) {
54
+ case 'string':
55
+ description = new ElementDescription(item as string)
56
+ break
57
+ case 'component':
58
+ case 'function':
59
+ description = new ComponentDescription(
60
+ runtime().resolveComponentClass(item, type),
61
+ )
62
+ break
63
+ default:
64
+ console.error(
65
+ 'Invalid node type:',
66
+ item,
67
+ `(${type}) at index: ${index}, template:`,
68
+ template,
69
+ )
70
+ throw new Error(`Invalid node type specified: ${type}`)
71
+ }
72
+ continue
73
+ }
74
+ if (index === 1 && type === 'props') {
75
+ if (description.type === 'component') {
76
+ this.assignPropsToComponent(item as Props, description)
77
+ } else if (description.type === 'element') {
78
+ this.assignPropsToElement(item as Props, description)
79
+ }
80
+ continue
81
+ }
82
+ if (isFalsy(item)) {
83
+ continue
84
+ }
85
+ if (type === 'string' || type === 'number' || item === true) {
86
+ description.children = description.children || []
87
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- only strings, numbers and true
88
+ description.children.push(new TextDescription(String(item)))
89
+ continue
90
+ } else if (type === 'node') {
91
+ description.children = description.children || []
92
+ description.children.push(this.describe(item)!)
93
+ } else {
94
+ console.error(
95
+ 'Invalid item',
96
+ item,
97
+ `at index: ${index}, template:`,
98
+ template,
99
+ )
100
+ throw new Error(`Invalid item specified: ${type}`)
101
+ }
102
+ }
103
+
104
+ if (runtime().isDebug()) {
105
+ deepFreeze(description)
106
+ }
107
+ return description
108
+ }
109
+
110
+ console.error('Invalid template definition:', template)
111
+ throw new Error('Expecting array, null or false')
112
+ },
113
+
114
+ /**
115
+ * Returns a new props object supplemented by overridden values.
116
+ */
117
+ normalizeProps(...overrides: Array<Props | undefined>): Props {
118
+ const result: Props = {}
119
+ for (const override of overrides) {
120
+ for (const [key, value] of Object.entries(override || {})) {
121
+ if (result[key] === undefined && value !== undefined) {
122
+ result[key] = value
123
+ }
124
+ }
125
+ }
126
+ return result
127
+ },
128
+
129
+ /**
130
+ * Normalizes specified element props object and returns either
131
+ * a non-empty object containing only supported props or null.
132
+ */
133
+ normalizeComponentProps(
134
+ props: Props | undefined,
135
+ ComponentClass: { defaultProps?: Props },
136
+ ): Props {
137
+ return this.normalizeProps(props, ComponentClass.defaultProps || {})
138
+ },
139
+
140
+ assignPropsToComponent(object: Props, description: ComponentDescription) {
141
+ const props = this.getComponentProps(
142
+ object,
143
+ description.component,
144
+ description.isRoot,
145
+ )
146
+ if (props) {
147
+ description.props = props
148
+ if (isDefined(props.key)) {
149
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- keys are stringified as given
150
+ description.key = String(props.key)
151
+ }
152
+ if (props.attrs) {
153
+ const attrs = this.getCustomAttributes(props.attrs as Props, true)
154
+ if (attrs) {
155
+ description.attrs = attrs
156
+ }
157
+ }
158
+ }
159
+ },
160
+
161
+ getComponentProps(
162
+ object: Props,
163
+ ComponentClass: ComponentClass,
164
+ isRoot: boolean,
165
+ ): Props | null {
166
+ const props = isRoot
167
+ ? object
168
+ : this.normalizeComponentProps(object, ComponentClass)
169
+ return isNotEmpty(props) ? props : null
170
+ },
171
+
172
+ assignPropsToElement(props: Props, description: ElementDescription) {
173
+ for (const [key, value] of Object.entries(props)) {
174
+ if (key === 'key') {
175
+ if (isDefined(value)) {
176
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- keys are stringified as given
177
+ description.key = String(value)
178
+ }
179
+ } else if (key === 'class') {
180
+ const className = this.getClassName(value)
181
+ if (className) {
182
+ description.class = className
183
+ }
184
+ } else if (key === 'style') {
185
+ const style = this.getStyle(value as Props)
186
+ if (style) {
187
+ description.style = style
188
+ }
189
+ } else if (key === 'dataset') {
190
+ const dataset = this.getDataset(value as Props)
191
+ if (dataset) {
192
+ description.dataset = dataset
193
+ }
194
+ } else if (key === 'properties') {
195
+ const properties = this.getProperties(value as Props)
196
+ if (properties) {
197
+ description.properties = properties
198
+ }
199
+ } else if (key === 'attrs') {
200
+ const customAttrs = this.getCustomAttributes(value as Props)
201
+ if (customAttrs) {
202
+ description.custom = description.custom || {}
203
+ description.custom.attrs = customAttrs
204
+ }
205
+ } else if (key === 'on') {
206
+ const customListeners = this.getCustomListeners(value as Props)
207
+ if (customListeners) {
208
+ description.custom = description.custom || {}
209
+ description.custom.listeners = customListeners
210
+ }
211
+ } else if (/^on[A-Z]/.test(key)) {
212
+ const listener = this.getListener(value, key)
213
+ if (listener) {
214
+ description.listeners = description.listeners || {}
215
+ description.listeners[key] = value as Listener
216
+ }
217
+ const element = description.name
218
+ // custom elements define their events themselves
219
+ if (
220
+ runtime().isDebug() &&
221
+ !element.includes('-') &&
222
+ !Browser.isEventSupported(key, element)
223
+ ) {
224
+ console.warn(
225
+ `The "${key}" listener is not supported on "${element}" elements.`,
226
+ )
227
+ }
228
+ } else {
229
+ const attr = this.getAttributeValue(value)
230
+ if (isDefined(attr)) {
231
+ description.attrs = description.attrs || {}
232
+ description.attrs[key] = attr
233
+ }
234
+ if (runtime().isDebug()) {
235
+ const element = description.name
236
+ if (attr === undefined) {
237
+ console.warn(
238
+ `Invalid undefined value for attribute "${key}"`,
239
+ `on element "${element}".`,
240
+ )
241
+ }
242
+ // custom elements define their attributes themselves
243
+ if (
244
+ !element.includes('-') &&
245
+ !Browser.isAttributeSupported(key, element)
246
+ ) {
247
+ console.warn(
248
+ `The "${key}" attribute is not supported on "${element}" elements.`,
249
+ )
250
+ }
251
+ }
252
+ }
253
+ }
254
+ },
255
+
256
+ /**
257
+ * Returns the type of item used in the array representing node template.
258
+ */
259
+ getItemType(item: unknown): ItemType {
260
+ const type = typeof item
261
+ switch (type) {
262
+ case 'function':
263
+ // the node type is inherited from the Component or Root class
264
+ if (
265
+ (item as ComponentClass).NodeType === 'component' ||
266
+ (item as ComponentClass).NodeType === 'root'
267
+ ) {
268
+ return 'component'
269
+ }
270
+ return 'function'
271
+ case 'object':
272
+ if (item === null) {
273
+ return 'null'
274
+ } else if (Array.isArray(item)) {
275
+ return 'node'
276
+ } else if ((item as object).constructor === Object) {
277
+ return 'props'
278
+ }
279
+ return 'unknown'
280
+ default:
281
+ return type
282
+ }
283
+ },
284
+
285
+ /**
286
+ * Resolves any object to a space separated string of class names.
287
+ */
288
+ getClassName(value: unknown): string {
289
+ if (!value) {
290
+ return ''
291
+ }
292
+ if (typeof value === 'string') {
293
+ return value
294
+ }
295
+ if (Array.isArray(value)) {
296
+ return value
297
+ .map(item => this.getClassName(item))
298
+ .filter(className => className)
299
+ .join(' ')
300
+ }
301
+ if (typeof value === 'object') {
302
+ return Object.keys(value)
303
+ .filter(key => key && (value as Props)[key])
304
+ .join(' ')
305
+ }
306
+ throw new Error(`Invalid value: ${JSON.stringify(value)}`)
307
+ },
308
+
309
+ /**
310
+ * Returns either a non-empty style object containing only understood
311
+ * styling rules or null.
312
+ */
313
+ getStyle(object: Props): Record<string, string> | null {
314
+ runtime().assert(
315
+ object.constructor === Object,
316
+ 'Style must be a plain object!',
317
+ )
318
+
319
+ const style: Record<string, string> = {}
320
+ for (const [name, value] of Object.entries(object)) {
321
+ if (runtime().isDebug() && !Browser.isStyleSupported(name)) {
322
+ console.warn(`Unsupported style property, key: ${name}, value:`, value)
323
+ }
324
+ const string = this.getStyleProperty(value, name)
325
+ if (isDefined(string)) {
326
+ style[name] = string
327
+ }
328
+ }
329
+ return isNotEmpty(style) ? style : null
330
+ },
331
+
332
+ getStyleProperty(value: unknown, name: string): string | null {
333
+ if (typeof value === 'string') {
334
+ return value || "''"
335
+ } else if ([true, false, null, undefined].includes(value as boolean)) {
336
+ return null
337
+ } else if (Array.isArray(value)) {
338
+ return value.join('')
339
+ } else if (typeof value === 'number') {
340
+ return String(value)
341
+ } else if (typeof value === 'object') {
342
+ if (name !== 'filter' && name !== 'transform') {
343
+ throw new Error(`Unknown function list: ${JSON.stringify(value)}`)
344
+ }
345
+ // the filter functions are named in camel case, e.g. dropShadow for
346
+ // drop-shadow, the transform functions are in CSS too, e.g. translateX
347
+ const functions = this.getFunctionList(
348
+ value as Props,
349
+ name === 'filter' ? lowerDash : undefined,
350
+ )
351
+ if (runtime().isDebug() && functions && !CSS.supports(name, functions)) {
352
+ console.warn(`Unsupported ${name} functions: ${functions}`)
353
+ }
354
+ return functions
355
+ }
356
+ throw new Error(`Invalid style property value: ${JSON.stringify(value)}`)
357
+ },
358
+
359
+ /**
360
+ * Returns a multi-property string value.
361
+ */
362
+ getFunctionList(
363
+ object: Props,
364
+ getCSSName: (name: string) => string = name => name,
365
+ ): string {
366
+ const composite: Record<string, string> = {}
367
+ for (const [key, value] of Object.entries(object)) {
368
+ const stringValue = this.getAttributeValue(value, /*= allowEmpty */ false)
369
+ if (isDefined(stringValue)) {
370
+ composite[getCSSName(key)] = stringValue
371
+ }
372
+ }
373
+ return Object.entries(composite)
374
+ .map(([key, value]) => `${key}(${value})`)
375
+ .join(' ')
376
+ },
377
+
378
+ getListener(value: unknown, name: string): Listener | null {
379
+ if (typeof value === 'function') {
380
+ return value as Listener
381
+ }
382
+ if (value === null || value === false || value === undefined) {
383
+ return null
384
+ }
385
+ throw new Error(`Invalid listener specified for event: ${name}`)
386
+ },
387
+
388
+ /**
389
+ * Resolves given value to a string.
390
+ */
391
+ getAttributeValue(
392
+ value: unknown,
393
+ allowEmpty = true,
394
+ ): string | null | undefined {
395
+ if (value === true || value === '') {
396
+ return allowEmpty ? '' : null
397
+ } else if (typeof value === 'string') {
398
+ return value
399
+ } else if (value === null || value === false) {
400
+ return null
401
+ } else if (value === undefined) {
402
+ return undefined
403
+ } else if (Array.isArray(value)) {
404
+ return value.join('')
405
+ } else if (['object', 'function', 'symbol'].includes(typeof value)) {
406
+ throw new Error(`Invalid attribute value: ${JSON.stringify(value)}!`)
407
+ }
408
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- objects are rejected above
409
+ return String(value)
410
+ },
411
+
412
+ /**
413
+ * Returns either a non-empty dataset object or null.
414
+ */
415
+ getDataset(object: Props): Record<string, string> | null {
416
+ const dataset: Record<string, string> = {}
417
+ for (const key of Object.keys(object)) {
418
+ const value = this.getAttributeValue(object[key])
419
+ if (isDefined(value)) {
420
+ dataset[key] = value
421
+ }
422
+ }
423
+ return isNotEmpty(dataset) ? dataset : null
424
+ },
425
+
426
+ /**
427
+ * Returns either a non-empty object containing properties set
428
+ * directly on a rendered DOM Element or null.
429
+ */
430
+ getProperties(object: Props): Props | null {
431
+ return isNotEmpty(object) ? object : null
432
+ },
433
+
434
+ getCustomAttributes(
435
+ object: Props,
436
+ forComponent?: boolean,
437
+ ): Record<string, string> | null {
438
+ runtime().assert(
439
+ object.constructor === Object,
440
+ 'Expecting object for custom attributes!',
441
+ )
442
+ const attrs: Record<string, string> = {}
443
+ for (const [key, value] of Object.entries(object)) {
444
+ const attr = this.getAttributeValue(value, /*= allowEmpty */ true)
445
+ if (isDefined(attr)) {
446
+ const name = forComponent ? lowerDash(key) : key
447
+ attrs[name] = attr
448
+ }
449
+ }
450
+ return isNotEmpty(attrs) ? attrs : null
451
+ },
452
+
453
+ getCustomListeners(object: Props): Record<string, Listener> | null {
454
+ runtime().assert(
455
+ object.constructor === Object,
456
+ 'Expecting object for custom listeners!',
457
+ )
458
+ const listeners: Record<string, Listener> = {}
459
+ for (const [key, value] of Object.entries(object)) {
460
+ const listener = this.getListener(value, key)
461
+ if (listener) {
462
+ listeners[key] = listener
463
+ }
464
+ }
465
+ return isNotEmpty(listeners) ? listeners : null
466
+ },
467
+ }
468
+
469
+ /* A Bragi template, see bragi.ts, sharing its name with the module above. */
470
+ type Template = BragiTemplate
471
+
472
+ export default Template
@@ -0,0 +1,24 @@
1
+ import type { TextDescription } from './description.js'
2
+ import VirtualNode from './virtual-node.js'
3
+
4
+ class Text extends VirtualNode {
5
+ static NodeType = 'text'
6
+
7
+ declare description: TextDescription
8
+ declare ref: globalThis.Text
9
+
10
+ constructor(description: TextDescription, parentNode?: VirtualNode) {
11
+ super(description, parentNode)
12
+ this.attachDOM()
13
+ }
14
+
15
+ get nodeType(): string {
16
+ return Text.NodeType
17
+ }
18
+
19
+ attachDOM() {
20
+ this.ref = document.createTextNode(this.description.text)
21
+ }
22
+ }
23
+
24
+ export default Text
@@ -0,0 +1,220 @@
1
+ import type { PureComponent, RenderResult } from './bragi.js'
2
+ import CreatedRoots from './created-roots.js'
3
+ import { updateStylesheets } from './custom-element.js'
4
+ import type { ComponentDescription, Props } from './description.js'
5
+ import { Component, type ComponentClass, WebComponent } from './nodes.js'
6
+ import Plugins, { type PluginManifest } from './plugins.js'
7
+ import Renderer from './renderer.js'
8
+ import { provideRuntime } from './runtime.js'
9
+ import Sandbox from './sandbox.js'
10
+ import Template, { type ItemType } from './template.js'
11
+ import { isSameList } from './utils.js'
12
+ import VirtualDOM from './virtual-dom.js'
13
+
14
+ export interface Settings {
15
+ readonly debug: boolean
16
+ }
17
+
18
+ export interface Options {
19
+ debug?: boolean
20
+ plugins?: PluginManifest[]
21
+ }
22
+
23
+ /*
24
+ * The state used by the core modules through the runtime, kept out of
25
+ * the public Toolkit instance.
26
+ */
27
+
28
+ /* The top-level root components, tracking their subroots. */
29
+ const roots = new Set<WebComponent>()
30
+
31
+ /* The settings used until Toolkit is configured. */
32
+ const defaultSettings: Settings = Object.freeze({ debug: false })
33
+
34
+ /* Function to Component mapping. */
35
+ const pureComponentClassRegistry = new Map<PureComponent, ComponentClass>()
36
+
37
+ const createPlugins = (manifests: PluginManifest[] = []): Plugins => {
38
+ const plugins = new Plugins(null)
39
+ for (const manifest of manifests) {
40
+ plugins.register(manifest)
41
+ }
42
+ return plugins
43
+ }
44
+
45
+ /* The plugins configured for all root components. */
46
+ let plugins = createPlugins()
47
+
48
+ /**
49
+ * Returns a PureComponent class rendering the template
50
+ * provided by the specified function.
51
+ */
52
+ const resolvePureComponentClass = (fn: PureComponent): ComponentClass => {
53
+ let ComponentClass = pureComponentClassRegistry.get(fn)
54
+ if (ComponentClass) {
55
+ return ComponentClass
56
+ }
57
+ ComponentClass = class PureComponent extends Component {
58
+ static renderer = fn
59
+
60
+ render(): RenderResult {
61
+ // the props type of a pure component is not known here
62
+ return fn.call(this, this.props as never)
63
+ }
64
+ }
65
+ pureComponentClassRegistry.set(fn, ComponentClass)
66
+ return ComponentClass
67
+ }
68
+
69
+ /**
70
+ * Returns resolved Component class.
71
+ */
72
+ const resolveComponentClass = (
73
+ component: unknown,
74
+ type: ItemType,
75
+ ): ComponentClass => {
76
+ switch (type) {
77
+ case 'component':
78
+ return component as ComponentClass
79
+ case 'function':
80
+ return resolvePureComponentClass(component as PureComponent)
81
+ default:
82
+ throw new Error(`Unsupported component type: ${type}`)
83
+ }
84
+ }
85
+
86
+ const track = (root: WebComponent) => {
87
+ if (root.parentNode) {
88
+ const parentRootNode = root.parentNode.rootNode
89
+ parentRootNode.subroots.add(root)
90
+ root.stopTracking = () => {
91
+ parentRootNode.subroots.delete(root)
92
+ }
93
+ } else {
94
+ roots.add(root)
95
+ root.stopTracking = () => {
96
+ roots.delete(root)
97
+ }
98
+ }
99
+ }
100
+
101
+ class Toolkit {
102
+ declare settings: Settings
103
+ declare assert: (condition: unknown, message?: string) => void
104
+
105
+ constructor() {
106
+ this.settings = defaultSettings
107
+ this.assert = console.assert as Toolkit['assert']
108
+ }
109
+
110
+ /**
111
+ * Configures Toolkit with given options, keeping the current values
112
+ * of the options not provided, by default no debug mode and no plugins.
113
+ * Changed plugins are uninstalled from the created roots, and the new
114
+ * ones installed in their place, with their stylesheets loaded.
115
+ */
116
+ configure(options: Options) {
117
+ if (options.debug !== undefined) {
118
+ this.settings = Object.freeze({ debug: options.debug })
119
+ }
120
+ const manifests = options.plugins
121
+ const previous = [...plugins]
122
+ const origins = previous.map(plugin => plugin.origin)
123
+ if (!manifests || isSameList(origins, manifests)) {
124
+ return
125
+ }
126
+ // the methods of the remaining plugins are registered again below
127
+ Sandbox.clearPluginMethods()
128
+ plugins = createPlugins(manifests)
129
+ for (const root of this.tracked) {
130
+ for (const plugin of previous) {
131
+ root.plugins!.uninstall(plugin.name)
132
+ }
133
+ for (const plugin of root.plugins!) {
134
+ plugin.register?.()
135
+ }
136
+ for (const plugin of plugins) {
137
+ root.plugins!.register(plugin)
138
+ root.plugins!.install(plugin)
139
+ }
140
+ if (root.shadow) {
141
+ updateStylesheets(root)
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Resets Toolkit to a pristine state, with the default settings
148
+ * and no plugins, forgetting the created roots.
149
+ */
150
+ reset() {
151
+ plugins.destroy()
152
+ plugins = createPlugins()
153
+ roots.clear()
154
+ this.settings = defaultSettings
155
+ pureComponentClassRegistry.clear()
156
+ Sandbox.clearPluginMethods()
157
+ }
158
+
159
+ /* The created root components, with their subroots, until destroyed. */
160
+ get tracked(): WebComponent[] {
161
+ const tracked: WebComponent[] = []
162
+ for (const root of roots) {
163
+ tracked.push(root, ...root.tracked)
164
+ }
165
+ return tracked
166
+ }
167
+
168
+ isDebug(): boolean {
169
+ return this.settings.debug
170
+ }
171
+
172
+ warn(...messages: unknown[]) {
173
+ if (this.isDebug()) {
174
+ console.warn(...messages)
175
+ }
176
+ }
177
+
178
+ async createRoot(
179
+ component: ComponentClass,
180
+ props: Props = {},
181
+ ): Promise<WebComponent> {
182
+ const description = Template.describe([
183
+ component,
184
+ props,
185
+ ]) as ComponentDescription
186
+ // not to stay tracked, with the plugins installed, when it throws
187
+ return CreatedRoots.destroyingRootsOnError(() =>
188
+ VirtualDOM.createWebComponent(description, null),
189
+ )
190
+ }
191
+
192
+ async render(
193
+ component: ComponentClass,
194
+ container: Element,
195
+ props: Props = {},
196
+ ): Promise<WebComponent> {
197
+ const root = await this.createRoot(component, props)
198
+ return root.mount(container)
199
+ }
200
+ }
201
+
202
+ /* The Toolkit singleton, exposed globally as toolkit. */
203
+ export const toolkit = new Toolkit()
204
+
205
+ // asserts, debug mode and warnings are read from the instance, which
206
+ // can be configured, e.g. with the assert function throwing in tests
207
+ provideRuntime({
208
+ get plugins() {
209
+ return plugins
210
+ },
211
+ assert: (condition, message) => toolkit.assert(condition, message),
212
+ isDebug: () => toolkit.isDebug(),
213
+ warn: (...messages) => toolkit.warn(...messages),
214
+ track,
215
+ resolveComponentClass,
216
+ update: (root, from, to, commands) =>
217
+ Renderer.update(root, from, to, commands),
218
+ })
219
+
220
+ export default Toolkit