diff options
Diffstat (limited to 'app/assets/javascripts/lib/utils/vue3compat')
7 files changed, 274 insertions, 0 deletions
diff --git a/app/assets/javascripts/lib/utils/vue3compat/compat_functional_mixin.js b/app/assets/javascripts/lib/utils/vue3compat/compat_functional_mixin.js new file mode 100644 index 00000000000..ec8feb7d2e6 --- /dev/null +++ b/app/assets/javascripts/lib/utils/vue3compat/compat_functional_mixin.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; + +export const compatFunctionalMixin = Vue.version.startsWith('3') + ? { + created() { + this.props = this.$props; + this.listeners = this.$listeners; + }, + } + : { + created() { + throw new Error('This mixin should not be executed in Vue.js 2'); + }, + }; diff --git a/app/assets/javascripts/lib/utils/vue3compat/get_instance_from_directive.js b/app/assets/javascripts/lib/utils/vue3compat/get_instance_from_directive.js new file mode 100644 index 00000000000..b69f5e0c546 --- /dev/null +++ b/app/assets/javascripts/lib/utils/vue3compat/get_instance_from_directive.js @@ -0,0 +1,9 @@ +// See https://v3-migration.vuejs.org/breaking-changes/custom-directives.html#edge-case-accessing-the-component-instance +export function getInstanceFromDirective({ binding, vnode }) { + if (binding.instance) { + // this is Vue.js 3, even in compat mode + return binding.instance; + } + + return vnode.context; +} diff --git a/app/assets/javascripts/lib/utils/vue3compat/mark_raw.js b/app/assets/javascripts/lib/utils/vue3compat/mark_raw.js new file mode 100644 index 00000000000..daafbad8ba1 --- /dev/null +++ b/app/assets/javascripts/lib/utils/vue3compat/mark_raw.js @@ -0,0 +1,9 @@ +// this will be replaced by markRaw from vue.js v3 +export function markRaw(obj) { + Object.defineProperty(obj, '__v_skip', { + value: true, + configurable: true, + }); + + return obj; +} diff --git a/app/assets/javascripts/lib/utils/vue3compat/normalize_children.js b/app/assets/javascripts/lib/utils/vue3compat/normalize_children.js new file mode 100644 index 00000000000..616bd4786a9 --- /dev/null +++ b/app/assets/javascripts/lib/utils/vue3compat/normalize_children.js @@ -0,0 +1,11 @@ +export function normalizeChildren(children) { + if (typeof children !== 'object' || Array.isArray(children)) { + return children; + } + + if (typeof children.default === 'function') { + return children.default(); + } + + return children; +} diff --git a/app/assets/javascripts/lib/utils/vue3compat/vue_apollo.js b/app/assets/javascripts/lib/utils/vue3compat/vue_apollo.js new file mode 100644 index 00000000000..fd08d34a80e --- /dev/null +++ b/app/assets/javascripts/lib/utils/vue3compat/vue_apollo.js @@ -0,0 +1,78 @@ +import Vue from 'vue'; +import { createApolloProvider } from '@vue/apollo-option'; +import { ApolloMutation } from '@vue/apollo-components'; + +export { ApolloMutation }; + +const installed = new WeakMap(); + +function callLifecycle(hookName, ...extraArgs) { + const { GITLAB_INTERNAL_ADDED_MIXINS: addedMixins } = this.$; + if (!addedMixins) { + return []; + } + + return addedMixins.map((m) => m[hookName]?.apply(this, extraArgs)); +} + +function createMixinForLateInit({ install, shouldInstall }) { + return { + created() { + callLifecycle.call(this, 'created'); + }, + // @vue/compat normalizez lifecycle hook names so there is no error here + destroyed() { + callLifecycle.call(this, 'unmounted'); + }, + + data(...args) { + const extraData = callLifecycle.call(this, 'data', ...args); + if (!extraData.length) { + return {}; + } + + return Object.assign({}, ...extraData); + }, + + beforeCreate() { + if (shouldInstall(this)) { + const { mixins } = this.$.appContext; + const globalMixinsBeforeInit = new Set(mixins); + install(this); + + this.$.GITLAB_INTERNAL_ADDED_MIXINS = mixins.filter((m) => !globalMixinsBeforeInit.has(m)); + + callLifecycle.call(this, 'beforeCreate'); + } + }, + }; +} + +export default class VueCompatApollo { + constructor(...args) { + // eslint-disable-next-line no-constructor-return + return createApolloProvider(...args); + } + + static install() { + Vue.mixin( + createMixinForLateInit({ + shouldInstall: (vm) => + vm.$options.apolloProvider && + !installed.get(vm.$.appContext.app)?.has(vm.$options.apolloProvider), + install: (vm) => { + const { app } = vm.$.appContext; + const { apolloProvider } = vm.$options; + + if (!installed.has(app)) { + installed.set(app, new WeakSet()); + } + + installed.get(app).add(apolloProvider); + + vm.$.appContext.app.use(vm.$options.apolloProvider); + }, + }), + ); + } +} diff --git a/app/assets/javascripts/lib/utils/vue3compat/vue_router.js b/app/assets/javascripts/lib/utils/vue3compat/vue_router.js new file mode 100644 index 00000000000..aa2963ece31 --- /dev/null +++ b/app/assets/javascripts/lib/utils/vue3compat/vue_router.js @@ -0,0 +1,115 @@ +import Vue from 'vue'; +import { + createRouter, + createMemoryHistory, + createWebHistory, + createWebHashHistory, +} from 'vue-router-vue3'; + +const mode = (value, options) => { + if (!value) return null; + let history; + // eslint-disable-next-line default-case + switch (value) { + case 'history': + history = createWebHistory(options.base); + break; + case 'hash': + history = createWebHashHistory(); + break; + case 'abstract': + history = createMemoryHistory(); + break; + } + return { history }; +}; + +const base = () => null; + +const toNewCatchAllPath = (path) => { + if (path === '*') return '/:pathMatch(.*)*'; + return path; +}; + +const routes = (value) => { + if (!value) return null; + const newRoutes = value.reduce(function handleRoutes(acc, route) { + const newRoute = { + ...route, + path: toNewCatchAllPath(route.path), + }; + if (route.children) { + newRoute.children = route.children.reduce(handleRoutes, []); + } + acc.push(newRoute); + return acc; + }, []); + return { routes: newRoutes }; +}; + +const scrollBehavior = (value) => { + return { + scrollBehavior(...args) { + const { x, y, left, top } = value(...args); + return { left: x || left, top: y || top }; + }, + }; +}; + +const transformers = { + mode, + base, + routes, + scrollBehavior, +}; + +const transformOptions = (options = {}) => { + const defaultConfig = { + routes: [], + history: createWebHashHistory(), + }; + return Object.keys(options).reduce((acc, key) => { + const value = options[key]; + if (key in transformers) { + Object.assign(acc, transformers[key](value, options)); + } else { + acc[key] = value; + } + return acc; + }, defaultConfig); +}; + +const installed = new WeakMap(); + +export default class VueRouterCompat { + constructor(options) { + // eslint-disable-next-line no-constructor-return + return new Proxy(createRouter(transformOptions(options)), { + get(target, prop) { + const result = target[prop]; + // eslint-disable-next-line no-underscore-dangle + if (result?.__v_isRef) { + return result.value; + } + + return result; + }, + }); + } + + static install() { + Vue.mixin({ + beforeCreate() { + const { app } = this.$.appContext; + const { router } = this.$options; + if (router && !installed.get(app)?.has(router)) { + if (!installed.has(app)) { + installed.set(app, new WeakSet()); + } + installed.get(app).add(router); + this.$.appContext.app.use(this.$options.router); + } + }, + }); + } +} diff --git a/app/assets/javascripts/lib/utils/vue3compat/vuex.js b/app/assets/javascripts/lib/utils/vue3compat/vuex.js new file mode 100644 index 00000000000..ff94ff3d04a --- /dev/null +++ b/app/assets/javascripts/lib/utils/vue3compat/vuex.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import { + createStore, + mapState, + mapGetters, + mapActions, + mapMutations, + createNamespacedHelpers, +} from 'vuex-vue3'; + +export { mapState, mapGetters, mapActions, mapMutations, createNamespacedHelpers }; + +const installedStores = new WeakMap(); + +export default { + Store: class VuexCompatStore { + constructor(...args) { + // eslint-disable-next-line no-constructor-return + return createStore(...args); + } + }, + + install() { + Vue.mixin({ + beforeCreate() { + const { app } = this.$.appContext; + const { store } = this.$options; + if (store && !installedStores.get(app)?.has(store)) { + if (!installedStores.has(app)) { + installedStores.set(app, new WeakSet()); + } + installedStores.get(app).add(store); + this.$.appContext.app.use(this.$options.store); + } + }, + }); + }, +}; |