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,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
@@ -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