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,221 @@
|
|
|
1
|
+
import { produce } from './draft.js'
|
|
2
|
+
import type { WebComponent } from './nodes.js'
|
|
3
|
+
import { runtime } from './runtime.js'
|
|
4
|
+
import type { AnyFunction } from './utils.js'
|
|
5
|
+
|
|
6
|
+
/* The state of a root component. */
|
|
7
|
+
export type State = Record<string, unknown>
|
|
8
|
+
|
|
9
|
+
/* A state transformation, calculating the next state. */
|
|
10
|
+
export type StateUpdate<S = State> = (state: S) => S
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* A command changing a draft of the state, available as `this`,
|
|
14
|
+
* which becomes the next immutable state.
|
|
15
|
+
*/
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- commands take any arguments
|
|
17
|
+
export type CommandMethod<S> = (this: S, ...args: any[]) => void
|
|
18
|
+
|
|
19
|
+
/* A map of command names to their methods. */
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- commands of any state by default
|
|
21
|
+
export type CommandsAPI<S = any> = Record<string, CommandMethod<S>>
|
|
22
|
+
|
|
23
|
+
/* The core commands available on every component. */
|
|
24
|
+
export type Commands<S = State> = {
|
|
25
|
+
update(overrides: Partial<S>): Promise<boolean>
|
|
26
|
+
replace(state: S): Promise<boolean>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* The commands of an API, called with the arguments of the API methods.
|
|
31
|
+
* They resolve with true once rendered, or with false when ignored.
|
|
32
|
+
*/
|
|
33
|
+
export type BoundCommands<C extends CommandsAPI> = {
|
|
34
|
+
[K in keyof C]: (...args: Parameters<C[K]>) => Promise<boolean>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const Mode = {
|
|
38
|
+
EXECUTE: Symbol('execute-commands'),
|
|
39
|
+
IGNORE: Symbol('ignore-commands'),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
* The limit of updates in a row, the one with the commands issued together
|
|
44
|
+
* followed by the ones with the commands issued in lifecycle methods.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_FLUSH_CYCLES = 4
|
|
47
|
+
|
|
48
|
+
/* The core commands, also setting the initial state, with no draft yet. */
|
|
49
|
+
const coreAPI: Record<keyof Commands, (arg: State) => StateUpdate> = {
|
|
50
|
+
update(overrides) {
|
|
51
|
+
return state => ({
|
|
52
|
+
...state,
|
|
53
|
+
...overrides,
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
replace(state) {
|
|
57
|
+
return () => state
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class Command {
|
|
62
|
+
declare name: string
|
|
63
|
+
declare args: unknown[]
|
|
64
|
+
declare update: StateUpdate
|
|
65
|
+
declare resolve?: (executed: boolean) => void
|
|
66
|
+
declare reject?: (error: unknown) => void
|
|
67
|
+
|
|
68
|
+
constructor(name: string, args: unknown[], update: StateUpdate) {
|
|
69
|
+
this.name = name
|
|
70
|
+
this.args = args
|
|
71
|
+
this.update = update
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
invoke(state: State | undefined): State {
|
|
75
|
+
return this.update(state as State)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const createCommandsAPI = (...apis: CommandsAPI[]) => {
|
|
80
|
+
const commandsAPI: CommandsAPI = {}
|
|
81
|
+
for (const api of apis) {
|
|
82
|
+
const overridden = Object.keys(api).find(
|
|
83
|
+
key => Object.hasOwn(coreAPI, key) || Object.hasOwn(commandsAPI, key),
|
|
84
|
+
)
|
|
85
|
+
if (overridden) {
|
|
86
|
+
throw new Error(`The "${overridden}" command is already defined!`)
|
|
87
|
+
}
|
|
88
|
+
Object.assign(commandsAPI, api)
|
|
89
|
+
}
|
|
90
|
+
return commandsAPI
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type { Command }
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* Queues the issued commands and executes the ones issued together
|
|
97
|
+
* in a single update, once the current task has completed.
|
|
98
|
+
*/
|
|
99
|
+
class Dispatcher {
|
|
100
|
+
declare mode: symbol
|
|
101
|
+
declare queue: Command[]
|
|
102
|
+
declare isFlushing: boolean
|
|
103
|
+
declare root: WebComponent
|
|
104
|
+
declare api: CommandsAPI
|
|
105
|
+
declare commands: Commands & Record<string, AnyFunction>
|
|
106
|
+
declare names: string[]
|
|
107
|
+
|
|
108
|
+
ignoreIncoming() {
|
|
109
|
+
this.mode = Mode.IGNORE
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
constructor(root: WebComponent) {
|
|
113
|
+
this.mode = Mode.EXECUTE
|
|
114
|
+
this.queue = []
|
|
115
|
+
this.isFlushing = false
|
|
116
|
+
this.root = root
|
|
117
|
+
this.commands = {} as Commands & Record<string, AnyFunction>
|
|
118
|
+
|
|
119
|
+
const { commands = [] } = root.constructor as typeof WebComponent
|
|
120
|
+
this.api = createCommandsAPI(
|
|
121
|
+
...(Array.isArray(commands) ? commands : [commands]),
|
|
122
|
+
)
|
|
123
|
+
this.names = [...Object.keys(coreAPI), ...Object.keys(this.api)]
|
|
124
|
+
|
|
125
|
+
for (const name of this.names) {
|
|
126
|
+
this.commands[name] = (...args: unknown[]) => this.issue(name, args)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
createCommand(name: string, args: unknown[]) {
|
|
131
|
+
const method = this.api[name]
|
|
132
|
+
const update: StateUpdate = method
|
|
133
|
+
? state =>
|
|
134
|
+
produce(state, draft => {
|
|
135
|
+
const result: unknown = method.apply(draft, args)
|
|
136
|
+
// the draft is revoked before the rest of an async command runs
|
|
137
|
+
if (result instanceof Promise) {
|
|
138
|
+
result.catch(() => {})
|
|
139
|
+
throw new Error(`The "${name}" command must be synchronous!`)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
: coreAPI[name as keyof Commands](...(args as [State]))
|
|
143
|
+
return new Command(name, args, update)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
issue(name: string, args: unknown[]): Promise<boolean> {
|
|
147
|
+
if (this.mode === Mode.IGNORE) {
|
|
148
|
+
return Promise.resolve(false)
|
|
149
|
+
}
|
|
150
|
+
const command = this.createCommand(name, args)
|
|
151
|
+
const done = new Promise<boolean>((resolve, reject) => {
|
|
152
|
+
command.resolve = resolve
|
|
153
|
+
command.reject = reject
|
|
154
|
+
})
|
|
155
|
+
this.queue.push(command)
|
|
156
|
+
// the commands issued while flushing are executed in the next cycle
|
|
157
|
+
if (this.queue.length === 1 && !this.isFlushing) {
|
|
158
|
+
queueMicrotask(() => this.flush())
|
|
159
|
+
}
|
|
160
|
+
return done
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Sets the state right away, when the root is rendered or receives
|
|
165
|
+
* new props from its parent.
|
|
166
|
+
*/
|
|
167
|
+
setState(state: State) {
|
|
168
|
+
if (this.mode === Mode.IGNORE) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
this.execute([this.createCommand('replace', [state])])
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Updates the root with the state calculated by the commands in turn.
|
|
176
|
+
*/
|
|
177
|
+
execute(commands: Command[]) {
|
|
178
|
+
const prevState = this.root.state as State | undefined
|
|
179
|
+
const nextState = commands.reduce<State | undefined>(
|
|
180
|
+
(state, command) => command.invoke(state),
|
|
181
|
+
prevState,
|
|
182
|
+
)!
|
|
183
|
+
runtime().update(this.root, prevState, nextState, commands)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Executes the queued commands, followed by the ones issued in lifecycle
|
|
188
|
+
* methods in turn, until there are none left. When an update fails, its
|
|
189
|
+
* commands reject, and the ones issued in its lifecycle methods are still
|
|
190
|
+
* executed, as the update is completed.
|
|
191
|
+
*/
|
|
192
|
+
flush() {
|
|
193
|
+
this.isFlushing = true
|
|
194
|
+
try {
|
|
195
|
+
for (let cycle = 1; this.queue.length; cycle++) {
|
|
196
|
+
const commands = this.queue.splice(0)
|
|
197
|
+
// the root can be destroyed before the queued commands are executed
|
|
198
|
+
if (this.mode === Mode.IGNORE) {
|
|
199
|
+
commands.forEach(command => command.resolve!(false))
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
if (cycle > MAX_FLUSH_CYCLES) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
'Too many cycles updating state in lifecycle methods!',
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
this.execute(commands)
|
|
209
|
+
} catch (error) {
|
|
210
|
+
commands.forEach(command => command.reject!(error))
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
commands.forEach(command => command.resolve!(true))
|
|
214
|
+
}
|
|
215
|
+
} finally {
|
|
216
|
+
this.isFlushing = false
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export default Dispatcher
|
package/src/core/dom.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ElementDescription, Listener } from './description.js'
|
|
2
|
+
import { getAttributeName, getEventName } from './utils.js'
|
|
3
|
+
|
|
4
|
+
/* Style and properties are set by name, as by the original assignments. */
|
|
5
|
+
type StyleMap = Record<string, string | null>
|
|
6
|
+
type PropertyMap = Record<string, unknown>
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Writes to the rendered elements, shared by the initial rendering and
|
|
10
|
+
* the patches, so that both follow the same rules. Custom attributes and
|
|
11
|
+
* listeners use the names as given, others are mapped to the DOM names.
|
|
12
|
+
*/
|
|
13
|
+
const DOM = {
|
|
14
|
+
setClassName(element: HTMLElement, className: string) {
|
|
15
|
+
element.className = className
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
setStyleProperty(element: HTMLElement, name: string, value: string) {
|
|
19
|
+
if (name.startsWith('--')) {
|
|
20
|
+
element.style.setProperty(name, ` ${value}`)
|
|
21
|
+
} else {
|
|
22
|
+
;(element.style as unknown as StyleMap)[name] = value
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
removeStyleProperty(element: HTMLElement, name: string) {
|
|
27
|
+
if (name.startsWith('--')) {
|
|
28
|
+
element.style.removeProperty(name)
|
|
29
|
+
} else {
|
|
30
|
+
;(element.style as unknown as StyleMap)[name] = null
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
setAttribute(
|
|
35
|
+
element: HTMLElement,
|
|
36
|
+
name: string,
|
|
37
|
+
value: string,
|
|
38
|
+
isCustom: boolean,
|
|
39
|
+
) {
|
|
40
|
+
const attr = isCustom ? name : getAttributeName(name)
|
|
41
|
+
element.setAttribute(attr, value)
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
removeAttribute(element: HTMLElement, name: string, isCustom: boolean) {
|
|
45
|
+
const attr = isCustom ? name : getAttributeName(name)
|
|
46
|
+
element.removeAttribute(attr)
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
setDataAttribute(element: HTMLElement, name: string, value: string) {
|
|
50
|
+
element.dataset[name] = value
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
removeDataAttribute(element: HTMLElement, name: string) {
|
|
54
|
+
delete element.dataset[name]
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
addListener(
|
|
58
|
+
element: HTMLElement,
|
|
59
|
+
name: string,
|
|
60
|
+
listener: Listener,
|
|
61
|
+
isCustom: boolean,
|
|
62
|
+
) {
|
|
63
|
+
const event = isCustom ? name : getEventName(name)
|
|
64
|
+
element.addEventListener(event, listener)
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
removeListener(
|
|
68
|
+
element: HTMLElement,
|
|
69
|
+
name: string,
|
|
70
|
+
listener: Listener,
|
|
71
|
+
isCustom: boolean,
|
|
72
|
+
) {
|
|
73
|
+
const event = isCustom ? name : getEventName(name)
|
|
74
|
+
element.removeEventListener(event, listener)
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
setProperty(element: HTMLElement, key: string, value: unknown) {
|
|
78
|
+
;(element as unknown as PropertyMap)[key] = value
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
deleteProperty(element: HTMLElement, key: string) {
|
|
82
|
+
delete (element as unknown as PropertyMap)[key]
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Creates a new DOM Element based on the specified description.
|
|
87
|
+
*/
|
|
88
|
+
createElement(description: ElementDescription): HTMLElement {
|
|
89
|
+
const element = document.createElement(description.name)
|
|
90
|
+
if (description.class) {
|
|
91
|
+
this.setClassName(element, description.class)
|
|
92
|
+
}
|
|
93
|
+
for (const [name, value] of Object.entries(description.style ?? {})) {
|
|
94
|
+
this.setStyleProperty(element, name, value)
|
|
95
|
+
}
|
|
96
|
+
for (const [name, listener] of Object.entries(
|
|
97
|
+
description.listeners ?? {},
|
|
98
|
+
)) {
|
|
99
|
+
this.addListener(element, name, listener, false)
|
|
100
|
+
}
|
|
101
|
+
for (const [name, value] of Object.entries(description.attrs ?? {})) {
|
|
102
|
+
this.setAttribute(element, name, value, false)
|
|
103
|
+
}
|
|
104
|
+
for (const [name, value] of Object.entries(description.dataset ?? {})) {
|
|
105
|
+
this.setDataAttribute(element, name, value)
|
|
106
|
+
}
|
|
107
|
+
for (const [key, value] of Object.entries(description.properties ?? {})) {
|
|
108
|
+
this.setProperty(element, key, value)
|
|
109
|
+
}
|
|
110
|
+
for (const [name, value] of Object.entries(
|
|
111
|
+
description.custom?.attrs ?? {},
|
|
112
|
+
)) {
|
|
113
|
+
this.setAttribute(element, name, value, true)
|
|
114
|
+
}
|
|
115
|
+
for (const [name, listener] of Object.entries(
|
|
116
|
+
description.custom?.listeners ?? {},
|
|
117
|
+
)) {
|
|
118
|
+
this.addListener(element, name, listener, true)
|
|
119
|
+
}
|
|
120
|
+
return element
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default DOM
|