From 5b90c4568421acedd00b0fa6de158412ea34fe0e Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Mon, 5 Sep 2022 10:13:12 +0300 Subject: [PATCH] Add type helpers for queries/mutations (#27) --- .changeset/kind-carrots-sit.md | 6 + packages/@next-fetch/react-query/src/index.ts | 37 +++++- packages/@next-fetch/swr/src/index.ts | 48 +++++--- packages/example-app/package.json | 13 ++- packages/example-app/pages/api/people.swr.ts | 3 + .../tests/types/type-inference.test.ts | 90 +++++++++++++++ packages/example-app/tsconfig.tests.json | 9 ++ packages/example-app/vitest.config.ts | 6 + pnpm-lock.yaml | 105 +++++++++++++++++- 9 files changed, 287 insertions(+), 30 deletions(-) create mode 100644 .changeset/kind-carrots-sit.md create mode 100644 packages/example-app/tests/types/type-inference.test.ts create mode 100644 packages/example-app/tsconfig.tests.json create mode 100644 packages/example-app/vitest.config.ts diff --git a/.changeset/kind-carrots-sit.md b/.changeset/kind-carrots-sit.md new file mode 100644 index 0000000..c218b22 --- /dev/null +++ b/.changeset/kind-carrots-sit.md @@ -0,0 +1,6 @@ +--- +"@next-fetch/react-query": patch +"@next-fetch/swr": patch +--- + +Add type helpers to get input/output of hooks diff --git a/packages/@next-fetch/react-query/src/index.ts b/packages/@next-fetch/react-query/src/index.ts index af246a3..a996341 100644 --- a/packages/@next-fetch/react-query/src/index.ts +++ b/packages/@next-fetch/react-query/src/index.ts @@ -8,15 +8,22 @@ import type { NextConfig } from "next"; import type { HookMetadata } from "@next-fetch/core-plugin/client"; import { createPlugin } from "@next-fetch/core-plugin"; +export type QueryResult = (v: Input) => UseQueryResult; +export type MutationResult = () => UseMutationResult< + Output, + any, + Input +> & { meta: HookMetadata }; + export function query( callback: HandlerCallback, options?: Partial> -): () => UseQueryResult; +): QueryResult; export function query( parser: Parser, callback: HandlerCallback, options?: Partial> -): (v: Input) => UseQueryResult; +): QueryResult; export function query(): unknown { throw new Error("This code path should not be reached"); } @@ -24,12 +31,12 @@ export function query(): unknown { export function mutation( callback: HandlerCallback, options?: Partial> -): () => UseMutationResult & { meta: HookMetadata }; +): MutationResult; export function mutation( parser: Parser, callback: HandlerCallback, options?: Partial> -): () => UseMutationResult & { meta: HookMetadata }; +): MutationResult; export function mutation(): unknown { throw new Error("This code path should not be reached"); } @@ -43,3 +50,25 @@ export function withReactQueryApiEndpoints(given: NextConfig = {}): NextConfig { serverPackageName: "@next-fetch/react-query/server", })(given); } + +/** + * Retrieves the type of the input of a given query/mutation hook + */ +export type inputOf< + T extends QueryResult | MutationResult +> = T extends MutationResult + ? Input + : T extends QueryResult + ? Input + : never; + +/** + * Retrieves the type of the output of a given query/mutation hook + */ +export type outputOf< + T extends QueryResult | MutationResult +> = T extends MutationResult + ? Output + : T extends QueryResult + ? Output + : never; diff --git a/packages/@next-fetch/swr/src/index.ts b/packages/@next-fetch/swr/src/index.ts index a1668f3..dda63b7 100644 --- a/packages/@next-fetch/swr/src/index.ts +++ b/packages/@next-fetch/swr/src/index.ts @@ -9,41 +9,42 @@ import type { import { createPlugin } from "@next-fetch/core-plugin"; import type { NextConfig } from "next"; +export type QueryResult = ((v: Input) => SWRResponse) & { + meta: HookMetadata; +}; + export function query( callback: HandlerCallback, options?: Partial> -): (() => SWRResponse) & { meta: HookMetadata }; +): QueryResult; export function query( parser: Parser, callback: HandlerCallback, options?: Partial> -): ((v: Input) => SWRResponse) & { meta: HookMetadata }; -export function query( - parser: unknown, - callback: unknown, - options?: unknown -): ((v: Input) => SWRResponse) & { meta: HookMetadata } { +): QueryResult; +export function query(): unknown { throw new Error("This code path should not be reached"); } export type MutationOptions = HookIntoResponse; +export type MutationResult = (() => SWRMutationResponse< + Output, + any, + Input +> & { meta: HookMetadata }) & { + meta: HookMetadata; +}; export function mutation( callback: HandlerCallback, options?: Partial> -): () => SWRMutationResponse & { meta: HookMetadata }; +): MutationResult; export function mutation( parser: Parser, callback: HandlerCallback, options?: Partial> -): () => SWRMutationResponse & { meta: HookMetadata }; -export function mutation( - parser: unknown, - callback: unknown, - options?: unknown -): (() => SWRMutationResponse & { meta: HookMetadata }) & { - meta: HookMetadata; -} { +): MutationResult; +export function mutation(): unknown { throw new Error("This code path should not be reached"); } @@ -56,3 +57,18 @@ export function withSwrApiEndpoints(given: NextConfig = {}): NextConfig { serverPackageName: "@next-fetch/swr/server", })(given); } + +export type inputOf< + T extends MutationResult | QueryResult +> = T extends MutationResult + ? Input + : T extends QueryResult + ? Input + : never; +export type outputOf< + T extends MutationResult | QueryResult +> = T extends MutationResult + ? Output + : T extends QueryResult + ? Output + : never; diff --git a/packages/example-app/package.json b/packages/example-app/package.json index e3fb1a8..19e38c4 100644 --- a/packages/example-app/package.json +++ b/packages/example-app/package.json @@ -9,17 +9,20 @@ "start": "next start", "dev": "next dev --port=3002", "prepare-env:test": "playwright install --with-deps", - "test": "playwright test", + "test": "concurrently 'pnpm test:tsc' 'pnpm test:e2e' 'pnpm test:unit'", + "test:tsc": "tsc --project tsconfig.tests.json", + "test:e2e": "playwright test", + "test:unit": "vitest run", "vercel-build": "pnpm run -w build" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { - "@tanstack/react-query": "^4.0.10", - "next": "^12.2.4", "@next-fetch/react-query": "workspace:*", "@next-fetch/swr": "workspace:*", + "@tanstack/react-query": "^4.0.10", + "next": "^12.2.4", "react": "^18.1.0", "react-dom": "^18.1.0", "swr": "^2.0.0-beta.6", @@ -30,6 +33,8 @@ "@types/node": "^17.0.38", "@types/react": "^18.0.10", "@types/react-dom": "^18.0.5", - "typescript": "^4.7.2" + "concurrently": "^7.3.0", + "typescript": "^4.7.2", + "vitest": "^0.20.2" } } diff --git a/packages/example-app/pages/api/people.swr.ts b/packages/example-app/pages/api/people.swr.ts index 23dec1d..6b50e76 100644 --- a/packages/example-app/pages/api/people.swr.ts +++ b/packages/example-app/pages/api/people.swr.ts @@ -2,6 +2,9 @@ import z from "zod"; import { query, mutation } from "@next-fetch/swr"; import { userAgent } from "next/server"; +/** + * Queries a string + */ export const useAllPeople = query( async () => { return `Many people are here!`; diff --git a/packages/example-app/tests/types/type-inference.test.ts b/packages/example-app/tests/types/type-inference.test.ts new file mode 100644 index 0000000..dde37a7 --- /dev/null +++ b/packages/example-app/tests/types/type-inference.test.ts @@ -0,0 +1,90 @@ +import { test, describe } from "vitest"; +import type { + inputOf as rqInputOf, + outputOf as rqOutputOf, +} from "@next-fetch/react-query"; +import type { + useListPeopleWith as ReactQueryUseListPeopleWith, + usePerson as ReactQueryUsePerson, + useAllPeople as ReactQueryUseAllPeople, +} from "../../pages/api/rq/people.rq"; +import type { + inputOf as swrInputOf, + outputOf as swrOutputOf, +} from "@next-fetch/swr"; +import type { + useListPeopleWith as SWRUseListPeopleWith, + usePerson as SWRUsePerson, + useAllPeople as SWRUseAllPeople, +} from "../../pages/api/people.swr"; + +type IsAny = 0 extends 1 & T ? true : false; +type SameType = true extends IsAny | IsAny + ? false + : [T] extends [U] + ? [U] extends [T] + ? true + : false + : false; +function expectSameType(_v: SameType) {} + +describe("helpers", () => { + test("expectSameType", () => { + expectSameType(false); + expectSameType(true); + expectSameType(false); + + // Check that `any` fails the conditional as well + expectSameType(false); + expectSameType(false); + + // @ts-expect-error this should fail + expectSameType(false); + }); +}); + +describe("react query", () => { + test("mutation", () => { + type Input = rqInputOf; + type Output = rqOutputOf; + expectSameType(true); + expectSameType(true); + }); + + test("query with no args", () => { + type Input = rqInputOf; + type Output = rqOutputOf; + expectSameType(true); + expectSameType(true); + }); + + test("query with args", () => { + type Input = rqInputOf; + type Output = rqOutputOf; + expectSameType(true); + expectSameType(true); + }); +}); + +describe("swr", () => { + test("mutation", () => { + type Input = swrInputOf; + type Output = swrOutputOf; + expectSameType(true); + expectSameType(true); + }); + + test("query with no args", () => { + type Input = swrInputOf; + type Output = swrOutputOf; + expectSameType(true); + expectSameType(true); + }); + + test("query with args", () => { + type Input = swrInputOf; + type Output = swrOutputOf; + expectSameType(true); + expectSameType(true); + }); +}); diff --git a/packages/example-app/tsconfig.tests.json b/packages/example-app/tsconfig.tests.json new file mode 100644 index 0000000..cc496af --- /dev/null +++ b/packages/example-app/tsconfig.tests.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "incremental": false + }, + "include": ["./tests/**/*.ts"] +} diff --git a/packages/example-app/vitest.config.ts b/packages/example-app/vitest.config.ts new file mode 100644 index 0000000..811d19f --- /dev/null +++ b/packages/example-app/vitest.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vitest/config"; +export default defineConfig({ + test: { + exclude: ["**/node_modules/**", "**/dist/**", "**/e2e/**"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 455f122..0705131 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,8 +110,8 @@ importers: typescript: ^4.7.2 dependencies: next: 12.2.4_ef5jwxihqo6n7gxfmzogljlgcm - nextra: 2.0.0-beta.19_yvocaa5laqq4lii6e25wp4dnnu - nextra-theme-docs: 2.0.0-beta.19_yvocaa5laqq4lii6e25wp4dnnu + nextra: 2.0.0-beta.24_yvocaa5laqq4lii6e25wp4dnnu + nextra-theme-docs: 2.0.0-beta.24_yvocaa5laqq4lii6e25wp4dnnu react: 18.1.0 react-dom: 18.1.0_react@18.1.0 devDependencies: @@ -129,11 +129,13 @@ importers: '@types/node': ^17.0.38 '@types/react': ^18.0.10 '@types/react-dom': ^18.0.5 + concurrently: ^7.3.0 next: ^12.2.4 react: ^18.1.0 react-dom: ^18.1.0 swr: ^2.0.0-beta.6 typescript: ^4.7.2 + vitest: ^0.20.2 zod: ^3.17.3 dependencies: '@next-fetch/react-query': link:../@next-fetch/react-query @@ -149,7 +151,9 @@ importers: '@types/node': 17.0.38 '@types/react': 18.0.10 '@types/react-dom': 18.0.5 + concurrently: 7.3.0 typescript: 4.7.2 + vitest: 0.20.2 packages: @@ -1440,6 +1444,22 @@ packages: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} dev: true + /concurrently/7.3.0: + resolution: {integrity: sha512-IiDwm+8DOcFEInca494A8V402tNTQlJaYq78RF2rijOrKEk/AOHTxhN4U1cp7GYKYX5Q6Ymh1dLTBlzIMN0ikA==} + engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} + hasBin: true + dependencies: + chalk: 4.1.2 + date-fns: 2.29.2 + lodash: 4.17.21 + rxjs: 7.5.6 + shell-quote: 1.7.3 + spawn-command: 0.0.2-1 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.5.1 + dev: true + /cross-spawn/5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -1485,6 +1505,11 @@ packages: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} dev: true + /date-fns/2.29.2: + resolution: {integrity: sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA==} + engines: {node: '>=0.11'} + dev: true + /debug/4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -1922,6 +1947,13 @@ packages: source-map: 0.7.3 dev: false + /estree-util-value-to-estree/1.3.0: + resolution: {integrity: sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==} + engines: {node: '>=12.0.0'} + dependencies: + is-plain-obj: 3.0.0 + dev: false + /estree-util-visit/1.2.0: resolution: {integrity: sha512-wdsoqhWueuJKsh5hqLw3j8lwFqNStm92VcwtAOAny8g/KS/l5Y8RISjR4k5W6skCj3Nirag/WUCMS0Nfy3sgsg==} dependencies: @@ -2446,6 +2478,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-plain-obj/3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + dev: false + /is-plain-obj/4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -2622,6 +2659,10 @@ packages: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} dev: true + /lodash/4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + /longest-streak/3.0.1: resolution: {integrity: sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==} dev: false @@ -3334,8 +3375,8 @@ packages: - '@babel/core' - babel-plugin-macros - /nextra-theme-docs/2.0.0-beta.19_yvocaa5laqq4lii6e25wp4dnnu: - resolution: {integrity: sha512-tbemgKWcDGN1aVyG1c9makmmNdRJlkCG/Ji6HgkYFH6jhtLI1ePjTmNYHmoXCb0YvbxyTD0xMcDqIomDMg+DKw==} + /nextra-theme-docs/2.0.0-beta.24_yvocaa5laqq4lii6e25wp4dnnu: + resolution: {integrity: sha512-GK8cEtxHoFl/FJGOB0I22HdwzVZA2PG4ovZ287kTODmIxT5ies06T6pznZJWmMzGdo/DTpzBr3L/CPiRcpU51w==} peerDependencies: next: '>=9.5.3' react: '>=16.13.1' @@ -3359,8 +3400,8 @@ packages: title: 3.5.3 dev: false - /nextra/2.0.0-beta.19_yvocaa5laqq4lii6e25wp4dnnu: - resolution: {integrity: sha512-JaXjemtLCRp/NUwL9W5sUmEC5sLnSCzbaczNrg2UuxmEDYdt3xYIrq821sVgxeSXvBKQgLUgUQEH8kYWQ37gew==} + /nextra/2.0.0-beta.24_yvocaa5laqq4lii6e25wp4dnnu: + resolution: {integrity: sha512-vGIHxc84yV0igLYwOvYTAZD5CkJkipMDU6Qm0mWfANb1QiI3WbAL/LRdHj5Jl1H9h3IZ/Hz3QAEmRhjR5cXQBA==} peerDependencies: next: '>=9.5.3' react: '>=16.13.1' @@ -3373,10 +3414,12 @@ packages: gray-matter: 4.0.3 next: 12.2.4_ef5jwxihqo6n7gxfmzogljlgcm react: 18.1.0 + react-children-utilities: 2.8.0_react@18.1.0 react-dom: 18.1.0_react@18.1.0 rehype-mdx-title: 1.0.0 rehype-pretty-code: 0.2.4_shiki@0.10.1 remark-gfm: 3.0.1 + remark-reading-time: 2.0.1 shiki: 0.10.1 slash: 3.0.0 transitivePeerDependencies: @@ -3698,6 +3741,14 @@ packages: safe-buffer: 5.2.1 dev: true + /react-children-utilities/2.8.0_react@18.1.0: + resolution: {integrity: sha512-g42oRsZLrFJgCcIdK1lad1CWujNH4gh1Cp1lsMQpHWdDjWQ8gUlaBebgy2iXofyPEpfJ4T/xt4qWrvDkgVCCNg==} + peerDependencies: + react: 18 || 17 || 16 || 15 + dependencies: + react: 18.1.0 + dev: false + /react-dom/18.1.0_react@18.1.0: resolution: {integrity: sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==} peerDependencies: @@ -3749,6 +3800,10 @@ packages: picomatch: 2.3.1 dev: true + /reading-time/1.5.0: + resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} + dev: false + /redent/3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -3818,6 +3873,15 @@ packages: - supports-color dev: false + /remark-reading-time/2.0.1: + resolution: {integrity: sha512-fy4BKy9SRhtYbEHvp6AItbRTnrhiDGbqLQTSYVbQPGuRCncU1ubSsh9p/W5QZSxtYcUXv8KGL0xBgPLyNJA1xw==} + dependencies: + estree-util-is-identifier-name: 2.0.1 + estree-util-value-to-estree: 1.3.0 + reading-time: 1.5.0 + unist-util-visit: 3.1.0 + dev: false + /remark-rehype/10.1.0: resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} dependencies: @@ -3873,6 +3937,12 @@ packages: queue-microtask: 1.2.3 dev: true + /rxjs/7.5.6: + resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==} + dependencies: + tslib: 2.4.0 + dev: true + /sade/1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -3953,6 +4023,10 @@ packages: engines: {node: '>=8'} dev: true + /shell-quote/1.7.3: + resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} + dev: true + /shiki/0.10.1: resolution: {integrity: sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==} dependencies: @@ -4020,6 +4094,10 @@ packages: resolution: {integrity: sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==} dev: false + /spawn-command/0.0.2-1: + resolution: {integrity: sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==} + dev: true + /spawndamnit/2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} dependencies: @@ -4607,6 +4685,13 @@ packages: unist-util-is: 4.1.0 dev: false + /unist-util-visit-parents/4.1.1: + resolution: {integrity: sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==} + dependencies: + '@types/unist': 2.0.6 + unist-util-is: 5.1.1 + dev: false + /unist-util-visit-parents/5.1.1: resolution: {integrity: sha512-gks4baapT/kNRaWxuGkl5BIhoanZo7sC/cUT/JToSRNL1dYoXRFl75d++NkjYk4TAu2uv2Px+l8guMajogeuiw==} dependencies: @@ -4622,6 +4707,14 @@ packages: unist-util-visit-parents: 3.1.1 dev: false + /unist-util-visit/3.1.0: + resolution: {integrity: sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==} + dependencies: + '@types/unist': 2.0.6 + unist-util-is: 5.1.1 + unist-util-visit-parents: 4.1.1 + dev: false + /unist-util-visit/4.1.1: resolution: {integrity: sha512-n9KN3WV9k4h1DxYR1LoajgN93wpEi/7ZplVe02IoB4gH5ctI1AaF2670BLHQYbwj+pY83gFtyeySFiyMHJklrg==} dependencies: