diff --git a/package.json b/package.json index 8268368..a54eaea 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,10 @@ "@trpc/client": "^10.0.0-proxy-beta.21", "@trpc/server": "^10.0.0-proxy-beta.21" }, + "peerDependencies": { + "@trpc/client": "<10.0.0", + "@trpc/server": "<10.0.0" + }, "dependencies": { "nuxt": "^3.0.0-rc.13", "h3": "^0.8.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 714f8d0..75bbbdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -800,11 +800,11 @@ packages: /@nuxt/devalue/2.0.0: resolution: {integrity: sha512-YBI/6o2EBz02tdEJRBK8xkt3zvOFOWlLBf7WKYGBsSYSRtjjgrqPe2skp6VLLmKx5WbHHDNcW+6oACaurxGzeA==} - /@nuxt/kit/3.0.0-rc.12: - resolution: {integrity: sha512-d/6SeNVL1OPdru5aKjjUIWIwqIjbYN/VYGCrZs5gddkzJ5202DsMxyn2rs/ZyT8+oBbbVTYcCK6M+G0945mQdA==} - engines: {node: ^14.16.0 || ^16.10.0 || ^17.0.0 || ^18.0.0} + /@nuxt/kit/3.0.0-rc.13: + resolution: {integrity: sha512-FYEnMRm4LvIUxygmBX/p5kykzSeBleUqCOfxervQFONkz5PVVYXEp1DDBINGR3xk01yuPElENuf+l59iEQ4q7g==} + engines: {node: ^14.16.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0} dependencies: - '@nuxt/schema': 3.0.0-rc.12 + '@nuxt/schema': 3.0.0-rc.13 c12: 0.2.13 consola: 2.15.3 defu: 6.1.0 @@ -820,7 +820,7 @@ packages: scule: 0.3.2 semver: 7.3.8 unctx: 2.0.2 - unimport: 0.6.8 + unimport: 0.7.0 untyped: 0.5.0 transitivePeerDependencies: - supports-color @@ -893,9 +893,9 @@ packages: - webpack dev: true - /@nuxt/schema/3.0.0-rc.12: - resolution: {integrity: sha512-LZFy8a+5tZKtqTHvUJrlCjZXmKPSmar4S/p3SpjzgIbc4jDuWzA5r4voUODozd2/bCnYxfYyNtOgtbJSJtDUrw==} - engines: {node: ^14.16.0 || ^16.10.0 || ^17.0.0 || ^18.0.0} + /@nuxt/schema/3.0.0-rc.13: + resolution: {integrity: sha512-yfNPvUkOQ1/8aKHX8OtU7stANAaZ3B8Rty7HPuo1KHv0R3wNqlRdoRXwFuf4D+jcsS+R5Kccr7i8YYD5IG56Iw==} + engines: {node: ^14.16.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0} dependencies: c12: 0.2.13 create-require: 1.1.1 @@ -907,9 +907,30 @@ packages: scule: 0.3.2 std-env: 3.3.0 ufo: 0.8.6 - unimport: 0.6.8 + unimport: 0.7.0 untyped: 0.5.0 transitivePeerDependencies: + - rollup + - supports-color + + /@nuxt/schema/3.0.0-rc.13_rollup@2.79.1: + resolution: {integrity: sha512-yfNPvUkOQ1/8aKHX8OtU7stANAaZ3B8Rty7HPuo1KHv0R3wNqlRdoRXwFuf4D+jcsS+R5Kccr7i8YYD5IG56Iw==} + engines: {node: ^14.16.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0} + dependencies: + c12: 0.2.13 + create-require: 1.1.1 + defu: 6.1.0 + jiti: 1.16.0 + pathe: 0.3.9 + pkg-types: 0.3.6 + postcss-import-resolver: 2.0.0 + scule: 0.3.2 + std-env: 3.3.0 + ufo: 0.8.6 + unimport: 0.7.0_rollup@2.79.1 + untyped: 0.5.0 + transitivePeerDependencies: + - rollup - supports-color dev: true @@ -1023,7 +1044,7 @@ packages: vite-node: 0.24.5 vite-plugin-checker: 0.5.1_vite@3.1.8 vue: 3.2.41 - vue-bundle-renderer: 0.4.4 + vue-bundle-renderer: 0.5.0 transitivePeerDependencies: - '@types/node' - eslint @@ -1544,10 +1565,16 @@ packages: dependencies: rollup: 2.79.1 - /@rollup/pluginutils/4.2.1: - resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} - engines: {node: '>= 8.0.0'} + /@rollup/pluginutils/5.0.2: + resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true dependencies: + '@types/estree': 1.0.0 estree-walker: 2.0.2 picomatch: 2.3.1 dev: true @@ -1747,6 +1774,10 @@ packages: resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} dev: true + /@types/semver/7.3.13: + resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} + dev: true + /@types/unist/2.0.6: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} dev: true @@ -1944,7 +1975,7 @@ packages: vite: ^3.0.0 vue: ^3.2.25 dependencies: - vite: 3.1.8 + vite: 3.2.2 vue: 3.2.41 dev: true @@ -6121,6 +6152,10 @@ packages: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true + /natural-compare-lite/1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: true + /natural-compare/1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -6302,9 +6337,9 @@ packages: dependencies: boolbase: 1.0.0 - /nuxi/3.0.0-rc.12: - resolution: {integrity: sha512-jOnWe/Gf2/5Zj4wCFDHpmBPDDHZFMGrhqK5C+8jhG2RHNJy+YOlZETwAgoXPjmH0Hhb441UDQhZHKg5+yyKhbw==} - engines: {node: ^14.16.0 || ^16.10.0 || ^17.0.0 || ^18.0.0} + /nuxi/3.0.0-rc.13: + resolution: {integrity: sha512-Uh+Vk6mj0Zm5QttwdNFGQG0tjCXNjpc4e9NLRWpCjCCfBk5owBo2axxoeqfqIZMs6vUuCQCa7sLXQuoumyVjcQ==} + engines: {node: ^14.16.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0} hasBin: true optionalDependencies: fsevents: 2.3.2 @@ -6323,8 +6358,8 @@ packages: hasBin: true dependencies: '@nuxt/devalue': 2.0.0 - '@nuxt/kit': 3.0.0-rc.12 - '@nuxt/schema': 3.0.0-rc.12 + '@nuxt/kit': 3.0.0-rc.13 + '@nuxt/schema': 3.0.0-rc.13 '@nuxt/telemetry': 2.1.6 '@nuxt/ui-templates': 0.4.0 '@nuxt/vite-builder': 3.0.0-rc.12_vue@3.2.41 @@ -6336,6 +6371,7 @@ packages: defu: 6.1.0 destr: 1.2.0 escape-string-regexp: 5.0.0 + estree-walker: 3.0.1 fs-extra: 10.1.0 globby: 13.1.2 h3: 0.8.6 @@ -6360,7 +6396,7 @@ packages: unplugin: 0.10.2 untyped: 0.5.0 vue: 3.2.41 - vue-bundle-renderer: 0.4.4 + vue-bundle-renderer: 0.5.0 vue-devtools-stub: 0.1.0 vue-router: 4.1.6_vue@3.2.41 transitivePeerDependencies: @@ -8636,8 +8672,8 @@ packages: vfile: 5.3.5 dev: true - /unimport/0.6.8: - resolution: {integrity: sha512-MWkaPYvN0j+6jfEuiVFhfmy+aOtgAP11CozSbu/I3Cx+8ybjXIueB7GVlKofHabtjzSlPeAvWKJSFjHWsG2JaA==} + /unimport/0.7.0: + resolution: {integrity: sha512-Cr0whz4toYVid3JHlni/uThwavDVVCk6Zw0Gxnol1c7DprTA+Isr4T+asO6rDGkhkgV7r3vSdSs5Ym8F15JA+w==} dependencies: '@rollup/pluginutils': 4.2.1 escape-string-regexp: 5.0.0 @@ -8992,14 +9028,15 @@ packages: vscode-uri: 3.0.6 dev: true - /vite/3.1.8: - resolution: {integrity: sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg==} + /vite/3.2.2: + resolution: {integrity: sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: less: '*' sass: '*' stylus: '*' + sugarss: '*' terser: ^5.4.0 peerDependenciesMeta: less: @@ -9008,6 +9045,8 @@ packages: optional: true stylus: optional: true + sugarss: + optional: true terser: optional: true dependencies: @@ -9084,8 +9123,8 @@ packages: /vscode-uri/3.0.6: resolution: {integrity: sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ==} - /vue-bundle-renderer/0.4.4: - resolution: {integrity: sha512-kjJWPayzup8QFynETVpoYD0gDM2nbwN//bpt86hAHpZ+FPdTJFDQqKpouSLQgb2XjkOYM1uB/yc6Zb3iCvS7Gw==} + /vue-bundle-renderer/0.5.0: + resolution: {integrity: sha512-EZBp4TZ5oamgg+JL7kih5xO/qLCPlC6Dz4BH9ymoNP6xM2urZazql3PCAdztgnzBkOgmoOegEw4kp7Hgp8qaaA==} dependencies: ufo: 0.8.6 dev: true diff --git a/src/module.ts b/src/module.ts new file mode 100644 index 0000000..f279b9f --- /dev/null +++ b/src/module.ts @@ -0,0 +1,80 @@ +import { fileURLToPath } from 'url' +import { join, resolve } from 'pathe' +import { defu } from 'defu' +import dedent from 'dedent' + +import { addImports, addPlugin, addServerHandler, addTemplate, defineNuxtModule, useLogger } from '@nuxt/kit' + +export interface ModuleOptions { + baseURL: string + endpoint: string + installPlugin?: boolean +} + +const metaName = 'trpc-nuxt' + +export default defineNuxtModule({ + meta: { + name: metaName, + configKey: 'trpc', + }, + defaults: { + baseURL: '', + endpoint: '/trpc', + installPlugin: true, + }, + async setup(options, nuxt) { + const logger = useLogger(metaName) + + const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url)) + nuxt.options.build.transpile.push(runtimeDir, '#build/trpc-handler') + + const handlerPath = join(nuxt.options.buildDir, 'trpc-handler.ts') + const trpcOptionsPath = join(nuxt.options.srcDir, 'server/trpc') + + // Final resolved configuration + const finalConfig = nuxt.options.runtimeConfig.public.trpc = defu(nuxt.options.runtimeConfig.public.trpc, { + baseURL: options.baseURL, + endpoint: options.endpoint, + installPlugin: options.installPlugin, + }) + + addServerHandler({ + route: `${finalConfig.endpoint}/*`, + handler: handlerPath, + }) + + addTemplate({ + filename: 'trpc-handler.ts', + write: true, + getContents() { + return dedent` + import { createTRPCHandler } from 'trpc-nuxt/api' + import * as functions from '${trpcOptionsPath}' + + export default createTRPCHandler({ + ...functions, + endpoint: '${finalConfig.endpoint}' + }) + ` + }, + }) + + if (finalConfig.installPlugin) { + addImports([ + { name: 'useClient', from: join(runtimeDir, 'client') }, + { name: 'useAsyncQuery', from: join(runtimeDir, 'client') }, + { name: 'useClientHeaders', from: join(runtimeDir, 'client') }, + { name: 'getQueryKey', from: join(runtimeDir, 'client') }, + ]) + + addPlugin(resolve(runtimeDir, 'plugin')) + + logger.success('Plugin successfully installed.') + } + else { + logger.info('Plugin not installed. Create your own @trpc/client client plugin and composables.') + } + }, +}) + diff --git a/src/runtime/client.ts b/src/runtime/client.ts new file mode 100644 index 0000000..e5123ea --- /dev/null +++ b/src/runtime/client.ts @@ -0,0 +1,71 @@ +import type { + AsyncData, + AsyncDataOptions, + KeyOfRes, + PickFrom, + _Transform, +} from 'nuxt/dist/app/composables/asyncData' +import type { ProcedureRecord, inferHandlerInput, inferProcedureInput, inferProcedureOutput } from '@trpc/server' +import type { TRPCClient, TRPCClientErrorLike } from '@trpc/client' +import { objectHash } from 'ohash' +import type { Ref } from 'vue' +import { useAsyncData, useNuxtApp, useState } from '#app' +import type { router } from '~/server/trpc' + +type MaybeRef = T | Ref + +type AppRouter = typeof router + +export type inferProcedures< + TObj extends ProcedureRecord, +> = { + [TPath in keyof TObj]: { + input: inferProcedureInput + output: inferProcedureOutput + }; +} + +export type TQueries = AppRouter['_def']['queries'] +export type TError = TRPCClientErrorLike + +export type TQueryValues = inferProcedures + +/** + * Calculates the key used for `useAsyncData` call + * @param pathAndInput + */ +export function getQueryKey< + TPath extends keyof TQueryValues & string, + >(pathAndInput: [path: TPath, ...args: inferHandlerInput]) { + return `${pathAndInput[0]}-${objectHash(pathAndInput[1] ? JSON.stringify(pathAndInput[1]) : '')}` +} + +export async function useAsyncQuery< + TPath extends keyof TQueryValues & string, + TOutput extends TQueryValues[TPath]['output'] = TQueryValues[TPath]['output'], + Transform extends _Transform = _Transform, + PickKeys extends KeyOfRes = KeyOfRes, +>( + pathAndInput: [path: TPath, ...args: inferHandlerInput], + options: AsyncDataOptions = {}, +): Promise, PickKeys>, TError>> { + const { $client } = useNuxtApp() + const key = getQueryKey(pathAndInput) + const result = await useAsyncData( + key, + () => $client.query(...pathAndInput), + // @ts-expect-error: Internal + options, + ) + + return result as any +} + +export function useClient(): TRPCClient { + const { $client } = useNuxtApp() + return $client +} + +export function useClientHeaders(initialValue: MaybeRef> = {}): Ref> { + return useState('trpc-nuxt-header', () => initialValue) +}