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,231 @@
1
+ import type { WebComponent } from './nodes.js'
2
+ import type { Update } from './renderer.js'
3
+ import Sandbox from './sandbox.js'
4
+ import { runtime } from './runtime.js'
5
+
6
+ const Permission = {
7
+ LISTEN_FOR_UPDATES: 'listen-for-updates',
8
+ REGISTER_METHOD: 'register-method',
9
+ INJECT_STYLESHEETS: 'inject-stylesheets',
10
+ }
11
+
12
+ /* The API a plugin receives in its register() method. */
13
+ export interface PluginSandbox {
14
+ registerMethod?(name: string): void
15
+ }
16
+
17
+ export interface PluginManifest {
18
+ name: string
19
+ permissions?: string[]
20
+ register?(sandbox: PluginSandbox): void
21
+ install?(root: WebComponent): () => void
22
+ onBeforeUpdate?(update: Update): void
23
+ onAfterUpdate?(update: Update): void
24
+ getStylesheets?(): string[]
25
+ [key: string]: unknown
26
+ }
27
+
28
+ class Plugin {
29
+ declare name: string
30
+ declare permissions: string[]
31
+ declare origin: PluginManifest
32
+ declare register?: () => void
33
+ declare install?: (root: WebComponent) => () => void
34
+ declare onBeforeUpdate?: (update: Update) => void
35
+ declare onAfterUpdate?: (update: Update) => void
36
+ declare getStylesheets?: () => string[]
37
+
38
+ constructor(manifest: PluginManifest) {
39
+ runtime().assert(
40
+ typeof manifest.name === 'string' && manifest.name.length,
41
+ 'Plugin name must be a non-empty string!',
42
+ )
43
+
44
+ Object.assign(this, manifest)
45
+ this.origin = manifest
46
+
47
+ if (this.permissions === undefined) {
48
+ this.permissions = []
49
+ } else {
50
+ runtime().assert(
51
+ Array.isArray(this.permissions),
52
+ 'Plugin permissions must be an array',
53
+ )
54
+ this.permissions = this.permissions.filter(permission =>
55
+ Object.values(Permission).includes(permission),
56
+ )
57
+ }
58
+
59
+ const sandbox = this.createSandbox()
60
+ if (typeof manifest.register === 'function') {
61
+ this.register = () => manifest.register!(sandbox)
62
+ }
63
+ if (typeof manifest.install === 'function') {
64
+ this.install = root => {
65
+ const uninstall = manifest.install!(root)
66
+ runtime().assert(
67
+ typeof uninstall === 'function',
68
+ 'The plugin installation must return the uninstall function!',
69
+ )
70
+ return uninstall
71
+ }
72
+ }
73
+ }
74
+
75
+ isListener(): boolean {
76
+ return this.permissions.includes(Permission.LISTEN_FOR_UPDATES)
77
+ }
78
+
79
+ isStylesheetProvider(): boolean {
80
+ return this.permissions.includes(Permission.INJECT_STYLESHEETS)
81
+ }
82
+
83
+ createSandbox(): PluginSandbox {
84
+ const sandbox: PluginSandbox = {}
85
+ for (const permission of this.permissions) {
86
+ switch (permission) {
87
+ case Permission.REGISTER_METHOD:
88
+ sandbox.registerMethod = name => Sandbox.registerPluginMethod(name)
89
+ }
90
+ }
91
+ return sandbox
92
+ }
93
+ }
94
+
95
+ class Registry {
96
+ declare plugins: Map<string, Plugin>
97
+ declare cache: { listeners: Plugin[] };
98
+ declare [Symbol.iterator]: () => Iterator<Plugin>
99
+
100
+ constructor() {
101
+ this.plugins = new Map()
102
+ this.cache = {
103
+ listeners: [],
104
+ }
105
+ this[Symbol.iterator] = () => this.plugins.values()[Symbol.iterator]()
106
+ }
107
+
108
+ /**
109
+ * Adds the plugin to the registry
110
+ */
111
+ add(plugin: Plugin) {
112
+ runtime().assert(
113
+ !this.isRegistered(plugin.name),
114
+ `Plugin '${plugin.name}' is already registered!`,
115
+ )
116
+ this.plugins.set(plugin.name, plugin)
117
+ this.updateCache()
118
+ }
119
+
120
+ /**
121
+ * Removes the plugin with specified name from the registry.
122
+ */
123
+ remove(name: string) {
124
+ this.plugins.delete(name)
125
+ this.updateCache()
126
+ }
127
+
128
+ /**
129
+ * Checks if plugin with specified name exists in the registry.
130
+ */
131
+ isRegistered(name: string): boolean {
132
+ return this.plugins.has(name)
133
+ }
134
+
135
+ /**
136
+ * Updates the cache.
137
+ */
138
+ updateCache() {
139
+ const plugins = [...this.plugins.values()]
140
+ this.cache.listeners = plugins.filter(plugin => plugin.isListener())
141
+ }
142
+ }
143
+
144
+ class Plugins {
145
+ static Plugin = Plugin
146
+
147
+ declare root: WebComponent | null
148
+ declare registry: Registry
149
+ declare uninstalls: Map<string, () => void>;
150
+ declare [Symbol.iterator]: () => Iterator<Plugin>
151
+
152
+ constructor(root: WebComponent | null) {
153
+ this.root = root
154
+ this.registry = new Registry()
155
+ this.uninstalls = new Map()
156
+ this[Symbol.iterator] = () => this.registry[Symbol.iterator]()
157
+ }
158
+
159
+ /**
160
+ * Creates a Plugin instance from the manifest object and registers it.
161
+ */
162
+ register(plugin: Plugin | PluginManifest) {
163
+ if (!(plugin instanceof Plugin)) {
164
+ plugin = new Plugin(plugin)
165
+ }
166
+ if (plugin.register) {
167
+ plugin.register()
168
+ }
169
+ this.registry.add(plugin)
170
+ }
171
+
172
+ installAll() {
173
+ for (const plugin of this.registry) {
174
+ this.install(plugin)
175
+ }
176
+ }
177
+
178
+ install(plugin: Plugin | PluginManifest) {
179
+ if (this.root && plugin.install) {
180
+ const uninstall = plugin.install(this.root)
181
+ this.uninstalls.set(plugin.name, uninstall)
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Removes the plugin from the registry and invokes it's uninstall method
187
+ * if present.
188
+ */
189
+ uninstall(name: string) {
190
+ const uninstall = this.uninstalls.get(name)
191
+ this.uninstalls.delete(name)
192
+ this.registry.remove(name)
193
+ if (uninstall) {
194
+ uninstall()
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Uninstalls all the plugins from the registry.
200
+ */
201
+ destroy() {
202
+ for (const plugin of this.registry) {
203
+ this.uninstall(plugin.name)
204
+ }
205
+ this.root = null
206
+ }
207
+
208
+ /**
209
+ * Invokes listener methods on registered listener plugins.
210
+ */
211
+ notify(action: 'before-update' | 'after-update', event: Update) {
212
+ switch (action) {
213
+ case 'before-update':
214
+ for (const listener of this.registry.cache.listeners) {
215
+ listener.onBeforeUpdate!(event)
216
+ }
217
+ return
218
+ case 'after-update':
219
+ for (const listener of this.registry.cache.listeners) {
220
+ listener.onAfterUpdate!(event)
221
+ }
222
+ return
223
+ default:
224
+ throw new Error(`Unknown action: ${action as string}`)
225
+ }
226
+ }
227
+ }
228
+
229
+ export type { Plugin }
230
+
231
+ export default Plugins
@@ -0,0 +1,185 @@
1
+ const Name = {
2
+ INSERT: Symbol('insert'),
3
+ MOVE: Symbol('move'),
4
+ REMOVE: Symbol('remove'),
5
+ }
6
+
7
+ interface MoveProps {
8
+ at?: number
9
+ from?: number
10
+ to?: number
11
+ }
12
+
13
+ class Move<T = unknown> {
14
+ static Name = Name
15
+
16
+ declare name: symbol
17
+ declare item: T
18
+ declare at?: number
19
+ declare from?: number
20
+ declare to?: number
21
+ declare make: (items: T[]) => void
22
+
23
+ constructor(
24
+ name: symbol,
25
+ item: T,
26
+ props: MoveProps,
27
+ make: (items: T[]) => void,
28
+ ) {
29
+ this.name = name
30
+ this.item = item
31
+ this.at = props.at
32
+ this.from = props.from
33
+ this.to = props.to
34
+ this.make = make
35
+ }
36
+
37
+ static insert<T>(item: T, at: number): Move<T> {
38
+ return new Move(Name.INSERT, item, { at }, items => {
39
+ items.splice(at, 0, item)
40
+ })
41
+ }
42
+
43
+ static move<T>(item: T, from: number, to: number): Move<T> {
44
+ return new Move(Name.MOVE, item, { from, to }, items => {
45
+ items.splice(from, 1)
46
+ items.splice(to, 0, item)
47
+ })
48
+ }
49
+
50
+ static remove<T>(item: T, at: number): Move<T> {
51
+ return new Move(Name.REMOVE, item, { at }, items => {
52
+ items.splice(at, 1)
53
+ })
54
+ }
55
+ }
56
+
57
+ /* The moves transforming the source into the target, with the result. */
58
+ export type Moves = Move<string>[] & { result?: string[] }
59
+
60
+ interface Item {
61
+ key: string
62
+ index: number
63
+ }
64
+
65
+ const Reconciler = {
66
+ comparator(this: void, a: Item, b: Item): number {
67
+ if (Object.is(a.key, b.key)) {
68
+ return 0
69
+ }
70
+ return a.key > b.key ? 1 : -1
71
+ },
72
+
73
+ calculateMoves(source: string[], target: string[]): Moves {
74
+ const moves: Moves = []
75
+
76
+ const createItem = function (key: string, index: number): Item {
77
+ return { key, index }
78
+ }
79
+
80
+ const before = source.map(createItem).sort(this.comparator)
81
+ const after = target.map(createItem).sort(this.comparator)
82
+
83
+ let removed: Item[] = []
84
+ let inserted: Item[] = []
85
+
86
+ while (before.length || after.length) {
87
+ if (!before.length) {
88
+ inserted = inserted.concat(after)
89
+ break
90
+ }
91
+ if (!after.length) {
92
+ removed = removed.concat(before)
93
+ break
94
+ }
95
+ const result = this.comparator(after[0]!, before[0]!)
96
+ if (result === 0) {
97
+ before.shift()
98
+ after.shift()
99
+ } else if (result === 1) {
100
+ removed.push(before.shift()!)
101
+ } else {
102
+ inserted.push(after.shift()!)
103
+ }
104
+ }
105
+
106
+ const sortByIndex = function (foo: Item, bar: Item) {
107
+ return foo.index - bar.index
108
+ }
109
+
110
+ removed.sort(sortByIndex).reverse()
111
+ inserted.sort(sortByIndex)
112
+
113
+ const result = [...source]
114
+
115
+ for (const item of removed) {
116
+ const move = Move.remove(item.key, item.index)
117
+ move.make(result)
118
+ moves.push(move)
119
+ }
120
+ for (const item of inserted) {
121
+ const move = Move.insert(item.key, item.index)
122
+ move.make(result)
123
+ moves.push(move)
124
+ }
125
+
126
+ if (
127
+ result.length === target.length &&
128
+ result.every((key, index) => key === target[index])
129
+ ) {
130
+ moves.result = result
131
+ return moves
132
+ }
133
+
134
+ const calculateIndexChanges = (
135
+ source: string[],
136
+ target: string[],
137
+ reversed = false,
138
+ ) => {
139
+ const moves: Moves = []
140
+
141
+ const moveItemIfNeeded = (index: number) => {
142
+ const item = target[index]!
143
+ if (source[index] !== item) {
144
+ const from = source.indexOf(item)
145
+ const move = Move.move(item, from, index)
146
+ move.make(source)
147
+ moves.push(move)
148
+ }
149
+ }
150
+
151
+ if (reversed) {
152
+ for (let i = target.length - 1; i >= 0; i--) {
153
+ moveItemIfNeeded(i)
154
+ }
155
+ } else {
156
+ for (let i = 0; i < target.length; i++) {
157
+ moveItemIfNeeded(i)
158
+ }
159
+ }
160
+ moves.result = source
161
+ return moves
162
+ }
163
+
164
+ const defaultMoves = calculateIndexChanges([...result], target)
165
+ if (defaultMoves.length > 1) {
166
+ const alternativeMoves = calculateIndexChanges(
167
+ [...result],
168
+ target,
169
+ /*= reversed */ true,
170
+ )
171
+ if (alternativeMoves.length <= defaultMoves.length) {
172
+ moves.push(...alternativeMoves)
173
+ moves.result = alternativeMoves.result
174
+ return moves
175
+ }
176
+ }
177
+ moves.push(...defaultMoves)
178
+ moves.result = defaultMoves.result
179
+ return moves
180
+ },
181
+
182
+ Move,
183
+ }
184
+
185
+ export default Reconciler
@@ -0,0 +1,75 @@
1
+ import type { NodeDescription, Props } from './description.js'
2
+ import Diff from './diff.js'
3
+ import type { Command, State } from './dispatcher.js'
4
+ import type { Component, WebComponent } from './nodes.js'
5
+ import type Patch from './patch.js'
6
+
7
+ /* Information about a root component update, passed to plugin listeners. */
8
+ export interface Update {
9
+ commands: Command[]
10
+ root: WebComponent
11
+ state: {
12
+ from: State | undefined
13
+ to: State
14
+ }
15
+ patches?: Patch[]
16
+ }
17
+
18
+ const Renderer = {
19
+ /**
20
+ * Calls the component render method and transforms the returned template
21
+ * into the normalised description of the rendered node.
22
+ */
23
+ render(
24
+ component: Component,
25
+ props: Props = {},
26
+ children: unknown[] = [],
27
+ ): NodeDescription | null {
28
+ return component.renderDescription(props, children)
29
+ },
30
+
31
+ /**
32
+ * Updates the Web component and patches the DOM tree
33
+ * to match the new component state.
34
+ */
35
+ update(
36
+ root: WebComponent,
37
+ from: State | undefined,
38
+ to: State,
39
+ commands: Command[],
40
+ ) {
41
+ const update: Update = {
42
+ commands,
43
+ root,
44
+ state: {
45
+ from,
46
+ to,
47
+ },
48
+ }
49
+
50
+ this.onBeforeUpdate(update, root)
51
+
52
+ const diff = new Diff(root, from, to)
53
+ // set when the patches are calculated, kept when rendering them fails
54
+ root.state = to
55
+ update.patches = diff.apply()
56
+
57
+ this.onAfterUpdate(update, root)
58
+ },
59
+
60
+ /**
61
+ * Notifies the observers about upcoming update.
62
+ */
63
+ onBeforeUpdate(update: Update, root: WebComponent) {
64
+ root.plugins!.notify('before-update', update)
65
+ },
66
+
67
+ /**
68
+ * Notifies the observers about completed update.
69
+ */
70
+ onAfterUpdate(update: Update, root: WebComponent) {
71
+ root.plugins!.notify('after-update', update)
72
+ },
73
+ }
74
+
75
+ export default Renderer
@@ -0,0 +1,36 @@
1
+ import type { Command, State } from './dispatcher.js'
2
+ import type { ComponentClass, WebComponent } from './nodes.js'
3
+ import type Plugins from './plugins.js'
4
+ import type { ItemType } from './template.js'
5
+ import { invariant } from './utils.js'
6
+
7
+ /*
8
+ * The services of the Toolkit instance the core modules rely on,
9
+ * provided by the instance, so that the modules do not import it.
10
+ */
11
+ export interface Runtime {
12
+ readonly plugins: Plugins
13
+ assert(condition: unknown, message?: string): void
14
+ isDebug(): boolean
15
+ warn(...messages: unknown[]): void
16
+ track(root: WebComponent): void
17
+ resolveComponentClass(component: unknown, type: ItemType): ComponentClass
18
+ /* Renders the state of the root, see Renderer.update(). */
19
+ update(
20
+ root: WebComponent,
21
+ from: State | undefined,
22
+ to: State,
23
+ commands: Command[],
24
+ ): void
25
+ }
26
+
27
+ let provided: Runtime | null = null
28
+
29
+ export const provideRuntime = (runtime: Runtime) => {
30
+ provided = runtime
31
+ }
32
+
33
+ export const runtime = (): Runtime => {
34
+ invariant(provided, 'No Toolkit runtime provided!')
35
+ return provided
36
+ }
@@ -0,0 +1,128 @@
1
+ import type { Props } from './description.js'
2
+ import type { Component, WebComponent } from './nodes.js'
3
+ import type { AnyFunction } from './utils.js'
4
+
5
+ /*
6
+ * The component proxy passed as `this` to render and lifecycle methods,
7
+ * exposing props and children and binding the component methods.
8
+ */
9
+ export type ComponentSandbox = Omit<Component, 'children'> & {
10
+ props: Props
11
+ children: unknown[]
12
+ }
13
+
14
+ type BoundListener = AnyFunction & { source?: unknown; component?: Component }
15
+
16
+ /* Reads a property as `target[property]` does. */
17
+ const get = (target: object, property: PropertyKey): unknown =>
18
+ Reflect.get(target, property)
19
+
20
+ const isFunction = (target: object, property: PropertyKey) =>
21
+ typeof get(target, property) === 'function'
22
+
23
+ const delegated: PropertyKey[] = [
24
+ 'commands',
25
+ 'constructor',
26
+ 'container',
27
+ 'elementName',
28
+ ]
29
+ const methods: PropertyKey[] = ['connectTo']
30
+ /* Registered again with each root the plugins are inherited by. */
31
+ const pluginMethods = new Set<PropertyKey>()
32
+
33
+ const createBoundListener = (
34
+ listener: AnyFunction,
35
+ component: Component,
36
+ context: unknown,
37
+ ) => {
38
+ const boundListener: BoundListener = listener.bind(context)
39
+ boundListener.source = listener
40
+ boundListener.component = component
41
+ return boundListener
42
+ }
43
+
44
+ class Sandbox {
45
+ static registerPluginMethod(name: string) {
46
+ pluginMethods.add(name)
47
+ }
48
+
49
+ static clearPluginMethods() {
50
+ pluginMethods.clear()
51
+ }
52
+
53
+ /**
54
+ * Creates the sandbox of a component, hiding the given properties
55
+ * of the Component class from the component methods.
56
+ */
57
+ static create(
58
+ component: Component,
59
+ hidden: readonly PropertyKey[],
60
+ ): ComponentSandbox {
61
+ const state: { props?: Props; children?: unknown[] } = {}
62
+ const autobound: Record<PropertyKey, BoundListener> = {}
63
+ return new Proxy(component, {
64
+ get: (target, property, receiver) => {
65
+ if (property === 'props') {
66
+ return state.props || (target as WebComponent).state || {}
67
+ }
68
+ if (property === 'children') {
69
+ return state.children || []
70
+ }
71
+ if (property === 'host') {
72
+ return target.isRoot() ? target.shadow!.host : null
73
+ }
74
+ if (property === 'ref') {
75
+ if (target.isRoot()) {
76
+ // returns rendered node instead of custom element for usage of
77
+ // this.ref.querySelector
78
+ return target.content!.ref
79
+ }
80
+ return target.ref
81
+ }
82
+ if (property === '$component') {
83
+ return component
84
+ }
85
+ if (delegated.includes(property)) {
86
+ return get(target, property)
87
+ }
88
+ if (methods.includes(property) && isFunction(target, property)) {
89
+ return createBoundListener(
90
+ get(target, property) as AnyFunction,
91
+ target,
92
+ target,
93
+ )
94
+ }
95
+ if (pluginMethods.has(property)) {
96
+ return get(target.rootNode, property)
97
+ }
98
+ if (hidden.includes(property)) {
99
+ return undefined
100
+ }
101
+ if (isFunction(autobound, property)) {
102
+ return autobound[property]
103
+ }
104
+ if (isFunction(target, property)) {
105
+ return (autobound[property] = createBoundListener(
106
+ get(target, property) as AnyFunction,
107
+ target,
108
+ receiver,
109
+ ))
110
+ }
111
+ return get(target, property)
112
+ },
113
+ set: (target, property, value) => {
114
+ if (property === 'props') {
115
+ state.props = value as Props
116
+ return true
117
+ }
118
+ if (property === 'children') {
119
+ state.children = (value as unknown[]) || []
120
+ return true
121
+ }
122
+ return false
123
+ },
124
+ }) as ComponentSandbox
125
+ }
126
+ }
127
+
128
+ export default Sandbox
@@ -0,0 +1,39 @@
1
+ import { runtime } from './runtime.js'
2
+
3
+ class Service {
4
+ declare static events: string[]
5
+
6
+ static validate(listeners: Record<string, unknown>): string[] {
7
+ if (runtime().isDebug()) {
8
+ const keys = Object.keys(listeners)
9
+ runtime().assert(
10
+ this.events instanceof Array,
11
+ `Service "${this.name}" does not provide information about valid events, implement "static events = ['foo', 'bar']"`,
12
+ )
13
+ runtime().assert(
14
+ this.events.length > 0,
15
+ `Service "${this.name}" returned an empty list of valid events, the "static events" list must contain at least one event name`,
16
+ )
17
+ const unsupportedKeys = keys.filter(key => !this.events.includes(key))
18
+ for (const unsupportedKey of unsupportedKeys) {
19
+ runtime().warn(
20
+ `Unsupported listener specified "${unsupportedKey}" when connecting to ${this.name}`,
21
+ )
22
+ }
23
+ const supportedKeys = this.events.filter(event => keys.includes(event))
24
+ runtime().assert(
25
+ supportedKeys.length > 0,
26
+ `No valid listener specified when connecting to ${this.name}, use one of [${this.events.join(', ')}]`,
27
+ )
28
+ for (const supportedKey of supportedKeys) {
29
+ runtime().assert(
30
+ listeners[supportedKey] instanceof Function,
31
+ `Specified listener "${supportedKey}" for ${this.name} is not a function`,
32
+ )
33
+ }
34
+ }
35
+ return this.events.filter(event => listeners[event] instanceof Function)
36
+ }
37
+ }
38
+
39
+ export default Service