diff --git a/docs/pages/en/6.API/3.callWithContext.md b/docs/pages/en/6.API/3.callWithContext.md new file mode 100644 index 00000000..840c33b2 --- /dev/null +++ b/docs/pages/en/6.API/3.callWithContext.md @@ -0,0 +1,51 @@ +--- +title: callWithContext +description: 'Ensures Context will always be available inside called Composable' +--- + +This function equals one of Nuxt Bridge/Nuxt 3 internals: `callWithNuxt`. +It accepts `useContext()` response as first argument, function-to-call as second and function's arguments as third. +When you call this function, you can always be sure that function-to-call will have access to `useContext`, `useRoute`, e.t.c. methods. + +Example of usage with useAsync: + +```ts +import { + defineComponent, + useContext, + callWithContext, +} from '@nuxtjs/composition-api' +import { useMyComposable, useSecondComposable } from '../composables' + +export default defineComponent({ + setup() { + const context = useContext() + + useAsync(async () => { + try { + //Context is lost after you call first await on SSR + const firstAwait = await useMyComposable({ option: true }) + //This one depends on firstAwait and calls useContext inside of it + const secondAwait = await callWithContext( + context, + useSecondComposable, + [{ option: true }] + ) + + return { + firstAwait, + secondAwait, + } + } catch (e) { + //Wait for logging system response etc + await callWithContext(context, useProcessError, [{ error: e }]) + throw e + } + }) + }, +}) +``` + +:::alert{type="info"} +Note: after first call of useContext on Client Side context will be stored as global. You will not have to call function to access returned in setup composables on client side. +::: diff --git a/docs/pages/en/6.API/3.useStatic.md b/docs/pages/en/6.API/4.useStatic.md similarity index 100% rename from docs/pages/en/6.API/3.useStatic.md rename to docs/pages/en/6.API/4.useStatic.md diff --git a/docs/pages/en/6.API/4.wrap.md b/docs/pages/en/6.API/5.wrap.md similarity index 100% rename from docs/pages/en/6.API/4.wrap.md rename to docs/pages/en/6.API/5.wrap.md diff --git a/package.json b/package.json index 2751119c..2d05a309 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,8 @@ "fs-extra": "^9.1.0", "magic-string": "^0.27.0", "pathe": "^1.0.0", - "ufo": "^1.0.1" + "ufo": "^1.0.1", + "unctx": "^2.1.2" }, "devDependencies": { "@babel/traverse": "^7.20.5", @@ -102,12 +103,14 @@ "tsd": "^0.25.0", "typescript": "4.9.4", "vitest": "^0.25.7", + "vue-router": "^3.6.5", "yorkie": "^2.0.0" }, "peerDependencies": { "@nuxt/vue-app": "^2.15", "nuxt": "^2.15", - "vue": "^2.7.14" + "vue": "^2.7.14", + "vue-router": "^3.6" }, "engines": { "node": ">=v14.13.0" diff --git a/src/runtime/composables/context.ts b/src/runtime/composables/context.ts index 44ae1fab..7e1f1f2d 100644 --- a/src/runtime/composables/context.ts +++ b/src/runtime/composables/context.ts @@ -3,9 +3,12 @@ import { computed } from 'vue' import type { Ref } from 'vue' import type { Context } from '@nuxt/types' import type { Route } from 'vue-router' +import { useRoute } from 'vue-router/composables' +import { getContext } from 'unctx' import { globalNuxt } from '@nuxtjs/composition-api/dist/runtime/globals' import { getCurrentInstance } from './utils' +import { Vue } from 'vue/types/vue' interface ContextCallback { (context: Context): void @@ -30,6 +33,25 @@ interface UseContextReturn params: Ref } +const nuxtCtx = getContext('nuxt-context') + +/** + * Ensures that the setup function passed in has access to the Nuxt instance via `useContext`. + * + * @param context useContext response + * @param setup The function to call + * @param args Function's arguments + */ +export function callWithContext any>( + context: UseContextReturn, + setup: T, + args?: Parameters +) { + const fn: () => ReturnType = () => + args ? setup(...(args as Parameters)) : setup() + return nuxtCtx.callAsync(context, fn) +} + /** * `useContext` will return the Nuxt context. * @example @@ -45,26 +67,42 @@ interface UseContextReturn ``` */ export const useContext = (): UseContextReturn => { - const vm = getCurrentInstance() - if (!vm) throw new Error('This must be called within a setup function.') + const nuxtContext = nuxtCtx.tryUse() + + if (!nuxtContext) { + const vm = getCurrentInstance() + if (!vm) { + throw new Error('This must be called within a setup function.') + } + + const root = vm.$root as unknown as { _$route: typeof vm.$root['$route'] } - return { - ...(vm[globalNuxt] || vm.$options).context, - /** - * @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `route` from `useContext` but rather to use the `useRoute` helper function. - */ - route: computed(() => vm.$route), - /** - * @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `query` from `useContext` but rather to use the `useRoute` helper function. - */ - query: computed(() => vm.$route.query), - /** - * @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `from` from `useContext` but rather to use the `useRoute` helper function. - */ - from: computed(() => (vm[globalNuxt] || vm.$options).context.from), - /** - * @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `params` from `useContext` but rather to use the `useRoute` helper function. - */ - params: computed(() => vm.$route.params), + // Call of vue-router initialization of _$route + if (!root._$route) useRoute() + + const context = { + ...(vm[globalNuxt] || vm.$options).context, + /** + * @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `route` from `useContext` but rather to use the `useRoute` helper function. + */ + route: computed(() => root._$route), + /** + * @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `query` from `useContext` but rather to use the `useRoute` helper function. + */ + query: computed(() => root._$route.query), + /** + * @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `from` from `useContext` but rather to use the `useRoute` helper function. + */ + from: computed(() => (vm[globalNuxt] || vm.$options).context.from), + /** + * @deprecated To smooth your upgrade to Nuxt 3, it is recommended not to access `params` from `useContext` but rather to use the `useRoute` helper function. + */ + params: computed(() => root._$route.params), + } + + if (process.client) nuxtCtx.set(context) + return context } + + return nuxtContext } diff --git a/src/runtime/composables/index.ts b/src/runtime/composables/index.ts index 18a31130..71cbb234 100644 --- a/src/runtime/composables/index.ts +++ b/src/runtime/composables/index.ts @@ -1,6 +1,6 @@ export { useAsync } from './async' export { defineComponent } from './component' -export { useContext, withContext } from './context' +export { callWithContext, useContext, withContext } from './context' export * from './defineHelpers' export { useFetch } from './fetch' export { globalPlugin, onGlobalSetup, setMetaPlugin } from './hooks' @@ -8,6 +8,13 @@ export { useMeta } from './meta' export { reqRef, reqSsrRef } from './req-ref' export { ssrRef, shallowSsrRef, setSSRContext, ssrPromise } from './ssr-ref' export { useStatic } from './static' -export { useRoute, useRouter, useStore, wrapProperty } from './wrappers' +export { + useRoute, + useRouter, + useStore, + wrapProperty, + wrapContextProperty, + useRedirect, +} from './wrappers' export * from './vue' diff --git a/src/runtime/composables/wrappers.ts b/src/runtime/composables/wrappers.ts index d6821a27..35b77cde 100644 --- a/src/runtime/composables/wrappers.ts +++ b/src/runtime/composables/wrappers.ts @@ -1,7 +1,10 @@ -import { computed, ComputedRef, InjectionKey } from 'vue' +import { computed, ComputedRef, InjectionKey, isRef } from 'vue' import type { Store } from 'vuex' +import { useContext } from './context' import { getCurrentInstance } from './utils' +import { Context } from '@nuxt/types' +import { useRouter as useVueRouter } from 'vue-router/composables' /** * You might want to create a custom helper to 'convert' a non-Composition API property to a Composition-ready one. `wrapProperty` enables you to do that easily, returning either a computed or a bare property as required. @@ -27,6 +30,27 @@ export const wrapProperty = < } } +/** + * You might want to create a custom helper to 'convert' a non-Composition Context property to a Composition-ready one. `wrapProperty` enables you to do that easily, returning either a computed or a bare property as required. + * @param property the name of the property you would like to access. For example, `store` to access `context.store`. + * @param makeComputed a boolean indicating whether the helper function should return a computed property or not. Defaults to `true`. + */ +export const wrapContextProperty = < + K extends keyof Context, + T extends boolean = true +>( + property: K, + makeComputed?: T +) => { + return (): T extends true ? ComputedRef : Context[K] => { + const context = useContext() + + return makeComputed !== false && !isRef(context[property]) + ? (computed(() => context[property]) as any) + : context[property] + } +} + /** * Gain access to the router just like using this.$router in a non-Composition API manner. * @example @@ -41,7 +65,30 @@ export const wrapProperty = < }) ``` */ -export const useRouter = wrapProperty('$router', false) +export const useRouter = (): VueRouter => { + const vm = getCurrentInstance() + if (vm) return useVueRouter() + + const contextRouter = useContext().app.router + if (contextRouter) return contextRouter + + throw new Error('This must be called within a setup function.') +} + +/** + * Gain safe access to the redirect method from Context + * @example + ```ts + import { defineComponent, useRedirect } from '@nuxtjs/composition-api' + + export default defineComponent({ + setup() { + useRedirect('/') + } + }) + ``` + */ +export const useRedirect = wrapContextProperty('redirect') /** * Returns `this.$route`, wrapped in a computed - so accessible from `.value`. @@ -57,7 +104,7 @@ export const useRouter = wrapProperty('$router', false) }) ``` */ -export const useRoute = wrapProperty('$route') +export const useRoute = wrapContextProperty('route') /** * Gain access to the store just like using this.$store in a non-Composition API manner. You can also provide an injection key or custom type to get back a semi-typed store: @@ -82,8 +129,5 @@ export const useRoute = wrapProperty('$route') ``` */ export const useStore = (key?: InjectionKey): Store => { - const vm = getCurrentInstance() - if (!vm) throw new Error('This must be called within a setup function.') - - return vm.$store + return useContext().store } diff --git a/yarn.lock b/yarn.lock index ac95e2b4..06d202aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2214,6 +2214,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.46.tgz#0fb6bfbbeabd7a30880504993369c4bf1deab1fe" integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg== +"@types/estree@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== + "@types/etag@1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@types/etag/-/etag-1.8.0.tgz#37f0b1f3ea46da7ae319bbedb607e375b4c99f7e" @@ -2925,6 +2930,11 @@ acorn@^8.8.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== +acorn@^8.8.2: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" @@ -3991,7 +4001,7 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.5.1, chokidar@^3.5.2: +chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.5.1, chokidar@^3.5.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -6018,6 +6028,13 @@ estree-walker@^2.0.1, estree-walker@^2.0.2: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -13515,6 +13532,16 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +unctx@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/unctx/-/unctx-2.1.2.tgz#12d34c540ef4fbaffb2a3b38a0697e42b152d478" + integrity sha512-KK18aLRKe3OlbPyHbXAkIWSU3xK8GInomXfA7fzDMGFXQ1crX1UWrCzKesVXeUyHIayHUrnTvf87IPCKMyeKTg== + dependencies: + acorn "^8.8.2" + estree-walker "^3.0.3" + magic-string "^0.27.0" + unplugin "^1.0.1" + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" @@ -13611,6 +13638,16 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +unplugin@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.1.0.tgz#96a14aa52d7637a56a88dec6baf4a73902f2db87" + integrity sha512-I8obQ8Rs/hnkxokRV6g8JKOQFgYNnTd9DL58vcSt5IJ9AkK8wbrtsnzD5hi4BJlvcY536JzfEXj9L6h7j559/A== + dependencies: + acorn "^8.8.2" + chokidar "^3.5.3" + webpack-sources "^3.2.3" + webpack-virtual-modules "^0.5.0" + unquote@^1.1.1, unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" @@ -13909,6 +13946,11 @@ vue-router@^3.5.1: resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.5.4.tgz#c453c0b36bc75554de066fefc3f2a9c3212aca70" integrity sha512-x+/DLAJZv2mcQ7glH2oV9ze8uPwcI+H+GgTgTmb5I55bCgY3+vXWIsqbYUzbBSZnwFHEJku4eoaH/x98veyymQ== +vue-router@^3.6.5: + version "3.6.5" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.6.5.tgz#95847d52b9a7e3f1361cb605c8e6441f202afad8" + integrity sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ== + vue-server-renderer@^2.6.12: version "2.7.4" resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.7.4.tgz#7a24713377af939511cb925b2e495548ac035cc4" @@ -14063,6 +14105,16 @@ webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack- source-list-map "^2.0.0" source-map "~0.6.1" +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack-virtual-modules@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c" + integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== + webpack@^4.46.0: version "4.46.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542"