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,190 @@
1
+ import type { Child, RenderResult } from './bragi.js'
2
+ import {
3
+ CommentDescription,
4
+ type ComponentDescription,
5
+ type NodeDescription,
6
+ type Props,
7
+ } from './description.js'
8
+ import type Dispatcher from './dispatcher.js'
9
+ import type { Commands, CommandsAPI } from './dispatcher.js'
10
+ import { runtime } from './runtime.js'
11
+ import Sandbox, { type ComponentSandbox } from './sandbox.js'
12
+ import Template from './template.js'
13
+ import { type AnyFunction, invariant } from './utils.js'
14
+ import VirtualNode, { type NodeRef } from './virtual-node.js'
15
+ import type WebComponent from './web-component.js'
16
+
17
+ /* A task run when a component is destroyed, e.g. disconnecting a service. */
18
+ export type CleanUpTask = (() => void) & { service?: unknown }
19
+
20
+ /* A service components connect to, e.g. a subclass of Service. */
21
+ export interface Connectable {
22
+ connect(listeners: Record<string, unknown>): () => void
23
+ }
24
+
25
+ /* The class of any component, whatever its props. */
26
+ export type ComponentClass = typeof Component<object>
27
+
28
+ /*
29
+ * Node representing Component in the virtual DOM tree.
30
+ * Components
31
+ */
32
+ class Component<P extends object = object> extends VirtualNode {
33
+ static NodeType = 'component'
34
+
35
+ declare static elementName?: string
36
+ declare static defaultProps?: Props
37
+ declare static commands?: CommandsAPI | CommandsAPI[]
38
+
39
+ static get displayName(): string {
40
+ return this.name
41
+ }
42
+
43
+ declare description: ComponentDescription
44
+ declare sandbox: ComponentSandbox
45
+ declare cleanUpTasks: CleanUpTask[]
46
+ declare isInitialized: boolean
47
+ declare content: VirtualNode | null
48
+
49
+ /* The component props, available on the sandbox passed as `this`. */
50
+ declare props: P
51
+
52
+ /* The child templates, available on the sandbox passed as `this`. */
53
+ declare children: Child[]
54
+
55
+ onCreated?(): void
56
+ onAttached?(): void
57
+ onPropsReceived?(nextProps: P): void
58
+ onUpdated?(prevProps: P): void
59
+ onDestroyed?(): void
60
+ onDetached?(): void
61
+
62
+ constructor(
63
+ description: ComponentDescription,
64
+ parent?: VirtualNode | null,
65
+ context?: WebComponent | null,
66
+ attachDOM = true,
67
+ ) {
68
+ super(description, parent, context)
69
+ this.sandbox = Sandbox.create(this, COMPONENT_PROPERTIES)
70
+ this.cleanUpTasks = []
71
+ this.isInitialized = attachDOM
72
+ if (attachDOM) {
73
+ this.attachDOM()
74
+ }
75
+ // the rendered content is inserted right after instantiation
76
+ this.content = null
77
+ }
78
+
79
+ /**
80
+ * Sets the component content.
81
+ */
82
+ setContent(node: VirtualNode) {
83
+ invariant(
84
+ node.parentNode === this,
85
+ 'Specified node does not have a valid parent!',
86
+ )
87
+ this.content!.parentNode = null
88
+ node.parentNode = this
89
+ this.content!.ref.replaceWith(node.ref)
90
+ this.content = node
91
+ }
92
+
93
+ hasOwnMethod(method: string): boolean {
94
+ // eslint-disable-next-line no-prototype-builtins
95
+ return (this.constructor as ComponentClass).prototype.hasOwnProperty(method)
96
+ }
97
+
98
+ connectTo(service: Connectable, listeners: Record<string, unknown>) {
99
+ runtime().assert(
100
+ typeof service.connect === 'function',
101
+ 'Services have to define the connect() method',
102
+ )
103
+ const disconnect: CleanUpTask = service.connect(listeners)
104
+ runtime().assert(
105
+ typeof disconnect === 'function',
106
+ 'The result of the connect() method has to be a disconnect() method',
107
+ )
108
+ disconnect.service = service
109
+ this.cleanUpTasks.push(disconnect)
110
+ }
111
+
112
+ get childElement(): VirtualNode | null {
113
+ if (this.content) {
114
+ if (this.content.isElement() || this.content.isRoot()) {
115
+ return this.content
116
+ }
117
+ if (this.content.isComponent()) {
118
+ return this.content.childElement
119
+ }
120
+ }
121
+ return null
122
+ }
123
+
124
+ get placeholder(): VirtualNode | null {
125
+ if (this.content!.isComment()) {
126
+ return this.content
127
+ }
128
+ return (this.content as Component).placeholder || null
129
+ }
130
+
131
+ render(): RenderResult {
132
+ return undefined
133
+ }
134
+
135
+ /**
136
+ * Calls the render method and transforms the returned template
137
+ * into the normalised description of the rendered node.
138
+ */
139
+ renderDescription(
140
+ props: Props = {},
141
+ children: unknown[] = [],
142
+ ): NodeDescription | null {
143
+ this.sandbox.props = props
144
+ this.sandbox.children = children
145
+ const template = this.render.call(this.sandbox)
146
+ if (template) {
147
+ return Template.describe(template)
148
+ }
149
+ const text = (this.constructor as ComponentClass).displayName
150
+ return new CommentDescription(text)
151
+ }
152
+
153
+ /* The commands of the root component, whose types are not known here. */
154
+ get commands(): Commands<unknown> & Record<string, AnyFunction> {
155
+ return this.context ? this.context.commands : this.rootNode.commands
156
+ }
157
+
158
+ get dispatcher(): Dispatcher {
159
+ return this.context ? this.context.dispatcher : this.rootNode.dispatcher
160
+ }
161
+
162
+ destroy() {
163
+ for (const cleanUpTask of this.cleanUpTasks) {
164
+ cleanUpTask()
165
+ }
166
+ }
167
+
168
+ get nodeType(): string {
169
+ return Component.NodeType
170
+ }
171
+
172
+ get ref(): NodeRef {
173
+ return this.content!.ref
174
+ }
175
+
176
+ isCompatible(node: VirtualNode | null | undefined) {
177
+ return super.isCompatible(node) && this.constructor === node!.constructor
178
+ }
179
+
180
+ attachDOM() {
181
+ if (this.content) {
182
+ this.content.attachDOM()
183
+ }
184
+ }
185
+ }
186
+
187
+ /* The properties of the Component class hidden from the sandbox. */
188
+ const COMPONENT_PROPERTIES = Object.getOwnPropertyNames(Component.prototype)
189
+
190
+ export default Component
@@ -0,0 +1,38 @@
1
+ import type { WebComponent } from './nodes.js'
2
+
3
+ /* The roots created by the function called by destroyingRootsOnError(). */
4
+ let createdRoots: WebComponent[] | null = null
5
+
6
+ const CreatedRoots = {
7
+ /**
8
+ * Calls the function, destroying the roots it creates when it throws.
9
+ * They are neither attached nor destroyed by the lifecycle then, but
10
+ * would stay tracked, with the plugins installed.
11
+ */
12
+ destroyingRootsOnError<T>(fn: () => T): T {
13
+ const outerRoots = createdRoots
14
+ const roots: WebComponent[] = []
15
+ createdRoots = roots
16
+ try {
17
+ const result = fn()
18
+ outerRoots?.push(...roots)
19
+ return result
20
+ } catch (error) {
21
+ for (const root of roots.reverse()) {
22
+ root.destroy()
23
+ }
24
+ throw error
25
+ } finally {
26
+ createdRoots = outerRoots
27
+ }
28
+ },
29
+
30
+ /**
31
+ * Records a root being created, see destroyingRootsOnError().
32
+ */
33
+ collectRoot(root: WebComponent) {
34
+ createdRoots?.push(root)
35
+ },
36
+ }
37
+
38
+ export default CreatedRoots
@@ -0,0 +1,191 @@
1
+ import DOM from './dom.js'
2
+ import Lifecycle, { throwErrors } from './lifecycle.js'
3
+ import type { WebComponent } from './nodes.js'
4
+ import Plugins, { type Plugin, type PluginManifest } from './plugins.js'
5
+ import { isSameList } from './utils.js'
6
+
7
+ const cssImports = (paths: string[]) =>
8
+ paths.map(path => `@import url(${path});`).join('\n')
9
+
10
+ type PluginOrManifest = Plugin | PluginManifest
11
+
12
+ export class ComponentElement extends HTMLElement {
13
+ declare $root: WebComponent | null
14
+ declare pendingDestruction?: ReturnType<typeof setTimeout>
15
+ declare stylesheets: string[]
16
+ declare styleElement: HTMLStyleElement | null
17
+ declare stylesheetsLoaded: Promise<void>
18
+ declare install: (plugin: PluginOrManifest, cascade?: boolean) => void
19
+ declare uninstall: (
20
+ plugin: PluginOrManifest | string,
21
+ cascade?: boolean,
22
+ ) => void
23
+
24
+ constructor(root: WebComponent) {
25
+ super()
26
+ this.$root = root
27
+ this.stylesheets = []
28
+ this.styleElement = null
29
+ this.stylesheetsLoaded = Promise.resolve()
30
+
31
+ addPluginsAPI(this)
32
+
33
+ root.shadow = this.attachShadow({
34
+ mode: 'open',
35
+ })
36
+
37
+ const stylesheets = root.getStylesheets()
38
+
39
+ const init = () => {
40
+ root.init().catch((error: Error) => root.markAsFailed(error))
41
+ }
42
+
43
+ if (stylesheets.length) {
44
+ // rejects mounting, as the errors are not thrown to the caller
45
+ this.loadStylesheets(stylesheets).then(init, (error: Error) =>
46
+ root.markAsFailed(error),
47
+ )
48
+ } else {
49
+ init()
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Replaces the stylesheets imported in the shadow root, keeping the
55
+ * previous ones until the new ones are loaded.
56
+ */
57
+ loadStylesheets(stylesheets: string[]): Promise<void> {
58
+ const previous = this.styleElement
59
+ this.stylesheets = stylesheets
60
+ let loaded = Promise.resolve()
61
+ if (stylesheets.length) {
62
+ const style = document.createElement('style')
63
+ style.textContent = cssImports(stylesheets)
64
+ loaded = new Promise((resolve, reject) => {
65
+ style.onload = () => resolve()
66
+ style.onerror = () =>
67
+ reject(
68
+ new Error(`Error loading stylesheets: ${stylesheets.join(', ')}`),
69
+ )
70
+ })
71
+ if (previous) {
72
+ previous.after(style)
73
+ } else {
74
+ this.shadowRoot!.prepend(style)
75
+ }
76
+ this.styleElement = style
77
+ } else {
78
+ this.styleElement = null
79
+ }
80
+ if (previous) {
81
+ // removed once loaded, as the root may be waiting to be initialized
82
+ void Promise.allSettled([this.stylesheetsLoaded, loaded]).then(() =>
83
+ previous.remove(),
84
+ )
85
+ }
86
+ this.stylesheetsLoaded = loaded
87
+ return loaded
88
+ }
89
+
90
+ get isComponentElement() {
91
+ return true
92
+ }
93
+
94
+ connectedCallback() {
95
+ clearTimeout(this.pendingDestruction)
96
+ }
97
+
98
+ disconnectedCallback() {
99
+ this.pendingDestruction = setTimeout(() => this.destroy(), 50)
100
+ }
101
+
102
+ destroy() {
103
+ const root = this.$root
104
+ if (!root) {
105
+ // already destroyed, e.g. directly before the scheduled destruction
106
+ return
107
+ }
108
+ // the hooks of all the components are called when some of them throw
109
+ const errors: unknown[] = []
110
+ Lifecycle.collectingErrors(errors, () => {
111
+ Lifecycle.onComponentDestroyed(root)
112
+ Lifecycle.onComponentDetached(root)
113
+ })
114
+ root.ref = null
115
+ this.$root = null
116
+ throwErrors(errors, 'Errors destroying the component')
117
+ }
118
+ }
119
+
120
+ const addPluginsAPI = (element: ComponentElement) => {
121
+ const { Plugin } = Plugins
122
+ element.install = (plugin, cascade = true) => {
123
+ const installTo = (root: WebComponent) => {
124
+ if (plugin instanceof Plugin) {
125
+ root.plugins!.registry.add(plugin)
126
+ root.plugins!.install(plugin)
127
+ } else {
128
+ root.plugins!.install(plugin)
129
+ }
130
+ if (cascade) {
131
+ for (const subroot of root.subroots) {
132
+ installTo(subroot)
133
+ }
134
+ }
135
+ }
136
+ installTo(element.$root!)
137
+ }
138
+ element.uninstall = (plugin, cascade = true) => {
139
+ const name = typeof plugin === 'string' ? plugin : plugin.name
140
+ const uninstallFrom = (root: WebComponent) => {
141
+ root.plugins!.uninstall(name)
142
+ if (cascade) {
143
+ for (const subroot of root.subroots) {
144
+ uninstallFrom(subroot)
145
+ }
146
+ }
147
+ }
148
+ uninstallFrom(element.$root!)
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Loads the stylesheets of the root in its custom element when they
154
+ * changed, e.g. with the plugins reconfigured.
155
+ */
156
+ export const updateStylesheets = (root: WebComponent) => {
157
+ const element = root.ref as ComponentElement
158
+ const stylesheets = root.getStylesheets()
159
+ if (!isSameList(stylesheets, element.stylesheets)) {
160
+ element
161
+ .loadStylesheets(stylesheets)
162
+ .catch((error: unknown) => console.error(error))
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Creates a new Custom Element instance assigned to specified Web Component.
168
+ */
169
+ export const createComponentElement = (
170
+ root: WebComponent,
171
+ ): ComponentElement => {
172
+ const defineCustomElementClass = (RootClass: typeof WebComponent) => {
173
+ let ElementClass = customElements.get(RootClass.elementName!) as
174
+ typeof ComponentElement | undefined
175
+ if (!ElementClass) {
176
+ ElementClass = class RootElement extends ComponentElement {}
177
+ customElements.define(RootClass.elementName!, ElementClass)
178
+ RootClass.prototype.elementClass = ElementClass
179
+ }
180
+ return ElementClass
181
+ }
182
+ const ElementClass = defineCustomElementClass(
183
+ root.constructor as typeof WebComponent,
184
+ )
185
+ const element = new ElementClass(root)
186
+ // the attributes of a root are custom, as set by the diff
187
+ for (const [name, value] of Object.entries(root.description.attrs ?? {})) {
188
+ DOM.setAttribute(element, name, value, true)
189
+ }
190
+ return element
191
+ }
@@ -0,0 +1,241 @@
1
+ import type { ComponentClass } from './nodes.js'
2
+
3
+ export type Props = Record<string, unknown>
4
+
5
+ /* An event listener, possibly bound to a component by its sandbox. */
6
+ export type Listener = ((event: Event) => unknown) & { source?: unknown }
7
+
8
+ export type NodeDescription =
9
+ | ComponentDescription
10
+ | ElementDescription
11
+ | CommentDescription
12
+ | TextDescription
13
+
14
+ /* A normalized template, as returned by the asTemplate getters. */
15
+ export type NormalizedTemplate = unknown[] | string | null
16
+
17
+ /*
18
+ * Normalized description of a template.
19
+ * Is used to calculate differences between nodes.
20
+ */
21
+ abstract class Description {
22
+ declare static ElementDescription: typeof ElementDescription
23
+ declare static ComponentDescription: typeof ComponentDescription
24
+ declare static CommentDescription: typeof CommentDescription
25
+ declare static TextDescription: typeof TextDescription
26
+
27
+ declare key?: string
28
+ declare children?: NodeDescription[]
29
+
30
+ abstract get asTemplate(): NormalizedTemplate
31
+
32
+ get childrenAsTemplates(): NormalizedTemplate[] | undefined {
33
+ if (this.children) {
34
+ return this.children.map(child => child.asTemplate)
35
+ }
36
+ return undefined
37
+ }
38
+
39
+ isCompatible(description: Description): boolean {
40
+ return this.constructor === description.constructor
41
+ }
42
+ }
43
+
44
+ /*
45
+ * Defines a normalized description of a component.
46
+ *
47
+ * Enumerable properties:
48
+ * - key (a unique node identifier within its parent),
49
+ * - component (an object with meta information)
50
+ * - children (an array of child nodes)
51
+ * - props (an object of any component rendering props)
52
+ *
53
+ * Non-enumerable properties:
54
+ * - asTemplate: returns component description as a normalized template
55
+ */
56
+ class ComponentDescription extends Description {
57
+ declare component: ComponentClass
58
+ declare type: 'component'
59
+ declare props?: Props
60
+ declare attrs?: Record<string, string>
61
+
62
+ constructor(component: ComponentClass) {
63
+ super()
64
+ this.component = component
65
+ this.type = 'component'
66
+ }
67
+
68
+ isCompatible(description: Description): boolean {
69
+ return (
70
+ super.isCompatible(description) &&
71
+ this.component === (description as ComponentDescription).component
72
+ )
73
+ }
74
+
75
+ get isRoot(): boolean {
76
+ // the node type is inherited from the Root class
77
+ return this.component.NodeType === 'root'
78
+ }
79
+
80
+ get asTemplate(): unknown[] {
81
+ const template: unknown[] = [this.component]
82
+ if (this.props) {
83
+ template.push(this.props)
84
+ }
85
+ if (this.children) {
86
+ template.push(...this.children.map(child => child.asTemplate))
87
+ }
88
+ return template
89
+ }
90
+ }
91
+
92
+ /*
93
+ * Defines a normalized description of an element.
94
+ *
95
+ * Enumerable properties:
96
+ * - key (a unique node identifier within its parent),
97
+ * - name (a string representing tag name),
98
+ * - children (an array of child nodes),
99
+ * - props (an object) defining:
100
+ * - class (a class name string)
101
+ * - style (an object for style property to string value mapping)
102
+ * - listeners (an object for event name to listener mapping)
103
+ * - attrs (an object for normalized attribute name to value mapping)
104
+ * - dataset (an object representing data attributes)
105
+ * - properties (an object for properties set directly on DOM element)
106
+ *
107
+ * Non-enumerable properties:
108
+ * - asTemplate: returns element description as a normalized template
109
+ */
110
+ class ElementDescription extends Description {
111
+ declare name: string
112
+ declare type: 'element'
113
+ declare class?: string
114
+ declare style?: Record<string, string>
115
+ declare attrs?: Record<string, string>
116
+ declare dataset?: Record<string, string>
117
+ declare listeners?: Record<string, Listener>
118
+ declare properties?: Record<string, unknown>
119
+ declare custom?: {
120
+ attrs?: Record<string, string>
121
+ listeners?: Record<string, Listener>
122
+ }
123
+
124
+ constructor(name: string) {
125
+ super()
126
+ this.name = name
127
+ this.type = 'element'
128
+ }
129
+
130
+ isCompatible(description: Description): boolean {
131
+ return (
132
+ super.isCompatible(description) &&
133
+ this.name === (description as ElementDescription).name
134
+ )
135
+ }
136
+
137
+ get asTemplate(): unknown[] {
138
+ const template: unknown[] = [this.name]
139
+ const props: Record<string, unknown> = {}
140
+ if (this.key) {
141
+ props.key = this.key
142
+ }
143
+ if (this.class) {
144
+ props.class = this.class
145
+ }
146
+ if (this.style) {
147
+ props.style = this.style
148
+ }
149
+ if (this.attrs) {
150
+ Object.assign(props, this.attrs)
151
+ }
152
+ if (this.dataset) {
153
+ props.dataset = this.dataset
154
+ }
155
+ if (this.listeners) {
156
+ Object.assign(props, this.listeners)
157
+ }
158
+ if (this.properties) {
159
+ props.properties = this.properties
160
+ }
161
+ if (this.custom?.attrs) {
162
+ props.attrs = this.custom.attrs
163
+ }
164
+ if (this.custom?.listeners) {
165
+ props.on = this.custom.listeners
166
+ }
167
+ if (Object.keys(props).length) {
168
+ template.push(props)
169
+ }
170
+ if (this.children) {
171
+ template.push(...this.children.map(child => child.asTemplate))
172
+ }
173
+ return template
174
+ }
175
+ }
176
+
177
+ /*
178
+ * Description of a Comment node.
179
+ */
180
+ class CommentDescription extends Description {
181
+ declare text: string
182
+ declare type: 'comment'
183
+
184
+ constructor(text: string) {
185
+ super()
186
+ this.text = text
187
+ this.type = 'comment'
188
+ }
189
+
190
+ get asTemplate(): null {
191
+ return null
192
+ }
193
+
194
+ isCompatible(description: Description): boolean {
195
+ return (
196
+ super.isCompatible(description) &&
197
+ this.text === (description as CommentDescription).text
198
+ )
199
+ }
200
+ }
201
+
202
+ /*
203
+ * Description of a Text node.
204
+ */
205
+ class TextDescription extends Description {
206
+ declare text: string
207
+ declare type: 'text'
208
+
209
+ constructor(text: string) {
210
+ super()
211
+ this.text = text
212
+ this.type = 'text'
213
+ }
214
+
215
+ get asTemplate(): string {
216
+ return this.text
217
+ }
218
+
219
+ isCompatible(description: Description): boolean {
220
+ return (
221
+ super.isCompatible(description) &&
222
+ this.text === (description as TextDescription).text
223
+ )
224
+ }
225
+ }
226
+
227
+ Object.assign(Description, {
228
+ ElementDescription,
229
+ ComponentDescription,
230
+ CommentDescription,
231
+ TextDescription,
232
+ })
233
+
234
+ export {
235
+ ElementDescription,
236
+ ComponentDescription,
237
+ CommentDescription,
238
+ TextDescription,
239
+ }
240
+
241
+ export default Description