Passed
Push — trunk ( d83e6d...476ba8 )
by Christian
14:11 queued 23s
created

VueAdapter.initVue3   C

Complexity

Conditions 6

Size

Total Lines 108
Code Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 79
dl 0
loc 108
rs 6.7539
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
// @ts-nocheck
2
3
/**
4
 * @package admin
5
 */
6
import ViewAdapter from 'src/core/adapter/view.adapter';
7
8
// Vue3 imports
9
import { createI18n } from 'vue-i18n_v3';
10
import type { FallbackLocale, I18n } from 'vue-i18n_v3';
11
import type Router from 'vue-router_v3';
12
13
// Vue2 imports
14
import VueRouter from 'vue-router';
15
import VueI18n from 'vue-i18n';
16
import VueMeta from 'vue-meta';
17
18
import Vue, { createApp, defineAsyncComponent, h } from 'vue';
19
import type { AsyncComponent, Component as VueComponent, PluginObject } from 'vue';
20
import VuePlugins from 'src/app/plugin';
21
import setupShopwareDevtools from 'src/app/adapter/view/sw-vue-devtools';
22
import type ApplicationBootstrapper from 'src/core/application';
23
import type { ComponentConfig } from 'src/core/factory/async-component.factory';
24
import type { Store } from 'vuex';
25
26
const { Component, State, Mixin } = Shopware;
27
28
/**
29
 * @deprecated tag:v6.6.0 - Will be private
30
 */
31
export default class VueAdapter extends ViewAdapter {
32
    private resolvedComponentConfigs: Map<string, ComponentConfig>;
33
34
    private vueComponents: {
35
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
        [componentName: string]: VueComponent<any, any, any, any> | AsyncComponent<any, any, any, any>
37
    };
38
39
    private i18n?: I18n;
40
41
    public app;
42
43
    private vue3 = false;
44
45
    constructor(Application: ApplicationBootstrapper) {
46
        super(Application);
47
48
        this.i18n = undefined;
49
        this.resolvedComponentConfigs = new Map();
50
        this.vueComponents = {};
51
        // @ts-expect-error
52
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
53
        this.vue3 = !!window._features_?.vue3;
54
55
        if (this.vue3) {
56
            // eslint-disable-next-line @typescript-eslint/no-unsafe-call
57
            this.app = createApp({ name: 'ShopwareAdministration', template: '<sw-admin />' }) as Vue;
58
        }
59
    }
60
61
    /**
62
     * Creates the main instance for the view layer.
63
     * Is used on startup process of the main application.
64
     */
65
    // @ts-expect-error
66
    init(renderElement: string, router: Router, providers: { [key: string]: unknown }): Vue {
67
        if (this.vue3) {
68
            return this.initVue3(renderElement, router, providers);
69
        }
70
71
        return this.initVue2(renderElement, router as VueRouter, providers);
72
    }
73
74
    // @ts-expect-error
75
    initVue3(renderElement: string, router: Router, providers: { [key: string]: unknown }): Vue {
76
        this.initPlugins();
77
        this.initDirectives();
78
        this.initFilters();
79
        this.initTitle();
80
81
        const store = State._store;
82
        const i18n = this.initLocales(store) as VueI18n;
83
84
        // add router to View
85
        this.router = router as VueRouter;
86
        // add i18n to View
87
        this.i18n = i18n as I18n;
88
89
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
90
        this.app.config.compilerOptions.whitespace = 'preserve';
91
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
92
        this.app.config.performance = process.env.NODE_ENV !== 'production';
93
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
94
        this.app.config.globalProperties.$t = i18n.global.t;
95
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
96
        this.app.config.globalProperties.$tc = i18n.global.tc;
97
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
98
        this.app.config.warnHandler = (
99
            msg: string,
100
            instance: unknown,
101
            trace: string,
102
        ) => {
103
            const warnArgs = [`[Vue warn]: ${msg}`, trace, instance];
104
105
            console.warn(...warnArgs);
106
107
            if (msg.includes('Template compilation error')) {
108
                console.error(...[`[Vue error]: ${msg}`, trace, instance]);
109
                throw new Error(msg);
110
            }
111
        };
112
113
        /**
114
         * This is a hack for providing the services to the components.
115
         * We shouldn't use this anymore because it is not supported well
116
         * in Vue3 (because the services are lazy loaded).
117
         *
118
         * So we should convert from provide/inject to Shopware.Service
119
         */
120
        Object.keys(providers).forEach((provideKey) => {
121
            // @ts-expect-error
122
            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
123
            Object.defineProperty(this.app._context.provides, provideKey, {
124
                get: () => providers[provideKey],
125
                enumerable: true,
126
                configurable: true,
127
                // eslint-disable-next-line @typescript-eslint/no-empty-function
128
                set() {},
129
            });
130
        });
131
132
        this.root = this.app;
133
134
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
135
        this.app.use(router);
136
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
137
        this.app.use(store);
138
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
139
        this.app.use(i18n);
140
141
        // Add global properties to root view instance
142
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
143
        this.app.$tc = i18n.global.tc;
144
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
145
        this.app.$t = i18n.global.t;
146
147
        /* eslint-disable max-len */
148
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
149
        this.app.config.globalProperties.$createTitle = function createTitle(this: Vue, identifier: string|null = null, ...additionalParams): string {
150
            if (!this.$root) {
151
                return '';
152
            }
153
154
            const baseTitle = this.$root.$tc('global.sw-admin-menu.textShopwareAdmin');
155
156
            if (!this.$route.meta || !this.$route.meta.$module) {
157
                return '';
158
            }
159
160
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
161
            const pageTitle = this.$root.$tc(this.$route.meta.$module.title);
162
163
            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
164
            const params = [baseTitle, pageTitle, identifier, ...additionalParams].filter((item) => {
165
                // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
166
                return item !== null && item.trim() !== '';
167
            });
168
169
            return params.reverse().join(' | ');
170
        };
171
        /* eslint-enable max-len */
172
173
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
174
        this.app.mount(renderElement);
175
176
        if (process.env.NODE_ENV === 'development') {
177
            setupShopwareDevtools(this.root);
178
        }
179
180
        return this.root;
181
    }
182
183
    initVue2(renderElement: string, router: VueRouter, providers: { [key: string]: unknown }): Vue {
184
        this.initPlugins();
185
        this.initDirectives();
186
        this.initFilters();
187
        this.initTitle();
188
189
        const store = State._store;
190
        const i18n = this.initLocales(store);
191
        const components = this.getComponents();
192
193
        // add router to View
194
        this.router = router;
195
        // add i18n to View
196
        this.i18n = i18n;
197
198
        // Enable performance measurements in development mode
199
        Vue.config.performance = process.env.NODE_ENV !== 'production';
200
201
        this.root = new Vue({
202
            el: renderElement,
203
            template: '<sw-admin />',
204
            router,
205
            store,
206
            i18n,
207
            provide() {
208
                /**
209
                 * Vue 2.7 creates a new copy for each provided value. This caused problems with bottlejs.
210
                 * There should be only one instance of each provided value. Therefore we use a getter wrapper.
211
                 */
212
                return Object.keys(providers).reduce<{
213
                    [key: string]: unknown
214
                }>((acc, provideKey) => {
215
                    Object.defineProperty(acc, provideKey, {
216
                        get: () => providers[provideKey],
217
                        enumerable: true,
218
                        configurable: true,
219
                        // eslint-disable-next-line @typescript-eslint/no-empty-function
220
                        set() {},
221
                    });
222
223
                    return acc;
224
                }, {});
225
            },
226
            components,
227
            data() {
228
                return {
229
                    initError: {},
230
                };
231
            },
232
        });
233
234
        if (process.env.NODE_ENV === 'development') {
235
            setupShopwareDevtools(this.root);
236
        }
237
238
        return this.root;
239
    }
240
241
    /**
242
     * Initialize of all dependencies.
243
     */
244
    async initDependencies() {
245
        const initContainer = this.Application.getContainer('init');
246
247
        // make specific components synchronous
248
        [
249
            'sw-admin',
250
            'sw-admin-menu',
251
            'sw-button',
252
            'sw-button-process',
253
            'sw-card',
254
            'sw-card-section',
255
            'sw-card-view',
256
            'sw-container',
257
            'sw-desktop',
258
            'sw-empty-state',
259
            'sw-entity-listing',
260
            'sw-entity-multi-select',
261
            'sw-entity-multi-id-select',
262
            'sw-entity-single-select',
263
            'sw-error-boundary',
264
            'sw-extension-component-section',
265
            'sw-field',
266
            'sw-ignore-class',
267
            'sw-loader',
268
            'sw-modal',
269
            'sw-multi-select',
270
            'sw-notification-center',
271
            'sw-notifications',
272
            'sw-page',
273
            'sw-router-link',
274
            'sw-search-bar',
275
            'sw-select-result',
276
            'sw-single-select',
277
            'sw-skeleton',
278
            'sw-skeleton-bar',
279
            'sw-tabs',
280
            'sw-tabs-item',
281
            'sw-version',
282
            /**
283
             * Quickfix for modules with refs and sync behavior.
284
             * They should be removed from the list in the future
285
             * when their async problems got fixed.
286
             */
287
            'sw-sales-channel-products-assignment-single-products',
288
            'sw-sales-channel-product-assignment-categories',
289
            'sw-sales-channel-products-assignment-dynamic-product-groups',
290
            'sw-upload-listener',
291
            'sw-media-list-selection-v2',
292
            'sw-media-list-selection-item-v2',
293
            'sw-settings-document-detail',
294
            'sw-settings-product-feature-sets-detail',
295
            'sw-system-config',
296
            'sw-settings-search-searchable-content',
297
        ].forEach(componentName => {
298
            Component.markComponentAsSync(componentName);
299
        });
300
301
        // initialize all components
302
        await this.initComponents();
303
304
        // initialize all module locales
305
        this.initModuleLocales();
306
307
        // initialize all module routes
308
        const allRoutes = this.applicationFactory.module.getModuleRoutes();
309
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
310
        initContainer.router.addModuleRoutes(allRoutes);
311
312
        // create routes for core and plugins
313
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
314
        initContainer.router.createRouterInstance();
315
    }
316
317
318
    /**
319
     * Initializes all core components as Vue components.
320
     */
321
    async initComponents() {
322
        const componentRegistry = this.componentFactory.getComponentRegistry();
323
        this.componentFactory.resolveComponentTemplates();
324
325
        const initializedComponents = [...componentRegistry.keys()].map((name) => {
326
            return this.createComponent(name);
327
        });
328
329
        await Promise.all(initializedComponents);
330
331
        return this.vueComponents;
332
    }
333
334
    /**
335
     * Initializes all core components as Vue components.
336
     */
337
    initModuleLocales() {
338
        // Extend default snippets with module specific snippets
339
        const moduleSnippets = this.applicationFactory.module.getModuleSnippets();
340
341
        Object.entries(moduleSnippets).forEach(([key, moduleSnippet]) => {
342
            this.applicationFactory.locale.extend(key, moduleSnippet);
343
        });
344
345
        return this.applicationFactory.locale;
346
    }
347
348
    /**
349
     * Returns the component as a Vue component.
350
     * Includes the full rendered template with all overrides.
351
     */
352
    createComponent(componentName: string): Promise<Vue> {
353
        return new Promise((resolve) => {
354
            // load sync components directly
355
            if (Component.isSyncComponent && Component.isSyncComponent(componentName)) {
356
                const resolvedComponent = this.componentResolver(componentName);
357
358
                if (resolvedComponent === undefined) {
359
                    return;
360
                }
361
362
                // eslint-disable-next-line @typescript-eslint/no-unsafe-call
363
                void resolvedComponent.then((component) => {
364
                    let vueComponent;
365
366
                    if (this.vue3) {
367
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-call
368
                        this.app.component(componentName, component);
369
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
370
                        vueComponent = this.app.component(componentName);
371
                    } else {
372
                        // @ts-expect-error - resolved config does not match completely a standard vue component
373
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
374
                        vueComponent = Vue.component(componentName, component);
375
                    }
376
377
378
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
379
                    this.vueComponents[componentName] = vueComponent;
380
                    resolve(vueComponent as unknown as Vue);
381
                });
382
383
                return;
384
            }
385
386
            // load async components
387
            let vueComponent;
388
            if (this.vue3) {
389
                // eslint-disable-next-line @typescript-eslint/no-unsafe-call
390
                this.app.component(componentName, defineAsyncComponent({
391
                    // the loader function
392
                    loader: () => this.componentResolver(componentName),
393
                    // Delay before showing the loading component. Default: 200ms.
394
                    delay: 0,
395
                    loadingComponent: {
396
                        name: 'async-loading-component',
397
                        inheritAttrs: false,
398
                        render() {
399
                            return h('div', {
400
                                style: { display: 'none' },
401
                            });
402
                        },
403
                    },
404
                }));
405
406
                // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
407
                vueComponent = this.app.component(componentName);
408
            } else {
409
                vueComponent = Vue.component(componentName, () => this.componentResolver(componentName));
410
            }
411
412
            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
413
            this.vueComponents[componentName] = vueComponent;
414
415
            resolve(vueComponent as unknown as Vue);
416
        });
417
    }
418
419
    componentResolver(componentName: string) {
420
        if (!this.resolvedComponentConfigs.has(componentName)) {
421
            this.resolvedComponentConfigs.set(componentName, new Promise((resolve) => {
422
                void Component.build(componentName).then((componentConfig) => {
423
                    this.resolveMixins(componentConfig);
424
425
                    resolve(componentConfig);
426
                });
427
            }));
428
        }
429
430
        return this.resolvedComponentConfigs.get(componentName);
431
    }
432
433
    /**
434
     * Builds and creates a Vue component using the provided component configuration.
435
     */
436
    buildAndCreateComponent(componentConfig: ComponentConfig) {
437
        if (!componentConfig.name) {
438
            throw new Error('Component name is missing');
439
        }
440
441
        const componentName = componentConfig.name;
442
        this.resolveMixins(componentConfig);
443
444
        // @ts-expect-error - resolved config does not match completely a standard vue component
445
        const vueComponent = Vue.component(componentConfig.name, componentConfig);
446
        this.vueComponents[componentName] = vueComponent;
447
448
        return vueComponent;
449
    }
450
451
    /**
452
     * Returns a final Vue component by its name.
453
     */
454
    getComponent(componentName: string) {
455
        if (!this.vueComponents[componentName]) {
456
            return null;
457
        }
458
459
        return this.vueComponents[componentName] as Vue;
460
    }
461
462
    /**
463
     * Returns a final Vue component by its name without defineAsyncComponent
464
     * which cannot be used in the router.
465
     */
466
    getComponentForRoute(componentName: string) {
467
        return () => this.componentResolver(componentName);
468
    }
469
470
    /**
471
     * Returns the complete set of available Vue components.
472
     */
473
    // @ts-expect-error - resolved config for each component does not match completely a standard vue component
474
    getComponents() {
475
        return this.vueComponents;
476
    }
477
478
    /**
479
     * Returns the adapter wrapper
480
     */
481
    getWrapper() {
482
        return Vue;
483
    }
484
485
    /**
486
     * Returns the name of the adapter
487
     */
488
    getName(): string {
489
        return 'Vue.js';
490
    }
491
492
    /**
493
     * Returns the Vue.set function
494
     */
495
    setReactive(target: Vue, propertyName: string, value: unknown) {
496
        return Vue.set(target, propertyName, value);
497
    }
498
499
    /**
500
     * Returns the Vue.delete function
501
     */
502
    deleteReactive(target: Vue, propertyName: string) {
503
        return Vue.delete(target, propertyName);
504
    }
505
506
    /**
507
     * Private methods
508
     */
509
510
    /**
511
     * Initialises all plugins for VueJS
512
     *
513
     * @private
514
     */
515
    initPlugins() {
516
        // placeholder variable because import is not filterable
517
        let plugins = VuePlugins;
518
519
        if (!this.vue3) {
520
            // Add the community plugins to the plugin list
521
            plugins.push(VueRouter, VueI18n, VueMeta);
522
523
            // Remove our meta info plugin because we use the vue-meta plugin with Vue 2
524
            plugins = plugins.filter((plugin) => {
525
                // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
526
                return !plugin?.isMetaInfoPluginInstalled;
527
            });
528
        }
529
530
        VuePlugins.forEach((plugin) => {
531
            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
532
            if (plugin?.install?.installed) {
533
                return;
534
            }
535
536
            if (this.vue3) {
537
                // eslint-disable-next-line @typescript-eslint/no-unsafe-call
538
                this.app.use(plugin as PluginObject<unknown>);
539
            } else {
540
                Vue.use(plugin as PluginObject<unknown>);
541
            }
542
        });
543
544
        return true;
545
    }
546
547
    /**
548
     * Initializes all custom directives.
549
     *
550
     * @private
551
     */
552
    initDirectives() {
553
        const registry = this.Application.getContainer('factory').directive.getDirectiveRegistry();
554
555
        registry.forEach((directive, name) => {
556
            Vue.directive(name, directive);
557
        });
558
559
        return true;
560
    }
561
562
    /**
563
     * Initialises helpful filters for global use
564
     *
565
     * @private
566
     */
567
    initFilters() {
568
        const registry = this.Application.getContainer('factory').filter.getRegistry();
569
570
        registry.forEach((factoryMethod, name) => {
571
            Vue.filter(name, factoryMethod);
572
        });
573
574
        return true;
575
    }
576
577
    /**
578
     * Initialises the standard locales.
579
     */
580
    initLocales(store: Store<VuexRootState>) {
581
        const registry = this.localeFactory.getLocaleRegistry();
582
        const messages = {};
583
        const fallbackLocale = Shopware.Context.app.fallbackLocale as FallbackLocale;
584
585
        registry.forEach((localeMessages, key) => {
586
            store.commit('registerAdminLocale', key);
587
            // @ts-expect-error - key is safe because we iterate through the registry
588
            messages[key] = localeMessages;
589
        });
590
591
        const lastKnownLocale = this.localeFactory.getLastKnownLocale();
592
        void store.dispatch('setAdminLocale', lastKnownLocale);
593
594
        const options = {
595
            locale: lastKnownLocale,
596
            fallbackLocale,
597
            silentFallbackWarn: true,
598
            sync: true,
599
            messages,
600
        };
601
602
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
603
        const i18n = window._features_?.vue3 ? createI18n(options) : new VueI18n(options);
604
605
        store.subscribe(({ type }, state) => {
606
            if (type === 'setAdminLocale') {
607
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
608
                i18n.locale = state.session.currentLocale!;
609
            }
610
        });
611
612
        this.setLocaleFromUser(store);
613
614
        return i18n;
615
    }
616
617
    setLocaleFromUser(store: Store<VuexRootState>) {
618
        const currentUser = store.state.session.currentUser;
619
620
        if (currentUser) {
621
            const userLocaleId = currentUser.localeId;
622
            // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
623
            Shopware.Service('localeHelper').setLocaleWithId(userLocaleId);
624
        }
625
    }
626
627
    /**
628
     * Extends Vue prototype to access $createTitle function
629
     *
630
     * @private
631
     */
632
    initTitle() {
633
        // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
634
        if (Vue.prototype.hasOwnProperty('$createTitle')) {
635
            return;
636
        }
637
638
        /**
639
         * Generates the document title out of the given VueComponent and parameters
640
         */
641
        // @ts-expect-error - additionalParams is not typed
642
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,max-len
643
        Vue.prototype.$createTitle = function createTitle(this: Vue, identifier: string|null = null, ...additionalParams): string {
644
            if (!this.$root) {
645
                return '';
646
            }
647
648
            const baseTitle = this.$root.$tc('global.sw-admin-menu.textShopwareAdmin');
649
650
            if (!this.$route.meta || !this.$route.meta.$module) {
651
                return '';
652
            }
653
654
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
655
            const pageTitle = this.$root.$tc(this.$route.meta.$module.title);
656
657
            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
658
            const params = [baseTitle, pageTitle, identifier, ...additionalParams].filter((item) => {
659
                // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
660
                return item !== null && item.trim() !== '';
661
            });
662
663
            return params.reverse().join(' | ');
664
        };
665
    }
666
667
    /**
668
     * Recursively resolves mixins referenced by name
669
     *
670
     * @private
671
     */
672
    resolveMixins(componentConfig: ComponentConfig) {
673
        // If the mixin is a string, use our mixin registry
674
        if (componentConfig.mixins?.length) {
675
            componentConfig.mixins = componentConfig.mixins.map((mixin) => {
676
                if (typeof mixin === 'string') {
677
                    return Mixin.getByName(mixin);
678
                }
679
680
                return mixin;
681
            });
682
        }
683
684
        if (componentConfig.extends) {
685
            // @ts-expect-error - extends can be a string or a component config
686
            this.resolveMixins(componentConfig.extends);
687
        }
688
    }
689
}
690