]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
refactor: wip remove vue-demi
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 30 Jan 2025 16:28:46 +0000 (17:28 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 30 Jan 2025 16:28:46 +0000 (17:28 +0100)
22 files changed:
packages/docs/vite.config.ts
packages/nuxt/src/runtime/plugin.vue2.ts [deleted file]
packages/pinia/__tests__/storeToRefs.spec.ts
packages/pinia/package.json
packages/pinia/src/createPinia.ts
packages/pinia/src/devtools/formatting.ts
packages/pinia/src/devtools/plugin.ts
packages/pinia/src/hmr.ts
packages/pinia/src/mapHelpers.ts
packages/pinia/src/rootStore.ts
packages/pinia/src/store.ts
packages/pinia/src/storeToRefs.ts
packages/pinia/src/subscriptions.ts
packages/pinia/src/types.ts
packages/pinia/src/vue2-plugin.ts
packages/playground/src/views/AllStoresDispose.vue
packages/playground/vite.config.ts
packages/testing/package.json
packages/testing/src/testing.ts
packages/testing/tsup.config.ts
pnpm-lock.yaml
rollup.config.mjs

index a084967b65d013c700f3693983157b7c8db70605..3da0892c8c44d602b34584fb1c720dbfce976e86 100644 (file)
@@ -1,4 +1,4 @@
-import { defineConfig, Plugin } from 'vite'
+import { defineConfig, type Plugin } from 'vite'
 import _fs from 'fs'
 import path from 'path'
 // import TypeDocPlugin from './vite-typedoc-plugin'
@@ -24,7 +24,7 @@ export default defineConfig({
     __BROWSER__: 'true',
   },
   optimizeDeps: {
-    exclude: ['vue-demi', '@vueuse/shared', '@vueuse/core', 'pinia'],
+    exclude: ['@vueuse/shared', '@vueuse/core', 'pinia'],
   },
 })
 
diff --git a/packages/nuxt/src/runtime/plugin.vue2.ts b/packages/nuxt/src/runtime/plugin.vue2.ts
deleted file mode 100644 (file)
index 188c3f9..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-import _Vue2 from 'vue'
-import { createPinia, setActivePinia, PiniaVuePlugin } from 'pinia'
-
-// TODO: workaround that should probably be removed in the future
-const Vue = 'default' in _Vue2 ? (_Vue2 as any).default : _Vue2
-Vue.use(PiniaVuePlugin)
-
-export default (context: any, provide: any) => {
-  const pinia = createPinia()
-  context.app.pinia = pinia
-  setActivePinia(pinia)
-
-  // add access to `$nuxt`
-  pinia._p.push(({ store }) => {
-    // make it non enumerable so it avoids any serialization and devtools
-    Object.defineProperty(store, '$nuxt', { value: context })
-  })
-
-  if (import.meta.server) {
-    context.beforeNuxtRender((ctx: any) => {
-      ctx.nuxtState.pinia = pinia.state.value
-    })
-  } else if (context.nuxtState && context.nuxtState.pinia) {
-    pinia.state.value = context.nuxtState.pinia
-  }
-
-  // Inject $pinia
-  provide('pinia', pinia)
-}
-
-declare module 'pinia' {
-  export interface PiniaCustomProperties {
-    /**
-     * Nuxt context. Requires you to install `@nuxt/types` to have types.
-     *
-     * @deprecated use `useNuxtApp()` and global `$fetch()` instead. See
-     * https://v3.nuxtjs.org/bridge/bridge-composition-api/
-     */
-    // @ts-ignore: heavy types, must be added by the user
-    $nuxt: import('@nuxt/types').Context
-  }
-}
index 17b4a3540ebdefec176b49487da777d53a950337..d571f1b94a330cfb9ee4835f4588fe3bb56ceea9 100644 (file)
@@ -1,7 +1,6 @@
 import { describe, beforeEach, it, expect, vi } from 'vitest'
 import { computed, reactive, ref, ToRefs } from 'vue'
 import { createPinia, defineStore, setActivePinia, storeToRefs } from '../src'
-import { set } from 'vue-demi'
 
 describe('storeToRefs', () => {
   beforeEach(() => {
@@ -181,11 +180,10 @@ describe('storeToRefs', () => {
     const { double } = storeToRefs(store)
 
     // Assuming HMR operation
-    set(
-      store,
-      'double',
+    // @ts-expect-error: hmr
+    store.double =
+      //
       computed(() => 1)
-    )
 
     expect(double.value).toEqual(1)
   })
index 70f7952c82d10ae852c191faf04a7b19e0aa9d1b..678a75d5cec6a8c375fb465cc166634f32c913a6 100644 (file)
@@ -73,8 +73,7 @@
     "@vue/test-utils": "^2.4.6"
   },
   "dependencies": {
-    "@vue/devtools-api": "^6.6.3",
-    "vue-demi": "^0.14.10"
+    "@vue/devtools-api": "^6.6.3"
   },
   "peerDependencies": {
     "typescript": ">=4.4.4",
index 7749247a3dff310be2f8b81c1da4d0defcc546d1..41129cfa242b408a371bbb6e6b3b1d9217ea84e2 100644 (file)
@@ -1,5 +1,5 @@
 import { Pinia, PiniaPlugin, setActivePinia, piniaSymbol } from './rootStore'
-import { ref, App, markRaw, effectScope, isVue2, Ref } from 'vue-demi'
+import { ref, App, markRaw, effectScope, Ref } from 'vue'
 import { registerPiniaDevtools, devtoolsPlugin } from './devtools'
 import { IS_CLIENT } from './env'
 import { StateTree, StoreGeneric } from './types'
@@ -24,21 +24,19 @@ export function createPinia(): Pinia {
       // this allows calling useStore() outside of a component setup after
       // installing pinia's plugin
       setActivePinia(pinia)
-      if (!isVue2) {
-        pinia._a = app
-        app.provide(piniaSymbol, pinia)
-        app.config.globalProperties.$pinia = pinia
-        /* istanbul ignore else */
-        if (__USE_DEVTOOLS__ && IS_CLIENT) {
-          registerPiniaDevtools(app, pinia)
-        }
-        toBeInstalled.forEach((plugin) => _p.push(plugin))
-        toBeInstalled = []
+      pinia._a = app
+      app.provide(piniaSymbol, pinia)
+      app.config.globalProperties.$pinia = pinia
+      /* istanbul ignore else */
+      if (__USE_DEVTOOLS__ && IS_CLIENT) {
+        registerPiniaDevtools(app, pinia)
       }
+      toBeInstalled.forEach((plugin) => _p.push(plugin))
+      toBeInstalled = []
     },
 
     use(plugin) {
-      if (!this._a && !isVue2) {
+      if (!this._a) {
         toBeInstalled.push(plugin)
       } else {
         _p.push(plugin)
index 651c9ef3c66cc348dc6d5fdd2b413adc658485c3..ea55dce6d5d9677d65605529b525b1e85b9c0536 100644 (file)
@@ -4,7 +4,7 @@ import {
   CustomInspectorState,
 } from '@vue/devtools-api'
 import { MutationType, StoreGeneric } from '../types'
-import { DebuggerEvent } from 'vue-demi'
+import { DebuggerEvent } from 'vue'
 import { Pinia } from '../rootStore'
 import { isPinia } from './utils'
 
index 2c9fdd5c707418c0b0ad9f57a923d3be86060900..9f15af9fc9e0f43844a7816fbd2112e62c284016 100644 (file)
@@ -3,7 +3,7 @@ import {
   TimelineEvent,
   App as DevtoolsApp,
 } from '@vue/devtools-api'
-import { ComponentPublicInstance, markRaw, toRaw, unref, watch } from 'vue-demi'
+import { ComponentPublicInstance, markRaw, toRaw, unref, watch } from 'vue'
 import { Pinia, PiniaPluginContext } from '../rootStore'
 import {
   _GettersTree,
index b5798d49e9450ec8789649c9ac8c267dd8b5cc9a..c8ce9b8edb31204718595fbf8155f1be6b666e3a 100644 (file)
@@ -1,4 +1,4 @@
-import { isRef, isReactive, isVue2, set } from 'vue-demi'
+import { isRef, isReactive } from 'vue'
 import { Pinia } from './rootStore'
 import {
   isPlainObject,
@@ -53,11 +53,7 @@ export function patchObject(
     } else {
       // objects are either a bit more complex (e.g. refs) or primitives, so we
       // just set the whole thing
-      if (isVue2) {
-        set(newState, key, subPatch)
-      } else {
-        newState[key] = subPatch
-      }
+      newState[key] = subPatch
     }
   }
 
index aa7662cb099e39456baed107242914239bbba3c4..eb5290a8927c09560d10cfe6a53342efee5041f9 100644 (file)
@@ -1,4 +1,4 @@
-import type { ComponentPublicInstance, ComputedRef, UnwrapRef } from 'vue-demi'
+import type { ComponentPublicInstance, ComputedRef, UnwrapRef } from 'vue'
 import type {
   _GettersTree,
   _StoreWithGetters_Writable,
index 1b30996a5f58784725fc855ffa8e991c5691bf1f..78e75c6a2bfccdbeafa1c61cfdaaaae9773c875b 100644 (file)
@@ -5,7 +5,7 @@ import {
   hasInjectionContext,
   InjectionKey,
   Ref,
-} from 'vue-demi'
+} from 'vue'
 import {
   StateTree,
   PiniaCustomProperties,
index a09196a4e50ca73e18fc2e57e644ae6855cb35e3..2eb7021f90b573c52b75480213011e23bfe8bece 100644 (file)
@@ -19,11 +19,8 @@ import {
   toRefs,
   Ref,
   ref,
-  set,
-  del,
   nextTick,
-  isVue2,
-} from 'vue-demi'
+} from 'vue'
 import {
   StateTree,
   SubscriptionCallback,
@@ -166,11 +163,7 @@ function createOptionsStore<
   function setup() {
     if (!initialState && (!__DEV__ || !hot)) {
       /* istanbul ignore if */
-      if (isVue2) {
-        set(pinia.state.value, id, state ? state() : {})
-      } else {
-        pinia.state.value[id] = state ? state() : {}
-      }
+      pinia.state.value[id] = state ? state() : {}
     }
 
     // avoid creating a state in pinia.state.value
@@ -198,8 +191,6 @@ function createOptionsStore<
               const store = pinia._s.get(id)!
 
               // allow cross using stores
-              /* istanbul ignore if */
-              if (isVue2 && !store._r) return
 
               // @ts-expect-error
               // return getters![name].call(context, context)
@@ -250,7 +241,7 @@ function createSetupStore<
   // watcher options for $subscribe
   const $subscribeOptions: WatchOptions = { deep: true }
   /* istanbul ignore else */
-  if (__DEV__ && !isVue2) {
+  if (__DEV__) {
     $subscribeOptions.onTrigger = (event) => {
       /* istanbul ignore else */
       if (isListening) {
@@ -282,11 +273,7 @@ function createSetupStore<
   // by the setup
   if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
     /* istanbul ignore if */
-    if (isVue2) {
-      set(pinia.state.value, $id, {})
-    } else {
-      pinia.state.value[$id] = {}
-    }
+    pinia.state.value[$id] = {}
   }
 
   const hotState = ref({} as S)
@@ -478,12 +465,6 @@ function createSetupStore<
     $dispose,
   } as _StoreWithState<Id, S, G, A>
 
-  /* istanbul ignore if */
-  if (isVue2) {
-    // start as non ready
-    partialStore._r = false
-  }
-
   const store: Store<Id, S, G, A> = reactive(
     __DEV__ || (__USE_DEVTOOLS__ && IS_CLIENT)
       ? assign(
@@ -517,7 +498,7 @@ function createSetupStore<
     if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
       // mark it as a piece of state to be serialized
       if (__DEV__ && hot) {
-        set(hotState.value, key, toRef(setupStore, key))
+        hotState.value[key] = toRef(setupStore, key)
         // createOptionStore directly sets the state in pinia.state.value so we
         // can just skip that
       } else if (!isOptionsStore) {
@@ -532,12 +513,7 @@ function createSetupStore<
           }
         }
         // transfer the ref to the pinia state to keep everything in sync
-        /* istanbul ignore if */
-        if (isVue2) {
-          set(pinia.state.value[$id], key, prop)
-        } else {
-          pinia.state.value[$id][key] = prop
-        }
+        pinia.state.value[$id][key] = prop
       }
 
       /* istanbul ignore else */
@@ -549,13 +525,8 @@ function createSetupStore<
       const actionValue = __DEV__ && hot ? prop : action(prop as _Method, key)
       // this a hot module replacement store because the hotUpdate method needs
       // to do it with the right context
-      /* istanbul ignore if */
-      if (isVue2) {
-        set(setupStore, key, actionValue)
-      } else {
-        // @ts-expect-error
-        setupStore[key] = actionValue
-      }
+      // @ts-expect-error
+      setupStore[key] = actionValue
 
       /* istanbul ignore else */
       if (__DEV__) {
@@ -585,16 +556,10 @@ function createSetupStore<
 
   // add the state, getters, and action properties
   /* istanbul ignore if */
-  if (isVue2) {
-    Object.keys(setupStore).forEach((key) => {
-      set(store, key, setupStore[key])
-    })
-  } else {
-    assign(store, setupStore)
-    // allows retrieving reactive objects with `storeToRefs()`. Must be called after assigning to the reactive object.
-    // Make `storeToRefs()` work with `reactive()` #799
-    assign(toRaw(store), setupStore)
-  }
+  assign(store, setupStore)
+  // allows retrieving reactive objects with `storeToRefs()`. Must be called after assigning to the reactive object.
+  // Make `storeToRefs()` work with `reactive()` #799
+  assign(toRaw(store), setupStore)
 
   // use this instead of a computed with setter to be able to create it anywhere
   // without linking the computed lifespan to wherever the store is first
@@ -635,13 +600,15 @@ function createSetupStore<
         }
         // patch direct access properties to allow store.stateProperty to work as
         // store.$state.stateProperty
-        set(store, stateKey, toRef(newStore.$state, stateKey))
+        // @ts-expect-error: any type
+        store[stateKey] = toRef(newStore.$state, stateKey)
       })
 
       // remove deleted state properties
       Object.keys(store.$state).forEach((stateKey) => {
         if (!(stateKey in newStore.$state)) {
-          del(store, stateKey)
+          // @ts-expect-error: noop if doesn't exist
+          delete store[stateKey]
         }
       })
 
@@ -657,7 +624,10 @@ function createSetupStore<
       for (const actionName in newStore._hmrPayload.actions) {
         const actionFn: _Method = newStore[actionName]
 
-        set(store, actionName, action(actionFn, actionName))
+        // @ts-expect-error: actionName is a string
+        store[actionName] =
+          //
+          action(actionFn, actionName)
       }
 
       // TODO: does this work in both setup and option store?
@@ -671,20 +641,25 @@ function createSetupStore<
             })
           : getter
 
-        set(store, getterName, getterValue)
+        // @ts-expect-error: getterName is a string
+        store[getterName] =
+          //
+          getterValue
       }
 
       // remove deleted getters
       Object.keys(store._hmrPayload.getters).forEach((key) => {
         if (!(key in newStore._hmrPayload.getters)) {
-          del(store, key)
+          // @ts-expect-error: noop if doesn't exist
+          delete store[key]
         }
       })
 
       // remove old actions
       Object.keys(store._hmrPayload.actions).forEach((key) => {
         if (!(key in newStore._hmrPayload.actions)) {
-          del(store, key)
+          // @ts-expect-error: noop if doesn't exist
+          delete store[key]
         }
       })
 
@@ -715,12 +690,6 @@ function createSetupStore<
     )
   }
 
-  /* istanbul ignore if */
-  if (isVue2) {
-    // mark the store as ready before plugins
-    store._r = true
-  }
-
   // apply all plugins
   pinia._p.forEach((extender) => {
     /* istanbul ignore else */
index 3278b0c698f1f01221d7cbd5d8bea8dd24719195..45211ac0e5999bc4583e4c35ba6c3080c682b2d8 100644 (file)
@@ -3,14 +3,12 @@ import {
   ComputedRef,
   isReactive,
   isRef,
-  isVue2,
   toRaw,
   ToRef,
   toRef,
   ToRefs,
-  toRefs,
   WritableComputedRef,
-} from 'vue-demi'
+} from 'vue'
 import { StoreGetters, StoreState } from './store'
 import type {
   _ActionsTree,
@@ -89,37 +87,30 @@ export type StoreToRefs<SS extends StoreGeneric> =
 export function storeToRefs<SS extends StoreGeneric>(
   store: SS
 ): StoreToRefs<SS> {
-  // See https://github.com/vuejs/pinia/issues/852
-  // It's easier to just use toRefs() even if it includes more stuff
-  if (isVue2) {
-    // @ts-expect-error: toRefs include methods and others
-    return toRefs(store)
-  } else {
-    const rawStore = toRaw(store)
+  const rawStore = toRaw(store)
 
-    const refs = {} as StoreToRefs<SS>
-    for (const key in rawStore) {
-      const value = rawStore[key]
-      // There is no native method to check for a computed
-      // https://github.com/vuejs/core/pull/4165
-      if (value.effect) {
-        // @ts-expect-error: too hard to type correctly
-        refs[key] =
-          // ...
-          computed({
-            get: () => store[key],
-            set(value) {
-              store[key] = value
-            },
-          })
-      } else if (isRef(value) || isReactive(value)) {
-        // @ts-expect-error: the key is state or getter
-        refs[key] =
-          // ---
-          toRef(store, key)
-      }
+  const refs = {} as StoreToRefs<SS>
+  for (const key in rawStore) {
+    const value = rawStore[key]
+    // There is no native method to check for a computed
+    // https://github.com/vuejs/core/pull/4165
+    if (value.effect) {
+      // @ts-expect-error: too hard to type correctly
+      refs[key] =
+        // ...
+        computed({
+          get: () => store[key],
+          set(value) {
+            store[key] = value
+          },
+        })
+    } else if (isRef(value) || isReactive(value)) {
+      // @ts-expect-error: the key is state or getter
+      refs[key] =
+        // ---
+        toRef(store, key)
     }
-
-    return refs
   }
+
+  return refs
 }
index 876966d6316404e590c775faa22bbf09a213356f..49b2d7174d18d0d821eba10fe981d59161cf0e8e 100644 (file)
@@ -1,4 +1,4 @@
-import { getCurrentScope, onScopeDispose } from 'vue-demi'
+import { getCurrentScope, onScopeDispose } from 'vue'
 import { _Method } from './types'
 
 export const noop = () => {}
index 085bb39fb53c70b41838ce0a9cd88bd014927b09..79a188df71f13d9f381b7b3354d1f4ff6a78dba8 100644 (file)
@@ -5,7 +5,7 @@ import type {
   UnwrapRef,
   WatchOptions,
   WritableComputedRef,
-} from 'vue-demi'
+} from 'vue'
 import { Pinia } from './rootStore'
 
 /**
index fbb9aad2834e6fab9419f61bb5135190d0179bfb..2ca3d052ad6b76b7eff87b95261639adf5c3da28 100644 (file)
@@ -1,4 +1,4 @@
-import type { Plugin } from 'vue-demi'
+import type { Plugin } from 'vue'
 import { registerPiniaDevtools } from './devtools'
 import { IS_CLIENT } from './env'
 import { Pinia, piniaSymbol, setActivePinia } from './rootStore'
index 108625506c5ae34fddd8ff51b31afc99ad1dd063..a8e6e136654489c60cf8ab106789bcabcc406048 100644 (file)
@@ -15,7 +15,7 @@
 import { useUserStore } from '../stores/user'
 import { useCartStore } from '../stores/cart'
 import { useCounter } from '../stores/counter'
-import { onUnmounted } from 'vue-demi'
+import { onUnmounted } from 'vue'
 
 const userStore = useUserStore()
 const cartStore = useCartStore()
index 91da24a276aa2d41c4c4bc410749139a465e403d..5d474d30f3829b893f722a81d00c14ab2a113886 100644 (file)
@@ -12,17 +12,14 @@ export default defineConfig({
     __TEST__: 'false',
   },
   resolve: {
-    // alias: {
-    //   '@vue/composition-api': 'vue-demi',
-    // },
-    dedupe: ['vue-demi', 'vue', 'pinia'],
+    dedupe: ['vue', 'pinia'],
     alias: {
       // FIXME: use fileToUrl
       pinia: path.resolve(__dirname, '../pinia/src/index.ts'),
     },
   },
   optimizeDeps: {
-    exclude: ['vue-demi', '@vueuse/shared', '@vueuse/core', 'pinia'],
+    exclude: ['@vueuse/shared', '@vueuse/core', 'pinia'],
   },
 })
 
index 428e932f15c7fdc874657743c51702c0b55bcf71..9099d908f9918588034fc71cd5bb5d7f8c13c00f 100644 (file)
@@ -43,9 +43,6 @@
     "build": "tsup",
     "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . -l @pinia/testing -r 1"
   },
-  "dependencies": {
-    "vue-demi": "^0.14.10"
-  },
   "devDependencies": {
     "pinia": "workspace:*",
     "tsup": "^8.3.5"
index 978cf662718ea44f3ef0462f8130600e2c736509..964a07c8fa32f36d3e225630b2720bbe35c7b233 100644 (file)
@@ -1,15 +1,5 @@
-import {
-  App,
-  createApp,
-  customRef,
-  isReactive,
-  isRef,
-  isVue2,
-  set,
-  toRaw,
-  triggerRef,
-} from 'vue-demi'
-import type { ComputedRef, WritableComputedRef } from 'vue-demi'
+import { createApp, customRef, isReactive, isRef, toRaw, triggerRef } from 'vue'
+import type { App, ComputedRef, WritableComputedRef } from 'vue'
 import {
   Pinia,
   PiniaPlugin,
@@ -191,12 +181,10 @@ function mergeReactiveObjects<T extends StateTree>(
     ) {
       target[key] = mergeReactiveObjects(targetValue, subPatch)
     } else {
-      if (isVue2) {
-        set(target, key, subPatch)
-      } else {
-        // @ts-expect-error: subPatch is a valid value
-        target[key] = subPatch
-      }
+      // @ts-expect-error: subPatch is a valid value
+      target[key] =
+        //
+        subPatch
     }
   }
 
index fbf80f04b20882730bc57a17d974b9f9a67edcdb..dd73743d00a7ec0a5d59ac3a280aabe9172839e9 100644 (file)
@@ -5,7 +5,7 @@ export default defineConfig({
   clean: true,
   format: ['cjs', 'esm'],
   dts: true,
-  external: ['vue-demi', 'vue', 'pinia'],
+  external: ['vue', 'pinia'],
   tsconfig: './tsconfig.build.json',
   // onSuccess: 'npm run build:fix',
 })
index 9a897b03963b3eb36e67c60de971b2e91e619135..851852e126e2f546b90a4352dab6e2bae47733b1 100644 (file)
@@ -203,9 +203,6 @@ importers:
       vue:
         specifier: ^2.7.0 || ^3.5.11
         version: 3.5.13(typescript@5.6.3)
-      vue-demi:
-        specifier: ^0.14.10
-        version: 0.14.10(@vue/composition-api@1.7.2(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))
     devDependencies:
       '@microsoft/api-extractor':
         specifier: 7.48.0
@@ -256,10 +253,6 @@ importers:
         version: 1.0.5
 
   packages/testing:
-    dependencies:
-      vue-demi:
-        specifier: ^0.14.10
-        version: 0.14.10(@vue/composition-api@1.7.2(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))
     devDependencies:
       pinia:
         specifier: workspace:*
index e829bb16655ee963449584945a2a57ccf371ce16..c29aca1918e4a673ba87f023f3544d5a5ec39eda 100644 (file)
@@ -94,7 +94,6 @@ function createConfig(buildName, output, plugins = []) {
   output.banner = banner
   output.externalLiveBindings = false
   output.globals = {
-    'vue-demi': 'VueDemi',
     vue: 'Vue',
   }
 
@@ -126,7 +125,7 @@ function createConfig(buildName, output, plugins = []) {
   // during a single build.
   hasTSChecked = true
 
-  const external = ['vue-demi', 'vue']
+  const external = ['vue']
   if (
     !isGlobalBuild &&
     // pinia.prod.cjs should not require `@vue/devtools-api` (like Vue)