turbo-web 4.2.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/bin/turbo-setup.js +52 -0
- package/dist/turbo.js +1196 -0
- package/package.json +33 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// 1. gets the project name from the terminal command
|
|
7
|
+
const projectName = process.argv[2];
|
|
8
|
+
|
|
9
|
+
if (!projectName) {
|
|
10
|
+
console.error('Error: Please specify a project name.');
|
|
11
|
+
console.error('Usage: npx turbo-charge <project-directory>');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 2. defines the target directory path
|
|
16
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
17
|
+
|
|
18
|
+
// 3. creates the directories
|
|
19
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
20
|
+
fs.mkdirSync(path.join(projectPath, 'src'), { recursive: true });
|
|
21
|
+
|
|
22
|
+
// 4. defines the boilerplate file contents
|
|
23
|
+
const storeContent = `import { Store } from 'your-framework';
|
|
24
|
+
|
|
25
|
+
export const appStore = new Store({
|
|
26
|
+
state: {},
|
|
27
|
+
mutations: {},
|
|
28
|
+
actions: {}
|
|
29
|
+
});
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const routerContent = `export function initRouter() {
|
|
33
|
+
window.addEventListener('hashchange', () => {
|
|
34
|
+
const path = window.location.hash.slice(1).toLowerCase() || '/';
|
|
35
|
+
console.log('Navigated to:', path);
|
|
36
|
+
// Add route matching and component mounting logic here
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const mainContent = `import { appStore } from './store.js';
|
|
42
|
+
import { initRouter } from './router.js';
|
|
43
|
+
|
|
44
|
+
initRouter();
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
// 5. Write the files to the user's new project directory
|
|
48
|
+
fs.writeFileSync(path.join(projectPath, 'src', 'store.js'), storeContent);
|
|
49
|
+
fs.writeFileSync(path.join(projectPath, 'src', 'router.js'), routerContent);
|
|
50
|
+
fs.writeFileSync(path.join(projectPath, 'src', 'main.js'), mainContent);
|
|
51
|
+
|
|
52
|
+
console.log(`BUENO! You project:${projectName} is TURBO charged for web development!`);
|
package/dist/turbo.js
ADDED
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
const ARRAY_DIFF_OP = {
|
|
2
|
+
ADD: 'add',
|
|
3
|
+
REMOVE: 'remove',
|
|
4
|
+
MOVE: 'move',
|
|
5
|
+
NOOP: 'noop',
|
|
6
|
+
};
|
|
7
|
+
function withoutNulls(arr) {
|
|
8
|
+
return arr.filter((item) => item != null)
|
|
9
|
+
}
|
|
10
|
+
function arraysDiff(oldArray, newArray) {
|
|
11
|
+
return {
|
|
12
|
+
added: newArray.filter(
|
|
13
|
+
(newItem) => !oldArray.includes(newItem)
|
|
14
|
+
),
|
|
15
|
+
removed: oldArray.filter(
|
|
16
|
+
(oldItem) => !newArray.includes(oldItem)
|
|
17
|
+
),
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
class ArrayWithOriginalIndices {
|
|
21
|
+
#array = []
|
|
22
|
+
#originalIndices = []
|
|
23
|
+
#equalsFn
|
|
24
|
+
constructor(array, equalsFn) {
|
|
25
|
+
this.#array = [...array];
|
|
26
|
+
this.#originalIndices = array.map((_, i) => i);
|
|
27
|
+
this.#equalsFn = equalsFn;
|
|
28
|
+
}
|
|
29
|
+
get length() {
|
|
30
|
+
return this.#array.length
|
|
31
|
+
}
|
|
32
|
+
isRemoval(index, newArray) {
|
|
33
|
+
if (index >= this.length) {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
const item = this.#array[index];
|
|
37
|
+
const indexInNewArray = newArray.findIndex((newItem) =>
|
|
38
|
+
this.#equalsFn(item, newItem)
|
|
39
|
+
);
|
|
40
|
+
return indexInNewArray === -1
|
|
41
|
+
}
|
|
42
|
+
removeItem(index) {
|
|
43
|
+
const operation = {
|
|
44
|
+
op: ARRAY_DIFF_OP.REMOVE,
|
|
45
|
+
index,
|
|
46
|
+
item: this.#array[index],
|
|
47
|
+
};
|
|
48
|
+
this.#array.splice(index, 1);
|
|
49
|
+
this.#originalIndices.splice(index, 1);
|
|
50
|
+
return operation
|
|
51
|
+
}
|
|
52
|
+
isNoop(index, newArray) {
|
|
53
|
+
if (index >= this.length) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
const item = this.#array[index];
|
|
57
|
+
const newItem = newArray[index];
|
|
58
|
+
return this.#equalsFn(item, newItem)
|
|
59
|
+
}
|
|
60
|
+
originalIndexAt(index) {
|
|
61
|
+
return this.#originalIndices[index]
|
|
62
|
+
}
|
|
63
|
+
noopItem(index) {
|
|
64
|
+
return {
|
|
65
|
+
op: ARRAY_DIFF_OP.NOOP,
|
|
66
|
+
originalIndex: this.originalIndexAt(index),
|
|
67
|
+
index,
|
|
68
|
+
item: this.#array[index],
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
isAddition(item, fromIdx) {
|
|
72
|
+
return this.findIndexFrom(item, fromIdx) === -1
|
|
73
|
+
}
|
|
74
|
+
findIndexFrom(item, fromIndex) {
|
|
75
|
+
for (let i = fromIndex; i < this.length; i++) {
|
|
76
|
+
if (this.#equalsFn(item, this.#array[i])) {
|
|
77
|
+
return i
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return -1
|
|
81
|
+
}
|
|
82
|
+
addItem(item, index) {
|
|
83
|
+
const operation = {
|
|
84
|
+
op: ARRAY_DIFF_OP.ADD,
|
|
85
|
+
index,
|
|
86
|
+
item,
|
|
87
|
+
};
|
|
88
|
+
this.#array.splice(index, 0, item);
|
|
89
|
+
this.#originalIndices.splice(index, 0, -1);
|
|
90
|
+
return operation
|
|
91
|
+
}
|
|
92
|
+
moveItem(item, toIndex) {
|
|
93
|
+
const fromIndex = this.findIndexFrom(item, toIndex);
|
|
94
|
+
const operation = {
|
|
95
|
+
op: ARRAY_DIFF_OP.MOVE,
|
|
96
|
+
originalIndex: this.originalIndexAt(fromIndex),
|
|
97
|
+
from: fromIndex,
|
|
98
|
+
index: toIndex,
|
|
99
|
+
item: this.#array[fromIndex],
|
|
100
|
+
};
|
|
101
|
+
const [_item] = this.#array.splice(fromIndex, 1);
|
|
102
|
+
this.#array.splice(toIndex, 0, _item);
|
|
103
|
+
const [originalIndex] =
|
|
104
|
+
this.#originalIndices.splice(fromIndex, 1);
|
|
105
|
+
this.#originalIndices.splice(toIndex, 0, originalIndex);
|
|
106
|
+
return operation
|
|
107
|
+
}
|
|
108
|
+
removeItemsAfter(index) {
|
|
109
|
+
const operations = [];
|
|
110
|
+
while (this.length > index) {
|
|
111
|
+
operations.push(this.removeItem(index));
|
|
112
|
+
}
|
|
113
|
+
return operations
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function arraysDiffSequence(
|
|
117
|
+
oldArray,
|
|
118
|
+
newArray,
|
|
119
|
+
equalsFn = (a, b) => a === b
|
|
120
|
+
) {
|
|
121
|
+
const sequence = [];
|
|
122
|
+
const array = new ArrayWithOriginalIndices(oldArray, equalsFn);
|
|
123
|
+
for (let index = 0; index < newArray.length; index++) {
|
|
124
|
+
if (array.isRemoval(index, newArray)) {
|
|
125
|
+
sequence.push(array.removeItem(index));
|
|
126
|
+
index--;
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if (array.isNoop(index, newArray)) {
|
|
130
|
+
sequence.push(array.noopItem(index));
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
const item = newArray[index];
|
|
134
|
+
if (array.isAddition(item, index)) {
|
|
135
|
+
sequence.push(array.addItem(item, index));
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
sequence.push(array.moveItem(item, index));
|
|
139
|
+
}
|
|
140
|
+
sequence.push(...array.removeItemsAfter(newArray.length));
|
|
141
|
+
return sequence
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let hSlotCalled = false;
|
|
145
|
+
const DOM_TYPES = {
|
|
146
|
+
TEXT: 'text',
|
|
147
|
+
ELEMENT: 'element',
|
|
148
|
+
FRAGMENT: 'fragment',
|
|
149
|
+
COMPONENT: 'component',
|
|
150
|
+
SLOT: 'slot',
|
|
151
|
+
};
|
|
152
|
+
function h(tag, props = {}, children = []) {
|
|
153
|
+
const type = typeof tag === 'string' ? DOM_TYPES.ELEMENT : DOM_TYPES.COMPONENT;
|
|
154
|
+
let processedChildren;
|
|
155
|
+
if (Array.isArray(children)) {
|
|
156
|
+
processedChildren = mapTextNodes(withoutNulls(children));
|
|
157
|
+
} else if (typeof children === 'object' && children !== null) {
|
|
158
|
+
processedChildren = {};
|
|
159
|
+
for (const [slotName, slotContent] of Object.entries(children)) {
|
|
160
|
+
processedChildren[slotName] = mapTextNodes(withoutNulls(slotContent));
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
processedChildren = [];
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
tag,
|
|
167
|
+
props,
|
|
168
|
+
type,
|
|
169
|
+
children: processedChildren,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function mapTextNodes(children) {
|
|
173
|
+
return children.map((child) =>
|
|
174
|
+
typeof child === 'string' ? hString(child) : child
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
function hString(str) {
|
|
178
|
+
return { type: DOM_TYPES.TEXT, value: str }
|
|
179
|
+
}
|
|
180
|
+
function hFragment(vNodes) {
|
|
181
|
+
return {
|
|
182
|
+
type: DOM_TYPES.FRAGMENT,
|
|
183
|
+
children: mapTextNodes(withoutNulls(vNodes)),
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function didCreateSlot() {
|
|
187
|
+
return hSlotCalled
|
|
188
|
+
}
|
|
189
|
+
function resetDidCreateSlot() {
|
|
190
|
+
hSlotCalled = false;
|
|
191
|
+
}
|
|
192
|
+
function hSlot(name = 'default', children = []) {
|
|
193
|
+
hSlotCalled = true;
|
|
194
|
+
return { type: DOM_TYPES.SLOT, name: name, children: children }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function addEventListener(eventName, handler, el, hostComponent = null) {
|
|
198
|
+
function boundHandler() {
|
|
199
|
+
hostComponent
|
|
200
|
+
? handler.apply(hostComponent, arguments)
|
|
201
|
+
: handler(...arguments);
|
|
202
|
+
}
|
|
203
|
+
el.addEventListener(eventName, boundHandler);
|
|
204
|
+
return boundHandler
|
|
205
|
+
}
|
|
206
|
+
function addEventListeners(listeners = {}, el, hostComponent = null ) {
|
|
207
|
+
const addedListeners = {};
|
|
208
|
+
Object.entries(listeners).forEach(([eventName, handler]) => {
|
|
209
|
+
const listener = addEventListener(eventName, handler, el, hostComponent);
|
|
210
|
+
addedListeners[eventName] = listener;
|
|
211
|
+
});
|
|
212
|
+
return addedListeners
|
|
213
|
+
}
|
|
214
|
+
function removeEventListeners(listeners = {}, el) {
|
|
215
|
+
Object.entries(listeners).forEach(([eventName, handler]) => {
|
|
216
|
+
el.removeEventListener(eventName, handler);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function setAttributes(el, attrs) {
|
|
221
|
+
const { class: className, style, ...otherAttrs } = attrs;
|
|
222
|
+
if (className) {
|
|
223
|
+
setClass(el, className);
|
|
224
|
+
}
|
|
225
|
+
if (style) {
|
|
226
|
+
Object.entries(style).forEach(([prop, value]) => {
|
|
227
|
+
setStyle(el, prop, value);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
for (const [name, value] of Object.entries(otherAttrs)) {
|
|
231
|
+
setAttribute(el, name, value);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function setClass(el, className) {
|
|
235
|
+
el.className = '';
|
|
236
|
+
if (typeof className === 'string') {
|
|
237
|
+
el.className = className;
|
|
238
|
+
}
|
|
239
|
+
if (Array.isArray(className)) {
|
|
240
|
+
el.classList.add(...className);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function setStyle(el, name, value) {
|
|
244
|
+
el.style[name] = value;
|
|
245
|
+
}
|
|
246
|
+
function removeStyle(el, name) {
|
|
247
|
+
el.style[name];
|
|
248
|
+
}
|
|
249
|
+
function setAttribute(el, name, value) {
|
|
250
|
+
if (value == null) {
|
|
251
|
+
removeAttribute(el, name);
|
|
252
|
+
} else if (name.startsWith('data-')) {
|
|
253
|
+
el.setAttribute(name, value);
|
|
254
|
+
} else {
|
|
255
|
+
el[name] = value;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function removeAttribute(el, name) {
|
|
259
|
+
el[name] = null;
|
|
260
|
+
el.removeAttribute(name);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function extractPropsAndEvents(vdom) {
|
|
264
|
+
const { on: events = {}, ...props } = vdom.props;
|
|
265
|
+
delete props.key;
|
|
266
|
+
return { props, events }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let isScheduled = false;
|
|
270
|
+
const jobs = [];
|
|
271
|
+
function enqueueJob(job) {
|
|
272
|
+
jobs.push(job);
|
|
273
|
+
scheduleUpdate();
|
|
274
|
+
}
|
|
275
|
+
function scheduleUpdate() {
|
|
276
|
+
if (isScheduled) return
|
|
277
|
+
isScheduled = true;
|
|
278
|
+
queueMicrotask(processJobs);
|
|
279
|
+
}
|
|
280
|
+
function processJobs() {
|
|
281
|
+
while (jobs.length > 0) {
|
|
282
|
+
const job = jobs.shift();
|
|
283
|
+
const result = job();
|
|
284
|
+
Promise.resolve(result).then(
|
|
285
|
+
() => {
|
|
286
|
+
},
|
|
287
|
+
(error) => {
|
|
288
|
+
console.error(`[scheduler]: ${error}`);
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
isScheduled = false;
|
|
293
|
+
}
|
|
294
|
+
function nextTick() {
|
|
295
|
+
scheduleUpdate();
|
|
296
|
+
return flushPromises()
|
|
297
|
+
}
|
|
298
|
+
function flushPromises() {
|
|
299
|
+
return new Promise((resolve) => setTimeout(resolve))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function mountDOM(vdom, parentEl, index, hostComponent = null) {
|
|
303
|
+
switch (vdom.type) {
|
|
304
|
+
case DOM_TYPES.TEXT: {
|
|
305
|
+
createTextNode(vdom, parentEl, index);
|
|
306
|
+
break
|
|
307
|
+
}
|
|
308
|
+
case DOM_TYPES.ELEMENT: {
|
|
309
|
+
createElementNode(vdom, parentEl, index, hostComponent);
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
case DOM_TYPES.FRAGMENT: {
|
|
313
|
+
createFragmentNodes(vdom, parentEl, index, hostComponent);
|
|
314
|
+
break
|
|
315
|
+
}
|
|
316
|
+
case DOM_TYPES.COMPONENT: {
|
|
317
|
+
createComponentNode(vdom, parentEl, index, hostComponent);
|
|
318
|
+
enqueueJob(() => vdom.component.onMounted());
|
|
319
|
+
break
|
|
320
|
+
}
|
|
321
|
+
default: {
|
|
322
|
+
throw new Error(`Can't mount DOM of type: ${vdom.type}`)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function createTextNode(vdom, parentEl, index) {
|
|
327
|
+
const { value } = vdom;
|
|
328
|
+
const textNode = document.createTextNode(value);
|
|
329
|
+
vdom.el = textNode;
|
|
330
|
+
insert(textNode, parentEl, index);
|
|
331
|
+
}
|
|
332
|
+
function createFragmentNodes(vdom, parentEl, index, hostComponent) {
|
|
333
|
+
const { children } = vdom;
|
|
334
|
+
vdom.el = parentEl;
|
|
335
|
+
children.forEach((child, i) =>
|
|
336
|
+
mountDOM(child, parentEl, index ? index + i : null, hostComponent)
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
function createElementNode(vdom, parentEl, index, hostComponent) {
|
|
340
|
+
const { tag, children } = vdom;
|
|
341
|
+
const element = document.createElement(tag);
|
|
342
|
+
addProps(element, vdom, hostComponent);
|
|
343
|
+
vdom.el = element;
|
|
344
|
+
children.forEach((child) => mountDOM(child, element, null, hostComponent));
|
|
345
|
+
insert(element, parentEl, index);
|
|
346
|
+
}
|
|
347
|
+
function addProps(el, vdom, hostComponent) {
|
|
348
|
+
const { props: attrs, events } = extractPropsAndEvents(vdom);
|
|
349
|
+
vdom.listeners = addEventListeners(events, el, hostComponent);
|
|
350
|
+
setAttributes(el, attrs);
|
|
351
|
+
}
|
|
352
|
+
function insert(el, parentEl, index) {
|
|
353
|
+
if (index == null) {
|
|
354
|
+
parentEl.append(el);
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
if (index < 0) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Index must be a positive integer, got ${index}`)
|
|
360
|
+
}
|
|
361
|
+
const children = parentEl.childNodes;
|
|
362
|
+
if (index >= children.length) {
|
|
363
|
+
parentEl.append(el);
|
|
364
|
+
} else {
|
|
365
|
+
parentEl.insertBefore(el, children[index]);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function createComponentNode(vdom, parentEl, index, hostComponent) {
|
|
369
|
+
const { tag: Component, children } = vdom;
|
|
370
|
+
const { props, events } = extractPropsAndEvents(vdom);
|
|
371
|
+
const component = new Component(props, events, hostComponent);
|
|
372
|
+
component.setExternalContent(children);
|
|
373
|
+
component.setAppContext(hostComponent?.appContext ?? {});
|
|
374
|
+
component.mount(parentEl, index);
|
|
375
|
+
vdom.component = component;
|
|
376
|
+
vdom.el = component.firstElement;
|
|
377
|
+
}
|
|
378
|
+
function extractChildren(vdom) {
|
|
379
|
+
if (vdom.children == null || !Array.isArray(vdom.children)) {
|
|
380
|
+
return []
|
|
381
|
+
}
|
|
382
|
+
const children = [];
|
|
383
|
+
for (const child of vdom.children) {
|
|
384
|
+
if (child.type === DOM_TYPES.FRAGMENT) {
|
|
385
|
+
children.push(...extractChildren(child));
|
|
386
|
+
} else {
|
|
387
|
+
children.push(child);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return children
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function assert(condition, message = 'Assertion failed') {
|
|
394
|
+
if (!condition) {
|
|
395
|
+
throw new Error(message)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function destroyDOM(vdom) {
|
|
400
|
+
const { type } = vdom;
|
|
401
|
+
switch (type) {
|
|
402
|
+
case DOM_TYPES.TEXT: {
|
|
403
|
+
removeTextNode(vdom);
|
|
404
|
+
break
|
|
405
|
+
}
|
|
406
|
+
case DOM_TYPES.ELEMENT: {
|
|
407
|
+
removeElementNode(vdom);
|
|
408
|
+
break
|
|
409
|
+
}
|
|
410
|
+
case DOM_TYPES.FRAGMENT: {
|
|
411
|
+
removeFragmentNodes(vdom);
|
|
412
|
+
break
|
|
413
|
+
}
|
|
414
|
+
case DOM_TYPES.COMPONENT: {
|
|
415
|
+
vdom.component.unmount();
|
|
416
|
+
enqueueJob(() => vdom.component.onUnmounted());
|
|
417
|
+
break
|
|
418
|
+
}
|
|
419
|
+
default: {
|
|
420
|
+
throw new Error(`Can't destroy DOM of type: ${type}`)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
delete vdom.el;
|
|
424
|
+
}
|
|
425
|
+
function removeTextNode(vdom) {
|
|
426
|
+
const { el } = vdom;
|
|
427
|
+
assert(el instanceof Text);
|
|
428
|
+
el.remove();
|
|
429
|
+
}
|
|
430
|
+
function removeElementNode(vdom) {
|
|
431
|
+
const { el, children, listeners } = vdom;
|
|
432
|
+
assert(el instanceof HTMLElement);
|
|
433
|
+
el.remove();
|
|
434
|
+
children.forEach(destroyDOM);
|
|
435
|
+
if (listeners) {
|
|
436
|
+
removeEventListeners(listeners, el);
|
|
437
|
+
delete vdom.listeners;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function removeFragmentNodes(vdom) {
|
|
441
|
+
const { children } = vdom;
|
|
442
|
+
children.forEach(destroyDOM);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const CATCH_ALL_ROUTE = '*';
|
|
446
|
+
function makeRouteMatcher(route) {
|
|
447
|
+
return routeHasParams(route)
|
|
448
|
+
? makeMatcherWithParams(route)
|
|
449
|
+
: makeMatcherWithoutParams(route)
|
|
450
|
+
}
|
|
451
|
+
function routeHasParams({ path }) {
|
|
452
|
+
return path.includes(':')
|
|
453
|
+
}
|
|
454
|
+
function makeMatcherWithParams(route) {
|
|
455
|
+
const regex = makeRouteWithParamsRegex(route);
|
|
456
|
+
const isRedirect = typeof route.redirect === 'string';
|
|
457
|
+
return {
|
|
458
|
+
route,
|
|
459
|
+
isRedirect,
|
|
460
|
+
checkMatch(path) {
|
|
461
|
+
return regex.test(path)
|
|
462
|
+
},
|
|
463
|
+
extractParams(path) {
|
|
464
|
+
const { groups } = regex.exec(path);
|
|
465
|
+
return groups
|
|
466
|
+
},
|
|
467
|
+
extractQuery,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function makeRouteWithParamsRegex({ path }) {
|
|
471
|
+
const regex = path.replace(
|
|
472
|
+
/:([^/]+)/g,
|
|
473
|
+
(_, paramName) => `(?<${paramName}>[^/]+)`
|
|
474
|
+
);
|
|
475
|
+
return new RegExp(`^${regex}$`)
|
|
476
|
+
}
|
|
477
|
+
function makeMatcherWithoutParams(route) {
|
|
478
|
+
const regex = makeRouteWithoutParamsRegex(route);
|
|
479
|
+
const isRedirect = typeof route.redirect === 'string';
|
|
480
|
+
return {
|
|
481
|
+
route,
|
|
482
|
+
isRedirect,
|
|
483
|
+
checkMatch(path) {
|
|
484
|
+
return regex.test(path)
|
|
485
|
+
},
|
|
486
|
+
extractParams() {
|
|
487
|
+
return {}
|
|
488
|
+
},
|
|
489
|
+
extractQuery,
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function makeRouteWithoutParamsRegex({ path }) {
|
|
493
|
+
if (path === CATCH_ALL_ROUTE) {
|
|
494
|
+
return new RegExp('^.*$')
|
|
495
|
+
}
|
|
496
|
+
return new RegExp(`^${path}$`)
|
|
497
|
+
}
|
|
498
|
+
function extractQuery(path) {
|
|
499
|
+
const queryIndex = path.indexOf('?');
|
|
500
|
+
if (queryIndex === -1) {
|
|
501
|
+
return {}
|
|
502
|
+
}
|
|
503
|
+
const search = new URLSearchParams(path.slice(queryIndex + 1));
|
|
504
|
+
return Object.fromEntries(search.entries())
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
class Dispatcher {
|
|
508
|
+
#subs = new Map()
|
|
509
|
+
#afterHandlers = []
|
|
510
|
+
subscribe(commandName, handler) {
|
|
511
|
+
if (!this.#subs.has(commandName)) {
|
|
512
|
+
this.#subs.set(commandName, []);
|
|
513
|
+
}
|
|
514
|
+
const handlers = this.#subs.get(commandName);
|
|
515
|
+
if (handlers.includes(handler)) {
|
|
516
|
+
return () => {}
|
|
517
|
+
}
|
|
518
|
+
handlers.push(handler);
|
|
519
|
+
return () => {
|
|
520
|
+
const idx = handlers.indexOf(handler);
|
|
521
|
+
handlers.splice(idx, 1);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
afterEveryCommand(handler) {
|
|
525
|
+
this.#afterHandlers.push(handler);
|
|
526
|
+
return () => {
|
|
527
|
+
const idx = this.#afterHandlers.indexOf(handler);
|
|
528
|
+
this.#afterHandlers.splice(idx, 1);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
dispatch(commandName, payload) {
|
|
532
|
+
if (this.#subs.has(commandName)) {
|
|
533
|
+
this.#subs.get(commandName).forEach((handler) => handler(payload));
|
|
534
|
+
} else {
|
|
535
|
+
console.warn(`No handlers for command: ${commandName}`);
|
|
536
|
+
}
|
|
537
|
+
this.#afterHandlers.forEach((handler) => handler());
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const ROUTER_EVENT = 'router-event';
|
|
542
|
+
class HashRouter {
|
|
543
|
+
#matchers = []
|
|
544
|
+
#onPopState = () => this.#matchCurrentRoute()
|
|
545
|
+
#isInitialized = false
|
|
546
|
+
#dispatcher = new Dispatcher()
|
|
547
|
+
#subscriptions = new WeakMap()
|
|
548
|
+
#subscriberFns = new Set()
|
|
549
|
+
#matchedRoute = null
|
|
550
|
+
#params = {}
|
|
551
|
+
#query = {}
|
|
552
|
+
get matchedRoute() {
|
|
553
|
+
return this.#matchedRoute
|
|
554
|
+
}
|
|
555
|
+
get params() {
|
|
556
|
+
return this.#params
|
|
557
|
+
}
|
|
558
|
+
get query() {
|
|
559
|
+
return this.#query
|
|
560
|
+
}
|
|
561
|
+
constructor(routes = []) {
|
|
562
|
+
this.#matchers = routes.map(makeRouteMatcher);
|
|
563
|
+
}
|
|
564
|
+
async init() {
|
|
565
|
+
if (this.#isInitialized) {
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
if (document.location.hash === '') {
|
|
569
|
+
window.history.replaceState({}, '', '#/');
|
|
570
|
+
}
|
|
571
|
+
window.addEventListener('popstate', this.#onPopState);
|
|
572
|
+
await this.#matchCurrentRoute();
|
|
573
|
+
this.#isInitialized = true;
|
|
574
|
+
}
|
|
575
|
+
destroy() {
|
|
576
|
+
if (!this.#isInitialized) {
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
window.removeEventListener('popstate', this.#onPopState);
|
|
580
|
+
Array.from(this.#subscriberFns).forEach(this.unsubscribe, this);
|
|
581
|
+
this.#isInitialized = false;
|
|
582
|
+
}
|
|
583
|
+
async navigateTo(path, push = true) {
|
|
584
|
+
const matcher = this.#matchers.find((matcher) => matcher.checkMatch(path));
|
|
585
|
+
const from = this.#matchedRoute;
|
|
586
|
+
if (matcher == null) {
|
|
587
|
+
console.warn(`[Router] No route matches path "${path}"`);
|
|
588
|
+
this.#matchedRoute = null;
|
|
589
|
+
this.#params = {};
|
|
590
|
+
this.#query = {};
|
|
591
|
+
if (push) {
|
|
592
|
+
this.#pushState(path);
|
|
593
|
+
}
|
|
594
|
+
this.#dispatcher.dispatch(ROUTER_EVENT, { from, to: null, router: this });
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
if (matcher.isRedirect) {
|
|
598
|
+
return this.navigateTo(matcher.route.redirect)
|
|
599
|
+
}
|
|
600
|
+
const to = matcher.route;
|
|
601
|
+
const params = matcher.extractParams(path);
|
|
602
|
+
const query = matcher.extractQuery(path);
|
|
603
|
+
const { shouldNavigate, shouldRedirect, redirectPath } = await this.#canChangeRoute(from, to);
|
|
604
|
+
if (shouldRedirect) {
|
|
605
|
+
return this.navigateTo(redirectPath, push)
|
|
606
|
+
}
|
|
607
|
+
if (shouldNavigate) {
|
|
608
|
+
this.#matchedRoute = to;
|
|
609
|
+
this.#params = params;
|
|
610
|
+
this.#query = query;
|
|
611
|
+
if (push) {
|
|
612
|
+
this.#pushState(path);
|
|
613
|
+
}
|
|
614
|
+
this.#dispatcher.dispatch(ROUTER_EVENT, { from, to, router: this });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
back() {
|
|
618
|
+
window.history.back();
|
|
619
|
+
}
|
|
620
|
+
forward() {
|
|
621
|
+
window.history.forward();
|
|
622
|
+
}
|
|
623
|
+
get #currentRouteHash() {
|
|
624
|
+
const hash = document.location.hash;
|
|
625
|
+
if (hash === '') {
|
|
626
|
+
return '/'
|
|
627
|
+
}
|
|
628
|
+
return hash.slice(1)
|
|
629
|
+
}
|
|
630
|
+
#matchCurrentRoute() {
|
|
631
|
+
return this.navigateTo(this.#currentRouteHash, false)
|
|
632
|
+
}
|
|
633
|
+
#pushState(path) {
|
|
634
|
+
window.history.pushState({}, '', `#${path}`);
|
|
635
|
+
}
|
|
636
|
+
async #canChangeRoute(from, to, params, query) {
|
|
637
|
+
const guard = to.beforeEnter;
|
|
638
|
+
if (typeof guard !== 'function') {
|
|
639
|
+
return {
|
|
640
|
+
shouldRedirect: false,
|
|
641
|
+
shouldNavigate: true,
|
|
642
|
+
redirectPath: null,
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const result = await guard(from?.path, to?.path, params, query);
|
|
646
|
+
if (result === false) {
|
|
647
|
+
return {
|
|
648
|
+
shouldRedirect: false,
|
|
649
|
+
shouldNavigate: false,
|
|
650
|
+
redirectPath: null,
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (typeof result === 'string') {
|
|
654
|
+
return {
|
|
655
|
+
shouldRedirect: true,
|
|
656
|
+
shouldNavigate: false,
|
|
657
|
+
redirectPath: result,
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
shouldRedirect: false,
|
|
662
|
+
shouldNavigate: true,
|
|
663
|
+
redirectPath: null,
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
subscribe(handler) {
|
|
667
|
+
const unsubscribe = this.#dispatcher.subscribe(ROUTER_EVENT, handler);
|
|
668
|
+
this.#subscriptions.set(handler, unsubscribe);
|
|
669
|
+
this.#subscriberFns.add(handler);
|
|
670
|
+
}
|
|
671
|
+
unsubscribe(handler) {
|
|
672
|
+
const unsubscribe = this.#subscriptions.get(handler);
|
|
673
|
+
if (unsubscribe) {
|
|
674
|
+
unsubscribe();
|
|
675
|
+
this.#subscriptions.delete(handler);
|
|
676
|
+
this.#subscriberFns.delete(handler);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
class NoopRouter {
|
|
681
|
+
init() {}
|
|
682
|
+
destroy() {}
|
|
683
|
+
navigateTo() {}
|
|
684
|
+
back() {}
|
|
685
|
+
forward() {}
|
|
686
|
+
subscribe() {}
|
|
687
|
+
unsubscribe() {}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function createApp(RootComponent, props = {}, options = {}) {
|
|
691
|
+
let parentEl = null;
|
|
692
|
+
let isMounted = false;
|
|
693
|
+
let vdom = null;
|
|
694
|
+
const context = {
|
|
695
|
+
router: options.router || new NoopRouter(),
|
|
696
|
+
store: options.store || null,
|
|
697
|
+
};
|
|
698
|
+
function reset() {
|
|
699
|
+
parentEl = null;
|
|
700
|
+
isMounted = false;
|
|
701
|
+
vdom = null;
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
mount(_parentEl) {
|
|
705
|
+
if (isMounted) {
|
|
706
|
+
throw new Error('The application is already mounted')
|
|
707
|
+
}
|
|
708
|
+
parentEl = _parentEl;
|
|
709
|
+
vdom = h(RootComponent, props);
|
|
710
|
+
mountDOM(vdom, parentEl, null, { appContext: context });
|
|
711
|
+
context.router.init();
|
|
712
|
+
isMounted = true;
|
|
713
|
+
},
|
|
714
|
+
unmount() {
|
|
715
|
+
if (!isMounted) {
|
|
716
|
+
throw new Error('The application is not mounted')
|
|
717
|
+
}
|
|
718
|
+
destroyDOM(vdom);
|
|
719
|
+
context.router.destroy();
|
|
720
|
+
reset();
|
|
721
|
+
},
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function areNodesEqual(nodeOne, nodeTwo) {
|
|
726
|
+
if (!nodeOne || !nodeTwo) {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
if (nodeOne.type !== nodeTwo.type) {
|
|
730
|
+
return false
|
|
731
|
+
}
|
|
732
|
+
if (nodeOne.type === DOM_TYPES.ELEMENT) {
|
|
733
|
+
const { tag: tagOne, props: { key: keyOne },} = nodeOne;
|
|
734
|
+
const { tag: tagTwo, props: { key: keyTwo },} = nodeTwo;
|
|
735
|
+
return tagOne === tagTwo && keyOne === keyTwo
|
|
736
|
+
}
|
|
737
|
+
if (nodeOne.type === DOM_TYPES.COMPONENT) {
|
|
738
|
+
const { tag: componentOne, props: { key: keyOne },} = nodeOne;
|
|
739
|
+
const { tag: componentTwo, props: { key: keyTwo },} = nodeTwo;
|
|
740
|
+
return componentOne === componentTwo && keyOne === keyTwo
|
|
741
|
+
}
|
|
742
|
+
return true
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function objectsDiff(oldObj, newObj) {
|
|
746
|
+
const added = [];
|
|
747
|
+
const updated = [];
|
|
748
|
+
const removed = [];
|
|
749
|
+
for (const key in newObj) {
|
|
750
|
+
if (!Object.prototype.hasOwnProperty.call(oldObj, key)) {
|
|
751
|
+
added.push(key);
|
|
752
|
+
} else if (newObj[key] !== oldObj[key]) {
|
|
753
|
+
updated.push(key);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
for (const key in oldObj) {
|
|
757
|
+
if (!Object.prototype.hasOwnProperty.call(newObj, key)) {
|
|
758
|
+
removed.push(key);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return { added, updated, removed };
|
|
762
|
+
}
|
|
763
|
+
function hasOwnProperty(obj, prop) {
|
|
764
|
+
return Object.prototype.hasOwnProperty.call(obj, prop)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function isNotEmptyString(str) {
|
|
768
|
+
return str !== ''
|
|
769
|
+
}
|
|
770
|
+
function isNotBlankOrEmptyString(str) {
|
|
771
|
+
return isNotEmptyString(str.trim())
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function patchDOM(oldVdom, newVdom, parentEl, hostComponent = null) {
|
|
775
|
+
if (!areNodesEqual(oldVdom, newVdom)) {
|
|
776
|
+
const index = findIndexInParent(parentEl, oldVdom.el);
|
|
777
|
+
destroyDOM(oldVdom);
|
|
778
|
+
mountDOM(newVdom, parentEl, index, hostComponent);
|
|
779
|
+
return newVdom
|
|
780
|
+
}
|
|
781
|
+
newVdom.el = oldVdom.el;
|
|
782
|
+
switch (newVdom.type) {
|
|
783
|
+
case DOM_TYPES.TEXT: {
|
|
784
|
+
patchText(oldVdom, newVdom);
|
|
785
|
+
return newVdom
|
|
786
|
+
}
|
|
787
|
+
case DOM_TYPES.ELEMENT: {
|
|
788
|
+
patchElement(oldVdom, newVdom, hostComponent);
|
|
789
|
+
break
|
|
790
|
+
}
|
|
791
|
+
case DOM_TYPES.COMPONENT: {
|
|
792
|
+
patchComponent(oldVdom, newVdom);
|
|
793
|
+
return newVdom
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
patchChildren(oldVdom, newVdom, hostComponent);
|
|
797
|
+
return newVdom
|
|
798
|
+
}
|
|
799
|
+
function findIndexInParent(parentEl, el) {
|
|
800
|
+
const index = Array.from(parentEl.childNodes).indexOf(el);
|
|
801
|
+
if (index < 0) {
|
|
802
|
+
return null
|
|
803
|
+
}
|
|
804
|
+
return index
|
|
805
|
+
}
|
|
806
|
+
function patchText(oldVdom, newVdom) {
|
|
807
|
+
const el = oldVdom.el;
|
|
808
|
+
const {value: oldText} = oldVdom;
|
|
809
|
+
const {value: newText} = newVdom;
|
|
810
|
+
if (oldText !== newText) {
|
|
811
|
+
el.nodeValue = newText;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function patchElement(oldVdom, newVdom, hostComponent) {
|
|
815
|
+
const el = oldVdom.el;
|
|
816
|
+
const {
|
|
817
|
+
class: oldClass,
|
|
818
|
+
style: oldStyle,
|
|
819
|
+
on: oldEvents,
|
|
820
|
+
...oldAttrs
|
|
821
|
+
} = oldVdom.props;
|
|
822
|
+
const {
|
|
823
|
+
class: newClass,
|
|
824
|
+
style: newStyle,
|
|
825
|
+
on: newEvents,
|
|
826
|
+
...newAttrs
|
|
827
|
+
} = newVdom.props;
|
|
828
|
+
const {listeners: oldListeners} = oldVdom;
|
|
829
|
+
patchAttrs(el, oldAttrs, newAttrs);
|
|
830
|
+
patchClasses(el, oldClass, newClass);
|
|
831
|
+
patchStyles(el, oldStyle, newStyle);
|
|
832
|
+
newVdom.listeners = patchEvents(el, oldListeners, oldEvents, newEvents, hostComponent);
|
|
833
|
+
}
|
|
834
|
+
function patchAttrs(el, oldAttrs, newAttrs) {
|
|
835
|
+
const {added, removed, updated} = objectsDiff(oldAttrs, newAttrs);
|
|
836
|
+
for (const attr of removed) {
|
|
837
|
+
removeAttribute(el, attr);
|
|
838
|
+
}
|
|
839
|
+
for (const attr of added.concat(updated)) {
|
|
840
|
+
setAttribute(el, attr, newAttrs[attr]);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function patchClasses(el, oldClass, newClass) {
|
|
844
|
+
const oldClasses = toClassList(oldClass);
|
|
845
|
+
const newClasses = toClassList(newClass);
|
|
846
|
+
const {added, removed} =
|
|
847
|
+
arraysDiff(oldClasses, newClasses);
|
|
848
|
+
if (removed.length > 0) {
|
|
849
|
+
el.classList.remove(...removed);
|
|
850
|
+
}
|
|
851
|
+
if (added.length > 0) {
|
|
852
|
+
el.classList.add(...added);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function toClassList(classes = '') {
|
|
856
|
+
return Array.isArray(classes)
|
|
857
|
+
? classes.filter(isNotBlankOrEmptyString)
|
|
858
|
+
: classes.split(/(\s+)/)
|
|
859
|
+
.filter(isNotBlankOrEmptyString)
|
|
860
|
+
}
|
|
861
|
+
function patchStyles(el, oldStyle = {}, newStyle = {}) {
|
|
862
|
+
const { added, removed, updated } = objectsDiff(oldStyle, newStyle);
|
|
863
|
+
for (const style of removed) {
|
|
864
|
+
removeStyle(el, style);
|
|
865
|
+
}
|
|
866
|
+
for (const style of added.concat(updated)) {
|
|
867
|
+
setStyle(el, style, newStyle[style]);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function patchEvents(el, oldListeners = {}, oldEvents = {}, newEvents = {}, hostComponent) {
|
|
871
|
+
const { removed, added, updated } = objectsDiff(oldEvents, newEvents);
|
|
872
|
+
for (const eventName of removed.concat(updated)) {
|
|
873
|
+
el.removeEventListener(eventName, oldListeners[eventName]);
|
|
874
|
+
}
|
|
875
|
+
const addedListeners = {};
|
|
876
|
+
for (const eventName of added.concat(updated)) {
|
|
877
|
+
const listener =
|
|
878
|
+
addEventListener(eventName, newEvents[eventName], el, hostComponent);
|
|
879
|
+
addedListeners[eventName] = listener;
|
|
880
|
+
}
|
|
881
|
+
return addedListeners
|
|
882
|
+
}
|
|
883
|
+
function patchComponent(oldVdom, newVdom) {
|
|
884
|
+
const {component} = oldVdom;
|
|
885
|
+
const { children } = newVdom;
|
|
886
|
+
const { props } = extractPropsAndEvents(newVdom);
|
|
887
|
+
component.setExternalContent(children);
|
|
888
|
+
component.updateProps(props);
|
|
889
|
+
newVdom.component = component;
|
|
890
|
+
newVdom.el = component.firstElement;
|
|
891
|
+
}
|
|
892
|
+
function patchChildren(oldVdom, newVdom, hostComponent) {
|
|
893
|
+
const oldChildren = extractChildren(oldVdom);
|
|
894
|
+
const newChildren = extractChildren(newVdom);
|
|
895
|
+
const parentEl = oldVdom.el;
|
|
896
|
+
const diffSeq = arraysDiffSequence(
|
|
897
|
+
oldChildren,
|
|
898
|
+
newChildren,
|
|
899
|
+
areNodesEqual
|
|
900
|
+
);
|
|
901
|
+
for (const operation of diffSeq) {
|
|
902
|
+
const { from, index, item, originalIndex } = operation;
|
|
903
|
+
const offset = hostComponent?.offset ?? 0;
|
|
904
|
+
switch (operation.op) {
|
|
905
|
+
case ARRAY_DIFF_OP.ADD: {
|
|
906
|
+
mountDOM(item, parentEl, index + offset, hostComponent);
|
|
907
|
+
break
|
|
908
|
+
}
|
|
909
|
+
case ARRAY_DIFF_OP.REMOVE: {
|
|
910
|
+
destroyDOM(item);
|
|
911
|
+
break
|
|
912
|
+
}
|
|
913
|
+
case ARRAY_DIFF_OP.MOVE: {
|
|
914
|
+
const oldVdom = oldChildren[originalIndex];
|
|
915
|
+
const el = oldVdom.el;
|
|
916
|
+
const elAtTargetIndex = parentEl.childNodes[index + offset];
|
|
917
|
+
parentEl.insertBefore(el, elAtTargetIndex);
|
|
918
|
+
patchDOM(
|
|
919
|
+
oldVdom,
|
|
920
|
+
newChildren[index],
|
|
921
|
+
parentEl,
|
|
922
|
+
hostComponent
|
|
923
|
+
);
|
|
924
|
+
break
|
|
925
|
+
}
|
|
926
|
+
case ARRAY_DIFF_OP.NOOP: {
|
|
927
|
+
patchDOM(
|
|
928
|
+
oldChildren[originalIndex],
|
|
929
|
+
newChildren[index],
|
|
930
|
+
parentEl,
|
|
931
|
+
hostComponent
|
|
932
|
+
);
|
|
933
|
+
break
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function traverseDFS(
|
|
940
|
+
vdom,
|
|
941
|
+
processNode,
|
|
942
|
+
shouldSkipBranch = () => false,
|
|
943
|
+
parentNode = null,
|
|
944
|
+
index = null
|
|
945
|
+
) {
|
|
946
|
+
if (shouldSkipBranch(vdom)) return
|
|
947
|
+
processNode(vdom, parentNode, index);
|
|
948
|
+
if (vdom.children) {
|
|
949
|
+
vdom.children.forEach((child, i) =>
|
|
950
|
+
traverseDFS(child, processNode, shouldSkipBranch, vdom, i)
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function fillSlots(vdom, externalContent = {}) {
|
|
956
|
+
function processNode(node, parent, index) {
|
|
957
|
+
insertViewInSlot(node, parent, index, externalContent);
|
|
958
|
+
}
|
|
959
|
+
traverseDFS(vdom, processNode, shouldSkipBranch);
|
|
960
|
+
}
|
|
961
|
+
function insertViewInSlot(node, parent, index, externalContent) {
|
|
962
|
+
if (node.type !== DOM_TYPES.SLOT) return
|
|
963
|
+
const slotName = node.name || 'default';
|
|
964
|
+
const customContent = externalContent[slotName];
|
|
965
|
+
const defaultContent = node.children;
|
|
966
|
+
const views = customContent !== undefined ? customContent : defaultContent;
|
|
967
|
+
const hasContent = views && views.length > 0;
|
|
968
|
+
if (hasContent) {
|
|
969
|
+
parent.children.splice(index, 1, hFragment(views));
|
|
970
|
+
} else {
|
|
971
|
+
parent.children.splice(index, 1);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function shouldSkipBranch(node) {
|
|
975
|
+
return node.type === DOM_TYPES.COMPONENT
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function getDefaultExportFromCjs (x) {
|
|
979
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
var fastDeepEqual;
|
|
983
|
+
var hasRequiredFastDeepEqual;
|
|
984
|
+
function requireFastDeepEqual () {
|
|
985
|
+
if (hasRequiredFastDeepEqual) return fastDeepEqual;
|
|
986
|
+
hasRequiredFastDeepEqual = 1;
|
|
987
|
+
fastDeepEqual = function equal(a, b) {
|
|
988
|
+
if (a === b) return true;
|
|
989
|
+
if (a && b && typeof a == 'object' && typeof b == 'object') {
|
|
990
|
+
if (a.constructor !== b.constructor) return false;
|
|
991
|
+
var length, i, keys;
|
|
992
|
+
if (Array.isArray(a)) {
|
|
993
|
+
length = a.length;
|
|
994
|
+
if (length != b.length) return false;
|
|
995
|
+
for (i = length; i-- !== 0;)
|
|
996
|
+
if (!equal(a[i], b[i])) return false;
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
999
|
+
if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
|
|
1000
|
+
if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
|
|
1001
|
+
if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
|
|
1002
|
+
keys = Object.keys(a);
|
|
1003
|
+
length = keys.length;
|
|
1004
|
+
if (length !== Object.keys(b).length) return false;
|
|
1005
|
+
for (i = length; i-- !== 0;)
|
|
1006
|
+
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
|
|
1007
|
+
for (i = length; i-- !== 0;) {
|
|
1008
|
+
var key = keys[i];
|
|
1009
|
+
if (!equal(a[key], b[key])) return false;
|
|
1010
|
+
}
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
return a!==a && b!==b;
|
|
1014
|
+
};
|
|
1015
|
+
return fastDeepEqual;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
var fastDeepEqualExports = requireFastDeepEqual();
|
|
1019
|
+
var equal = /*@__PURE__*/getDefaultExportFromCjs(fastDeepEqualExports);
|
|
1020
|
+
|
|
1021
|
+
function defineComponent({ render, state, ...methods }) {
|
|
1022
|
+
class Component {
|
|
1023
|
+
#isMounted = false
|
|
1024
|
+
#vdom = null
|
|
1025
|
+
#hostEl = null
|
|
1026
|
+
#eventHandlers = null
|
|
1027
|
+
#parentComponent = null
|
|
1028
|
+
#dispatcher = new Dispatcher()
|
|
1029
|
+
#subscriptions = []
|
|
1030
|
+
#appContext = null
|
|
1031
|
+
#children = []
|
|
1032
|
+
setExternalContent(children) {
|
|
1033
|
+
this.#children = children;
|
|
1034
|
+
}
|
|
1035
|
+
constructor(props = {}, eventHandlers = {}, parentComponent = null,) {
|
|
1036
|
+
this.props = props;
|
|
1037
|
+
this.state = state ? state(props) : {};
|
|
1038
|
+
this.#eventHandlers = eventHandlers;
|
|
1039
|
+
this.#parentComponent = parentComponent;
|
|
1040
|
+
}
|
|
1041
|
+
onMounted() {
|
|
1042
|
+
return Promise.resolve()
|
|
1043
|
+
}
|
|
1044
|
+
onUnmounted() {
|
|
1045
|
+
return Promise.resolve()
|
|
1046
|
+
}
|
|
1047
|
+
setAppContext(appContext) {
|
|
1048
|
+
this.#appContext = appContext;
|
|
1049
|
+
}
|
|
1050
|
+
get appContext() {
|
|
1051
|
+
return this.#appContext
|
|
1052
|
+
}
|
|
1053
|
+
get elements() {
|
|
1054
|
+
if (this.#vdom == null) {
|
|
1055
|
+
return []
|
|
1056
|
+
}
|
|
1057
|
+
if (this.#vdom.type === DOM_TYPES.FRAGMENT) {
|
|
1058
|
+
return extractChildren(this.#vdom).flatMap((child) => {
|
|
1059
|
+
if (child.type === DOM_TYPES.COMPONENT) {
|
|
1060
|
+
return child.component.elements
|
|
1061
|
+
}
|
|
1062
|
+
return [child.el]
|
|
1063
|
+
})
|
|
1064
|
+
}
|
|
1065
|
+
return [this.#vdom.el]
|
|
1066
|
+
}
|
|
1067
|
+
get firstElement() {
|
|
1068
|
+
return this.elements[0]
|
|
1069
|
+
}
|
|
1070
|
+
get offset() {
|
|
1071
|
+
if (this.#vdom.type === DOM_TYPES.FRAGMENT) {
|
|
1072
|
+
return Array.from(this.#hostEl.children).indexOf(this.firstElement)
|
|
1073
|
+
}
|
|
1074
|
+
return 0
|
|
1075
|
+
}
|
|
1076
|
+
updateProps(props) {
|
|
1077
|
+
const newProps = { ...this.props, ...props };
|
|
1078
|
+
if (equal(this.props, newProps)) {
|
|
1079
|
+
return
|
|
1080
|
+
}
|
|
1081
|
+
this.props = newProps;
|
|
1082
|
+
this.#patch();
|
|
1083
|
+
}
|
|
1084
|
+
updateState(state) {
|
|
1085
|
+
this.state = { ...this.state, ...state };
|
|
1086
|
+
this.#patch();
|
|
1087
|
+
}
|
|
1088
|
+
render() {
|
|
1089
|
+
const vdom = render.call(this);
|
|
1090
|
+
if (didCreateSlot()) {
|
|
1091
|
+
fillSlots(vdom, this.#children);
|
|
1092
|
+
resetDidCreateSlot();
|
|
1093
|
+
}
|
|
1094
|
+
return vdom
|
|
1095
|
+
}
|
|
1096
|
+
mount(hostEl, index = null) {
|
|
1097
|
+
if (this.#isMounted) {
|
|
1098
|
+
throw new Error('Component is already mounted')
|
|
1099
|
+
}
|
|
1100
|
+
this.#vdom = this.render();
|
|
1101
|
+
mountDOM(this.#vdom, hostEl, index, this);
|
|
1102
|
+
this.#wireEventHandlers();
|
|
1103
|
+
this.#isMounted = true;
|
|
1104
|
+
this.#hostEl = hostEl;
|
|
1105
|
+
}
|
|
1106
|
+
unmount() {
|
|
1107
|
+
if (!this.#isMounted) {
|
|
1108
|
+
throw new Error('Component is not mounted')
|
|
1109
|
+
}
|
|
1110
|
+
destroyDOM(this.#vdom);
|
|
1111
|
+
this.#subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
1112
|
+
this.#vdom = null;
|
|
1113
|
+
this.#isMounted = false;
|
|
1114
|
+
this.#hostEl = null;
|
|
1115
|
+
this.#subscriptions = [];
|
|
1116
|
+
}
|
|
1117
|
+
emit(eventName, payload) {
|
|
1118
|
+
this.#dispatcher.dispatch(eventName, payload);
|
|
1119
|
+
}
|
|
1120
|
+
#patch() {
|
|
1121
|
+
if (!this.#isMounted) {
|
|
1122
|
+
throw new Error('Component is not mounted')
|
|
1123
|
+
}
|
|
1124
|
+
const vdom = this.render();
|
|
1125
|
+
this.#vdom = patchDOM(this.#vdom, vdom, this.#hostEl, this);
|
|
1126
|
+
}
|
|
1127
|
+
#wireEventHandlers() {
|
|
1128
|
+
this.#subscriptions = Object.entries(this.#eventHandlers).map(
|
|
1129
|
+
([eventName, handler]) =>
|
|
1130
|
+
this.#wireEventHandler(eventName, handler)
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
#wireEventHandler(eventName, handler) {
|
|
1134
|
+
return this.#dispatcher.subscribe(eventName, (payload) => {
|
|
1135
|
+
if (this.#parentComponent) {
|
|
1136
|
+
handler.call(this.#parentComponent, payload);
|
|
1137
|
+
} else {
|
|
1138
|
+
handler(payload);
|
|
1139
|
+
}
|
|
1140
|
+
})
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
for (const methodName in methods) {
|
|
1144
|
+
if (hasOwnProperty(Component, methodName)) {
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
`Method "${methodName}()" already exists in the component.`
|
|
1147
|
+
)
|
|
1148
|
+
}
|
|
1149
|
+
Component.prototype[methodName] = methods[methodName];
|
|
1150
|
+
}
|
|
1151
|
+
return Component
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const RouterLink = defineComponent({
|
|
1155
|
+
render() {
|
|
1156
|
+
const { to } = this.props;
|
|
1157
|
+
return h(
|
|
1158
|
+
'a',
|
|
1159
|
+
{
|
|
1160
|
+
href: to,
|
|
1161
|
+
on: {
|
|
1162
|
+
click: (e) => {
|
|
1163
|
+
e.preventDefault();
|
|
1164
|
+
this.appContext.router.navigateTo(to);
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1167
|
+
},
|
|
1168
|
+
[hSlot()]
|
|
1169
|
+
)
|
|
1170
|
+
},
|
|
1171
|
+
});
|
|
1172
|
+
const RouterOutlet = defineComponent({
|
|
1173
|
+
state() {
|
|
1174
|
+
return {
|
|
1175
|
+
matchedRoute: this.appContext.router.matchedRoute,
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
onMounted() {
|
|
1179
|
+
this.boundHandler = this.handleRouteChange.bind(this);
|
|
1180
|
+
this.appContext.router.subscribe(this.boundHandler);
|
|
1181
|
+
},
|
|
1182
|
+
onUnmounted() {
|
|
1183
|
+
this.appContext.router.unsubscribe(this.boundHandler);
|
|
1184
|
+
},
|
|
1185
|
+
handleRouteChange({ to }) {
|
|
1186
|
+
this.updateState({ matchedRoute: to });
|
|
1187
|
+
},
|
|
1188
|
+
render() {
|
|
1189
|
+
const { matchedRoute } = this.state;
|
|
1190
|
+
return h('div', { id: 'router-outlet' }, [
|
|
1191
|
+
matchedRoute ? h(matchedRoute.component) : null,
|
|
1192
|
+
])
|
|
1193
|
+
},
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
export { DOM_TYPES, HashRouter, RouterLink, RouterOutlet, createApp, defineComponent, h, hFragment, hSlot, hString, nextTick };
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "turbo-web",
|
|
3
|
+
"version": "4.2.0",
|
|
4
|
+
"main": "dist/turbo.js",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist/turbo.js"
|
|
7
|
+
],
|
|
8
|
+
"bin": {
|
|
9
|
+
"turbo-charge": "/bin/turbo-setup.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"prepack": "npm run build",
|
|
13
|
+
"build": "rollup -c",
|
|
14
|
+
"lint": "eslint src",
|
|
15
|
+
"lint:fix": "eslint src --fix",
|
|
16
|
+
"test": "vitest",
|
|
17
|
+
"test:run": "vitest run"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@rollup/plugin-commonjs": "=29.0.1",
|
|
21
|
+
"@rollup/plugin-node-resolve": "=16.0.3",
|
|
22
|
+
"eslint": "=10.0.0",
|
|
23
|
+
"eslint-plugin-no-floating-promise": "=2.0.0",
|
|
24
|
+
"jsdom": "=28.1.0",
|
|
25
|
+
"rollup": "=4.59.0",
|
|
26
|
+
"rollup-plugin-bundle-stats": "=4.21.10",
|
|
27
|
+
"rollup-plugin-cleanup": "=3.2.1",
|
|
28
|
+
"vitest": "=4.0.18"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"fast-deep-equal": "=3.1.3"
|
|
32
|
+
}
|
|
33
|
+
}
|