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.
@@ -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
+ }