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.
- package/AUTHORS +3 -0
- package/LICENSE +1 -1
- package/NOTICE +7 -0
- package/README.md +162 -6
- package/dist/release/index.d.ts +650 -0
- package/dist/release/index.d.ts.map +1 -0
- package/dist/release/index.js +2831 -0
- package/dist/release/index.js.map +1 -0
- package/dist/release/toolkit-0.70.0.js +2834 -0
- package/dist/release/toolkit-0.70.0.js.map +1 -0
- package/package.json +71 -18
- package/src/core/bragi.ts +122 -0
- package/src/core/browser.ts +538 -0
- package/src/core/child-nodes.ts +77 -0
- package/src/core/comment.ts +24 -0
- package/src/core/component.ts +190 -0
- package/src/core/created-roots.ts +38 -0
- package/src/core/custom-element.ts +191 -0
- package/src/core/description.ts +241 -0
- package/src/core/diff.ts +590 -0
- package/src/core/dispatcher.ts +221 -0
- package/src/core/dom.ts +124 -0
- package/src/core/draft.ts +609 -0
- package/src/core/lifecycle.ts +301 -0
- package/src/core/nodes.ts +13 -0
- package/src/core/patch.ts +412 -0
- package/src/core/plugins.ts +231 -0
- package/src/core/reconciler.ts +185 -0
- package/src/core/renderer.ts +75 -0
- package/src/core/runtime.ts +36 -0
- package/src/core/sandbox.ts +128 -0
- package/src/core/service.ts +39 -0
- package/src/core/template.ts +472 -0
- package/src/core/text.ts +24 -0
- package/src/core/toolkit.ts +220 -0
- package/src/core/utils.ts +148 -0
- package/src/core/virtual-dom.ts +118 -0
- package/src/core/virtual-element.ts +72 -0
- package/src/core/virtual-node.ts +87 -0
- package/src/core/web-component.ts +285 -0
- package/src/globals.d.ts +10 -0
- package/src/index.ts +62 -0
- package/src/plugins/logger.ts +38 -0
- package/src/release.ts +3 -0
- 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
|