diff --git a/.changeset/silent-snakes-confess.md b/.changeset/silent-snakes-confess.md new file mode 100644 index 00000000000..faa37bba4a8 --- /dev/null +++ b/.changeset/silent-snakes-confess.md @@ -0,0 +1,5 @@ +--- +"@clerk/vue": patch +--- + +Introduce an experimental version of Clerk SDK for [Vue](https://vuejs.org) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 611d3df9de5..2bfb494e7d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: run: | if [ "${{ matrix.node-version }}" == "18" ]; then echo "Running tests on Node 18 only for packages with LTS support." - npx turbo test $TURBO_ARGS --filter="@clerk/astro" --filter="@clerk/backend" --filter="@clerk/express" --filter="@clerk/nextjs" --filter="@clerk/clerk-react" --filter="@clerk/clerk-sdk-node" --filter="@clerk/shared" --filter="@clerk/remix" --filter="@clerk/tanstack-start" --filter="@clerk/elements" + npx turbo test $TURBO_ARGS --filter="@clerk/astro" --filter="@clerk/backend" --filter="@clerk/express" --filter="@clerk/nextjs" --filter="@clerk/clerk-react" --filter="@clerk/clerk-sdk-node" --filter="@clerk/shared" --filter="@clerk/remix" --filter="@clerk/tanstack-start" --filter="@clerk/elements" --filter="@clerk/vue" else echo "Running tests for all packages on Node 20." npx turbo test $TURBO_ARGS diff --git a/package-lock.json b/package-lock.json index f79ce8d3ee8..f362409325d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2988,6 +2988,10 @@ "resolved": "packages/upgrade", "link": true }, + "node_modules/@clerk/vue": { + "resolved": "packages/vue", + "link": true + }, "node_modules/@cloudflare/kv-asset-handler": { "version": "0.3.4", "dev": true, @@ -6871,8 +6875,8 @@ "license": "MIT", "dependencies": { "ansi-escapes": "^6.2.0", + "cardinal": "^2.1.1", "chalk": "^5.3.0", - "cli-highlight": "^2.1.11", "cli-table3": "^0.6.3", "node-emoji": "^2.1.3", "supports-hyperlinks": "^3.0.0" @@ -8180,6 +8184,109 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@nuxt/kit": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.13.2.tgz", + "integrity": "sha512-KvRw21zU//wdz25IeE1E5m/aFSzhJloBRAQtv+evcFeZvuroIxpIQuUqhbzuwznaUwpiWbmwlcsp5uOWmi4vwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/schema": "3.13.2", + "c12": "^1.11.2", + "consola": "^3.2.3", + "defu": "^6.1.4", + "destr": "^2.0.3", + "globby": "^14.0.2", + "hash-sum": "^2.0.0", + "ignore": "^5.3.2", + "jiti": "^1.21.6", + "klona": "^2.0.6", + "knitwork": "^1.1.0", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "semver": "^7.6.3", + "ufo": "^1.5.4", + "unctx": "^2.3.1", + "unimport": "^3.12.0", + "untyped": "^1.4.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/kit/node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/kit/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/kit/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/schema": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.13.2.tgz", + "integrity": "sha512-CCZgpm+MkqtOMDEgF9SWgGPBXlQ01hV/6+2reDEpJuqFPGzV8HYKPBcIFvn7/z5ahtgutHLzjP71Na+hYcqSpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "compatx": "^0.1.8", + "consola": "^3.2.3", + "defu": "^6.1.4", + "hookable": "^5.5.3", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "std-env": "^3.7.0", + "ufo": "^1.5.4", + "uncrypto": "^0.1.3", + "unimport": "^3.12.0", + "untyped": "^1.4.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/@octokit/auth-token": { "version": "4.0.0", "dev": true, @@ -8341,6 +8448,13 @@ "@octokit/openapi-types": "^19.0.2" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", "dev": true, @@ -13412,6 +13526,113 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@testing-library/vue": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/vue/-/vue-8.1.0.tgz", + "integrity": "sha512-ls4RiHO1ta4mxqqajWRh8158uFObVrrtAPoxk7cIp4HrnQUj/ScKzqz53HxYpG3X6Zb7H2v+0eTGLSoy8HQ2nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@testing-library/dom": "^9.3.3", + "@vue/test-utils": "^2.4.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@vue/compiler-sfc": ">= 3", + "vue": ">= 3" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, + "node_modules/@testing-library/vue/node_modules/@testing-library/dom": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/vue/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/vue/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/vue/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/vue/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/vue/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "dev": true, @@ -15493,6 +15714,257 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@volar/language-core": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.1.6.tgz", + "integrity": "sha512-pAlMCGX/HatBSiDFMdMyqUshkbwWbLxpN/RL7HCQDOo2gYBE+uS+nanosLc1qR6pTQ/U8q00xt8bdrrAFPSC0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.1.6" + } + }, + "node_modules/@volar/source-map": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.1.6.tgz", + "integrity": "sha512-TeyH8pHHonRCHYI91J7fWUoxi0zWV8whZTVRlsWHSYfjm58Blalkf9LrZ+pj6OiverPTmrHRkBsG17ScQyWECw==", + "dev": true, + "license": "MIT", + "dependencies": { + "muggle-string": "^0.4.0" + } + }, + "node_modules/@volar/typescript": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.1.6.tgz", + "integrity": "sha512-JgPGhORHqXuyC3r6skPmPHIZj4LoMmGlYErFTuPNBq9Nhc9VTv7ctHY7A3jMN3ngKEfRrfnUcwXHztvdSQqNfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.1.6", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@vue.ts/common": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@vue.ts/common/-/common-0.6.0.tgz", + "integrity": "sha512-uqC27JZ1nEPTx+ezkJugYcltxeh6N5JSh7ril4YpJal4hTIEYAu0idKQcWng5lj9iFBZpeZV/WyEQ4V4qJpWMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.5" + } + }, + "node_modules/@vue.ts/language": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@vue.ts/language/-/language-0.6.0.tgz", + "integrity": "sha512-zM6xAuntcByyj4uRDqBpy58g3NI/xc03GrAP1xAbv0Rfa6P23hJTlFS0ByZmMy/f9myfr8bssT6oSIOk5JsEJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~2.1.6", + "@vue.ts/common": "0.6.0", + "@vue/language-core": "2.0.7" + } + }, + "node_modules/@vue.ts/tsx-auto-props": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@vue.ts/tsx-auto-props/-/tsx-auto-props-0.6.0.tgz", + "integrity": "sha512-4LNbjjc3C+OtBTJ7Sk2gAGkDws4jrc6HMIOh+/Tq7cZdCuJgBxh5hIW7fhGZ+D6b46ORtUFWTT4/POF95lUKow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.8.1", + "@vue.ts/common": "0.6.0", + "@vue.ts/language": "0.6.0", + "magic-string": "^0.30.5", + "unplugin": "^1.5.0" + }, + "peerDependencies": { + "typescript": "^5.2.2", + "vue": "^3.3.7" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", + "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.12", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", + "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", + "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.12", + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.47", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT", + "peer": true + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", + "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/language-core": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.7.tgz", + "integrity": "sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~2.1.3", + "@vue/compiler-dom": "^3.4.0", + "@vue/shared": "^3.4.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", + "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz", + "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/reactivity": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", + "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/reactivity": "3.5.12", + "@vue/runtime-core": "3.5.12", + "@vue/shared": "3.5.12", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz", + "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12" + }, + "peerDependencies": { + "vue": "3.5.12" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "license": "MIT" + }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, "node_modules/@web3-storage/multipart-parser": { "version": "1.0.0", "dev": true, @@ -15711,7 +16183,9 @@ } }, "node_modules/acorn": { - "version": "8.12.1", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -19318,6 +19792,7 @@ }, "node_modules/cli-highlight": { "version": "2.1.11", + "dev": true, "license": "ISC", "dependencies": { "chalk": "^4.0.0", @@ -19337,6 +19812,7 @@ }, "node_modules/cli-highlight/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19350,6 +19826,7 @@ }, "node_modules/cli-highlight/node_modules/chalk": { "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -19364,6 +19841,7 @@ }, "node_modules/cli-highlight/node_modules/cliui": { "version": "7.0.4", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -19373,6 +19851,7 @@ }, "node_modules/cli-highlight/node_modules/color-convert": { "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -19383,14 +19862,17 @@ }, "node_modules/cli-highlight/node_modules/color-name": { "version": "1.1.4", + "dev": true, "license": "MIT" }, "node_modules/cli-highlight/node_modules/parse5": { "version": "5.1.1", + "dev": true, "license": "MIT" }, "node_modules/cli-highlight/node_modules/yargs": { "version": "16.2.0", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^7.0.2", @@ -19407,6 +19889,7 @@ }, "node_modules/cli-highlight/node_modules/yargs-parser": { "version": "20.2.9", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -19885,6 +20368,13 @@ "node": ">=0.10.0" } }, + "node_modules/compatx": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/compatx/-/compatx-0.1.8.tgz", + "integrity": "sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==", + "dev": true, + "license": "MIT" + }, "node_modules/component-emitter": { "version": "1.3.1", "dev": true, @@ -20016,6 +20506,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "license": "MIT" @@ -20129,6 +20626,24 @@ "dev": true, "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/connect": { "version": "3.7.0", "license": "MIT", @@ -21330,6 +21845,13 @@ } } }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, "node_modules/debounce": { "version": "1.2.1", "dev": true, @@ -21427,6 +21949,39 @@ "node": ">=6" } }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "license": "MIT", @@ -22131,6 +22686,61 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ee-first": { "version": "1.1.1", "license": "MIT" @@ -22396,6 +23006,27 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-iterator-helpers": { "version": "1.1.0", "license": "MIT", @@ -26496,6 +27127,13 @@ "dev": true, "license": "ISC" }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true, + "license": "MIT" + }, "node_modules/hasown": { "version": "2.0.2", "license": "MIT", @@ -26728,6 +27366,7 @@ }, "node_modules/highlight.js": { "version": "10.7.3", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": "*" @@ -27076,7 +27715,9 @@ "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.2.4", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "license": "MIT", "engines": { "node": ">= 4" @@ -27590,6 +28231,23 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "license": "MIT", @@ -30417,6 +31075,101 @@ "node": ">=10" } }, + "node_modules/js-beautify": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/js-cookie": { "version": "3.0.5", "license": "MIT", @@ -33968,6 +34721,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/multicast-dns": { "version": "7.2.5", "dev": true, @@ -35131,6 +35891,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "license": "MIT", @@ -35783,6 +36560,7 @@ }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "6.0.1", + "dev": true, "license": "MIT", "dependencies": { "parse5": "^6.0.1" @@ -35790,6 +36568,7 @@ }, "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { "version": "6.0.1", + "dev": true, "license": "MIT" }, "node_modules/parseurl": { @@ -36630,6 +37409,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/proxy-addr": { "version": "2.0.7", "dev": true, @@ -40138,6 +40924,19 @@ "graceful-fs": "^4.1.3" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream-buffers": { "version": "2.2.0", "license": "Unlicense", @@ -42504,11 +43303,13 @@ } }, "node_modules/unimport": { - "version": "3.11.1", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.13.1.tgz", + "integrity": "sha512-nNrVzcs93yrZQOW77qnyOVHtb68LegvhYFwxFMfuuWScmwQmyVCG/NBuN8tYsaGzgQUVYv34E/af+Cc9u4og4A==", "dev": true, "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.1.0", + "@rollup/pluginutils": "^5.1.2", "acorn": "^8.12.1", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", @@ -42520,7 +43321,7 @@ "pkg-types": "^1.2.0", "scule": "^1.3.0", "strip-literal": "^2.1.0", - "unplugin": "^1.12.2" + "unplugin": "^1.14.1" } }, "node_modules/unimport/node_modules/escape-string-regexp": { @@ -42716,17 +43517,25 @@ } }, "node_modules/unplugin": { - "version": "1.12.2", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.15.0.tgz", + "integrity": "sha512-jTPIs63W+DUEDW207ztbaoO7cQ4p5aVaB823LSlxpsFEU3Mykwxf3ZGC/wzxFJeZlASZYgVrWeo7LgOrqJZ8RA==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.12.1", - "chokidar": "^3.6.0", - "webpack-sources": "^3.2.3", + "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" }, "engines": { "node": ">=14.0.0" + }, + "peerDependencies": { + "webpack-sources": "^3" + }, + "peerDependenciesMeta": { + "webpack-sources": { + "optional": true + } } }, "node_modules/unplugin/node_modules/webpack-virtual-modules": { @@ -42833,6 +43642,35 @@ "untun": "bin/untun.mjs" } }, + "node_modules/untyped": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/untyped/-/untyped-1.5.1.tgz", + "integrity": "sha512-reBOnkJBFfBZ8pCKaeHgfZLcehXtM6UTxc+vqs1JvCps0c4amLNp3fhdGBZwYp+VLyoY9n3X5KOP7lCyWBUX9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.7", + "@babel/standalone": "^7.25.7", + "@babel/types": "^7.25.7", + "defu": "^6.1.4", + "jiti": "^2.3.1", + "mri": "^1.2.0", + "scule": "^1.3.0" + }, + "bin": { + "untyped": "dist/cli.mjs" + } + }, + "node_modules/untyped/node_modules/jiti": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.0.tgz", + "integrity": "sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/unwasm": { "version": "0.3.9", "dev": true, @@ -44242,6 +45080,46 @@ "version": "1.0.1", "license": "MIT" }, + "node_modules/vue": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz", + "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-sfc": "3.5.12", + "@vue/runtime-dom": "3.5.12", + "@vue/server-renderer": "3.5.12", + "@vue/shared": "3.5.12" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.1.10.tgz", + "integrity": "sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "dev": true, @@ -44698,6 +45576,7 @@ "version": "3.2.3", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -50600,6 +51479,26 @@ "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } + }, + "packages/vue": { + "name": "@clerk/vue", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@clerk/shared": "2.11.5", + "@clerk/types": "4.30.0" + }, + "devDependencies": { + "@testing-library/vue": "^8.1.0", + "@vue.ts/tsx-auto-props": "^0.6.0", + "typescript": "*" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "vue": "^3.2.0" + } } } } diff --git a/packages/vue/.eslintrc.cjs b/packages/vue/.eslintrc.cjs new file mode 100644 index 00000000000..7f702050e52 --- /dev/null +++ b/packages/vue/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['@clerk/custom/browser', '@clerk/custom/typescript', '@clerk/custom/jest'], +}; diff --git a/packages/vue/LICENSE b/packages/vue/LICENSE new file mode 100644 index 00000000000..5713d0938b3 --- /dev/null +++ b/packages/vue/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Clerk, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/vue/README.md b/packages/vue/README.md new file mode 100644 index 00000000000..0c8c26a6117 --- /dev/null +++ b/packages/vue/README.md @@ -0,0 +1,119 @@ +

+ + + + + + +
+

@clerk/vue

+

+ +
+ +[![Chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://clerk.com/discord) +[![Clerk documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs?utm_source=github&utm_medium=clerk_vue) +[![Follow on Twitter](https://img.shields.io/twitter/follow/ClerkDev?style=social)](https://twitter.com/intent/follow?screen_name=ClerkDev) + +[Changelog](https://github.com/clerk/javascript/blob/main/packages/vue/CHANGELOG.md) +· +[Report a Bug](https://github.com/clerk/javascript/issues/new?assignees=&labels=needs-triage&projects=&template=BUG_REPORT.yml) +· +[Request a Feature](https://feedback.clerk.com/roadmap) +· +[Get help](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_vue) + +
+ +[Clerk](https://clerk.com/?utm_source=github&utm_medium=clerk_vue) is the easiest way to add authentication and user management to your Vue application. Add sign up, sign in, and profile management to your application in minutes. + +### Prerequisites + +- Vue 3.2 or later +- Node.js `>=18.17.0` or later +- An existing Clerk application. [Create your account for free](https://dashboard.clerk.com/sign-up?utm_source=github&utm_medium=clerk_vue). + +### Installation + +Add `@clerk/vue` as a dependency + +```bash +npm install @clerk/vue +``` + +### Build + +To build the package locally with the TypeScript compiler, run: + +```bash +npm run build +``` + +## Usage + +Make sure the following environment variables are set in a `.env.local` file in your Vite project: + +``` +VITE_CLERK_PUBLISHABLE_KEY=[publishable-key] +``` + +Then, install the Clerk plugin in your main Vue application: + +```js +import { createApp } from 'vue'; +import App from './App.vue'; +import { clerkPlugin } from '@clerk/vue'; + +const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; + +const app = createApp(App); +app.use(clerkPlugin, { + publishableKey: PUBLISHABLE_KEY, +}); +app.mount('#app'); +``` + +You can now start using Clerk's components. Here's a basic example showing a header component: + +```vue + + + +``` + +## Support + +You can get in touch with us in any of the following ways: + +- Join our official community [Discord server](https://clerk.com/discord) +- On [our support page](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_vue) + +## Contributing + +We're open to all community contributions! If you'd like to contribute in any way, please read [our contribution guidelines](https://github.com/clerk/javascript/blob/main/docs/CONTRIBUTING.md) and [code of conduct](https://github.com/clerk/javascript/blob/main/docs/CODE_OF_CONDUCT.md). + +## Security + +`@clerk/vue` follows good practices of security, but 100% security cannot be assured. + +`@clerk/vue` is provided **"as is"** without any **warranty**. Use at your own risk. + +_For more information and to report security issues, please refer to our [security documentation](https://github.com/clerk/javascript/blob/main/docs/SECURITY.md)._ + +## License + +This project is licensed under the **MIT license**. + +See [LICENSE](https://github.com/clerk/javascript/blob/main/packages/vue/LICENSE) for more information. diff --git a/packages/vue/package.json b/packages/vue/package.json new file mode 100644 index 00000000000..c4e8fea676c --- /dev/null +++ b/packages/vue/package.json @@ -0,0 +1,66 @@ +{ + "name": "@clerk/vue", + "version": "0.0.0", + "description": "Clerk SDK for Vue", + "keywords": [ + "clerk", + "typescript", + "vue", + "auth", + "authentication", + "passwordless", + "session", + "jwt" + ], + "homepage": "https://clerk.com/", + "bugs": { + "url": "https://github.com/clerk/javascript/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/clerk/javascript.git", + "directory": "packages/vue" + }, + "license": "MIT", + "author": "Clerk", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "lint": "eslint src/", + "lint:attw": "attw --pack . --ignore-rules cjs-resolves-to-esm", + "lint:publint": "publint", + "publish:local": "npx yalc push --replace --sig", + "test": "vitest", + "test:ci": "vitest --maxWorkers=70%" + }, + "dependencies": { + "@clerk/shared": "2.11.5", + "@clerk/types": "4.30.0" + }, + "devDependencies": { + "@testing-library/vue": "^8.1.0", + "@vue.ts/tsx-auto-props": "^0.6.0", + "typescript": "*" + }, + "peerDependencies": { + "vue": "^3.2.0" + }, + "engines": { + "node": ">=18.17.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/vue/src/components/SignInButton.ts b/packages/vue/src/components/SignInButton.ts new file mode 100644 index 00000000000..37a5536de2d --- /dev/null +++ b/packages/vue/src/components/SignInButton.ts @@ -0,0 +1,47 @@ +import type { SignInProps } from '@clerk/types'; +import { defineComponent, h } from 'vue'; + +import { useClerk } from '../composables/useClerk'; +import { assertSingleChild, normalizeWithDefaultValue } from '../utils'; + +type SignInButtonProps = Pick< + SignInProps, + 'fallbackRedirectUrl' | 'forceRedirectUrl' | 'signUpForceRedirectUrl' | 'signUpFallbackRedirectUrl' +>; + +export const SignInButton = defineComponent( + ( + props: SignInButtonProps & { + mode?: 'modal' | 'redirect'; + }, + { slots, attrs }, + ) => { + const clerk = useClerk(); + + function clickHandler() { + const { mode, ...opts } = props; + + if (mode === 'modal') { + return clerk.value?.openSignIn(opts); + } + + void clerk.value?.redirectToSignIn({ + ...opts, + signInFallbackRedirectUrl: props.fallbackRedirectUrl, + signInForceRedirectUrl: props.forceRedirectUrl, + }); + } + + return () => { + const children = normalizeWithDefaultValue(slots.default?.(), 'Sign in'); + const child = assertSingleChild(children, 'SignInButton'); + return h(child, { + ...attrs, + onClick: clickHandler, + }); + }; + }, + { + props: ['signUpForceRedirectUrl', 'signUpFallbackRedirectUrl', 'fallbackRedirectUrl', 'forceRedirectUrl', 'mode'], + }, +); diff --git a/packages/vue/src/components/SignInWithMetamaskButton.ts b/packages/vue/src/components/SignInWithMetamaskButton.ts new file mode 100644 index 00000000000..765f348f9ec --- /dev/null +++ b/packages/vue/src/components/SignInWithMetamaskButton.ts @@ -0,0 +1,32 @@ +import type { RedirectUrlProp } from '@clerk/types'; +import { defineComponent, h } from 'vue'; + +import { useClerk } from '../composables/useClerk'; +import { assertSingleChild, normalizeWithDefaultValue } from '../utils'; + +export const SignInWithMetamaskButton = defineComponent( + ( + props: RedirectUrlProp & { + mode?: 'modal' | 'redirect'; + }, + { slots, attrs }, + ) => { + const clerk = useClerk(); + + function clickHandler() { + void clerk.value?.authenticateWithMetamask({ redirectUrl: props.redirectUrl || undefined }); + } + + return () => { + const children = normalizeWithDefaultValue(slots.default?.(), 'Sign in with Metamask'); + const child = assertSingleChild(children, 'SignInWithMetamaskButton'); + return h(child, { + ...attrs, + onClick: clickHandler, + }); + }; + }, + { + props: ['redirectUrl', 'mode'], + }, +); diff --git a/packages/vue/src/components/SignOutButton.ts b/packages/vue/src/components/SignOutButton.ts new file mode 100644 index 00000000000..d5745343dc9 --- /dev/null +++ b/packages/vue/src/components/SignOutButton.ts @@ -0,0 +1,37 @@ +import type { SignOutOptions } from '@clerk/types'; +import { defineComponent, h } from 'vue'; + +import { useClerk } from '../composables/useClerk'; +import { assertSingleChild, normalizeWithDefaultValue } from '../utils'; + +interface SignOutButtonProps { + signOutOptions?: SignOutOptions; + sessionId?: string; + redirectUrl?: string; +} + +export const SignOutButton = defineComponent( + (props: SignOutButtonProps, { slots, attrs }) => { + const clerk = useClerk(); + + function clickHandler() { + const signOutOptions: SignOutOptions = { + redirectUrl: props.signOutOptions?.redirectUrl ?? props.redirectUrl, + sessionId: props.signOutOptions?.sessionId ?? props.sessionId, + }; + void clerk.value?.signOut(signOutOptions); + } + + return () => { + const children = normalizeWithDefaultValue(slots.default?.(), 'Sign out'); + const child = assertSingleChild(children, 'SignOutButton'); + return h(child, { + ...attrs, + onClick: clickHandler, + }); + }; + }, + { + props: ['signOutOptions', 'sessionId', 'redirectUrl'], + }, +); diff --git a/packages/vue/src/components/SignUpButton.ts b/packages/vue/src/components/SignUpButton.ts new file mode 100644 index 00000000000..29281cac8d6 --- /dev/null +++ b/packages/vue/src/components/SignUpButton.ts @@ -0,0 +1,56 @@ +import type { SignUpProps } from '@clerk/types'; +import { defineComponent, h } from 'vue'; + +import { useClerk } from '../composables/useClerk'; +import { assertSingleChild, normalizeWithDefaultValue } from '../utils'; + +type SignUpButtonProps = { + unsafeMetadata?: SignUpUnsafeMetadata; +} & Pick< + SignUpProps, + 'fallbackRedirectUrl' | 'forceRedirectUrl' | 'signInForceRedirectUrl' | 'signInFallbackRedirectUrl' +>; + +export const SignUpButton = defineComponent( + ( + props: SignUpButtonProps & { + mode?: 'modal' | 'redirect'; + }, + { slots, attrs }, + ) => { + const clerk = useClerk(); + + function clickHandler() { + const { mode, ...opts } = props; + + if (mode === 'modal') { + return clerk.value?.openSignUp(opts); + } + + void clerk.value?.redirectToSignUp({ + ...opts, + signUpFallbackRedirectUrl: props.fallbackRedirectUrl, + signUpForceRedirectUrl: props.forceRedirectUrl, + }); + } + + return () => { + const children = normalizeWithDefaultValue(slots.default?.(), 'Sign up'); + const child = assertSingleChild(children, 'SignUpButton'); + return h(child, { + ...attrs, + onClick: clickHandler, + }); + }; + }, + { + props: [ + 'unsafeMetadata', + 'signInForceRedirectUrl', + 'signInFallbackRedirectUrl', + 'fallbackRedirectUrl', + 'forceRedirectUrl', + 'mode', + ], + }, +); diff --git a/packages/vue/src/components/__tests__/SignInButton.test.ts b/packages/vue/src/components/__tests__/SignInButton.test.ts new file mode 100644 index 00000000000..4850e12e459 --- /dev/null +++ b/packages/vue/src/components/__tests__/SignInButton.test.ts @@ -0,0 +1,100 @@ +import { userEvent } from '@testing-library/user-event'; +import { render, screen } from '@testing-library/vue'; +import { vi } from 'vitest'; +import { defineComponent, h, ref } from 'vue'; + +import { SignInButton } from '../SignInButton'; + +const mockRedirectToSignIn = vi.fn(); +const originalError = console.error; + +vi.mock('../../composables/useClerk', () => ({ + useClerk: () => + ref({ + redirectToSignIn: mockRedirectToSignIn, + }), +})); + +const url = 'https://www.clerk.com'; + +describe('', () => { + beforeAll(() => { + console.error = vi.fn(); + }); + + beforeEach(() => { + mockRedirectToSignIn.mockReset(); + }); + + afterAll(() => { + console.error = originalError; + }); + + it('calls clerk.redirectToSignIn when clicked', async () => { + render(SignInButton); + const btn = screen.getByText('Sign in'); + await userEvent.click(btn); + expect(mockRedirectToSignIn).toHaveBeenCalled(); + }); + + it('handles forceRedirectUrl prop', async () => { + render(SignInButton, { + props: { + forceRedirectUrl: url, + }, + }); + const btn = screen.getByText('Sign in'); + await userEvent.click(btn); + + expect(mockRedirectToSignIn).toHaveBeenCalledWith({ forceRedirectUrl: url, signInForceRedirectUrl: url }); + }); + + it('handles fallbackRedirectUrl prop', async () => { + render(SignInButton, { + props: { + fallbackRedirectUrl: url, + }, + }); + const btn = screen.getByText('Sign in'); + await userEvent.click(btn); + + expect(mockRedirectToSignIn).toHaveBeenCalledWith({ + fallbackRedirectUrl: url, + signInFallbackRedirectUrl: url, + }); + }); + + it('renders passed button and calls both click handlers', async () => { + const handler = vi.fn(); + + const Button = defineComponent(() => { + return () => h(SignInButton, {}, () => h('button', { onClick: handler, type: 'button' }, 'custom button')); + }); + + render(Button); + + const btn = screen.getByText('custom button'); + await userEvent.click(btn); + + expect(handler).toHaveBeenCalled(); + expect(mockRedirectToSignIn).toHaveBeenCalled(); + }); + + it('uses text passed as children', async () => { + const Button = defineComponent(() => { + return () => h(SignInButton, () => 'text'); + }); + render(Button); + screen.getByText('text'); + }); + + it('throws if multiple children provided', async () => { + const Button = defineComponent(() => { + return () => h(SignInButton, () => [h('button', { type: 'button' }, '1'), h('button', { type: 'button' }, '2')]); + }); + + expect(() => { + render(Button); + }).toThrow(); + }); +}); diff --git a/packages/vue/src/components/__tests__/SignInWithMetamaskButton.test.ts b/packages/vue/src/components/__tests__/SignInWithMetamaskButton.test.ts new file mode 100644 index 00000000000..c8a78456f2c --- /dev/null +++ b/packages/vue/src/components/__tests__/SignInWithMetamaskButton.test.ts @@ -0,0 +1,71 @@ +import { userEvent } from '@testing-library/user-event'; +import { render, screen } from '@testing-library/vue'; +import { vi } from 'vitest'; +import { defineComponent, h, ref } from 'vue'; + +import { SignInWithMetamaskButton } from '../SignInWithMetamaskButton'; + +const mockAuthenticatewithMetamask = vi.fn(); +const originalError = console.error; + +vi.mock('../../composables/useClerk', () => ({ + useClerk: () => + ref({ + authenticateWithMetamask: mockAuthenticatewithMetamask, + }), +})); + +const url = 'https://www.clerk.com'; + +describe('', () => { + beforeAll(() => { + console.error = vi.fn(); + }); + + beforeEach(() => { + mockAuthenticatewithMetamask.mockReset(); + }); + + afterAll(() => { + console.error = originalError; + }); + + it('calls clerk.authenticateWithMetamask when clicked', async () => { + render(SignInWithMetamaskButton); + const btn = screen.getByText('Sign in with Metamask'); + await userEvent.click(btn); + + expect(mockAuthenticatewithMetamask).toHaveBeenCalled(); + }); + + it('handles redirectUrl prop', async () => { + render(SignInWithMetamaskButton, { + props: { + redirectUrl: url, + }, + }); + const btn = screen.getByText('Sign in with Metamask'); + await userEvent.click(btn); + + expect(mockAuthenticatewithMetamask).toHaveBeenCalledWith({ redirectUrl: url }); + }); + + it('uses text passed as children', async () => { + const Button = defineComponent(() => { + return () => h(SignInWithMetamaskButton, () => 'text'); + }); + render(Button); + screen.getByText('text'); + }); + + it('throws if multiple children provided', async () => { + const Button = defineComponent(() => { + return () => + h(SignInWithMetamaskButton, () => [h('button', { type: 'button' }, '1'), h('button', { type: 'button' }, '2')]); + }); + + expect(() => { + render(Button); + }).toThrow(); + }); +}); diff --git a/packages/vue/src/components/__tests__/SignOutButton.test.ts b/packages/vue/src/components/__tests__/SignOutButton.test.ts new file mode 100644 index 00000000000..0415a66478a --- /dev/null +++ b/packages/vue/src/components/__tests__/SignOutButton.test.ts @@ -0,0 +1,83 @@ +import { userEvent } from '@testing-library/user-event'; +import { render, screen } from '@testing-library/vue'; +import { vi } from 'vitest'; +import { defineComponent, h, ref } from 'vue'; + +import { SignOutButton } from '../SignOutButton'; + +const mockSignOut = vi.fn(); +const originalError = console.error; + +vi.mock('../../composables/useClerk', () => ({ + useClerk: () => + ref({ + signOut: mockSignOut, + }), +})); + +const url = 'https://www.clerk.com'; + +describe('', () => { + beforeAll(() => { + console.error = vi.fn(); + }); + + beforeEach(() => { + mockSignOut.mockReset(); + }); + + afterAll(() => { + console.error = originalError; + }); + + it('calls clerk.signOutOne when clicked', async () => { + render(SignOutButton); + const btn = screen.getByText('Sign out'); + await userEvent.click(btn); + + expect(mockSignOut).toHaveBeenCalled(); + }); + + it('handles redirectUrl prop', async () => { + render(SignOutButton, { + props: { + redirectUrl: url, + }, + }); + const btn = screen.getByText('Sign out'); + await userEvent.click(btn); + + expect(mockSignOut).toHaveBeenCalledWith({ redirectUrl: url }); + }); + + it('handles signOutOptions prop', async () => { + render(SignOutButton, { + props: { + redirectUrl: url, + sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe', + }, + }); + const btn = screen.getByText('Sign out'); + await userEvent.click(btn); + + expect(mockSignOut).toHaveBeenCalledWith({ redirectUrl: url, sessionId: 'sess_1yDceUR8SIKtQ0gIOO8fNsW7nhe' }); + }); + + it('uses text passed as children', async () => { + const Button = defineComponent(() => { + return () => h(SignOutButton, () => 'text'); + }); + render(Button); + screen.getByText('text'); + }); + + it('throws if multiple children provided', async () => { + const Button = defineComponent(() => { + return () => h(SignOutButton, () => [h('button', { type: 'button' }, '1'), h('button', { type: 'button' }, '2')]); + }); + + expect(() => { + render(Button); + }).toThrow(); + }); +}); diff --git a/packages/vue/src/components/__tests__/SignUpButton.test.ts b/packages/vue/src/components/__tests__/SignUpButton.test.ts new file mode 100644 index 00000000000..3b33b4301c1 --- /dev/null +++ b/packages/vue/src/components/__tests__/SignUpButton.test.ts @@ -0,0 +1,101 @@ +import { userEvent } from '@testing-library/user-event'; +import { render, screen } from '@testing-library/vue'; +import { vi } from 'vitest'; +import { defineComponent, h, ref } from 'vue'; + +import { SignUpButton } from '../SignUpButton'; + +const mockRedirectToSignUp = vi.fn(); +const originalError = console.error; + +vi.mock('../../composables/useClerk', () => ({ + useClerk: () => + ref({ + redirectToSignUp: mockRedirectToSignUp, + }), +})); + +const url = 'https://www.clerk.com'; + +describe('', () => { + beforeAll(() => { + console.error = vi.fn(); + }); + + beforeEach(() => { + mockRedirectToSignUp.mockReset(); + }); + + afterAll(() => { + console.error = originalError; + }); + + it('calls clerk.redirectToSignUp when clicked', async () => { + render(SignUpButton); + const btn = screen.getByText('Sign up'); + await userEvent.click(btn); + + expect(mockRedirectToSignUp).toHaveBeenCalled(); + }); + + it('handles forceRedirectUrl prop', async () => { + render(SignUpButton, { + props: { + forceRedirectUrl: url, + }, + }); + const btn = screen.getByText('Sign up'); + await userEvent.click(btn); + + expect(mockRedirectToSignUp).toHaveBeenCalledWith({ forceRedirectUrl: url, signUpForceRedirectUrl: url }); + }); + + it('handles fallbackRedirectUrl prop', async () => { + render(SignUpButton, { + props: { + fallbackRedirectUrl: url, + }, + }); + const btn = screen.getByText('Sign up'); + await userEvent.click(btn); + + expect(mockRedirectToSignUp).toHaveBeenCalledWith({ + fallbackRedirectUrl: url, + signUpFallbackRedirectUrl: url, + }); + }); + + it('renders passed button and calls both click handlers', async () => { + const handler = vi.fn(); + + const Button = defineComponent(() => { + return () => h(SignUpButton, null, () => h('button', { onClick: handler, type: 'button' }, 'custom button')); + }); + + render(Button); + + const btn = screen.getByText('custom button'); + await userEvent.click(btn); + + expect(handler).toHaveBeenCalled(); + expect(mockRedirectToSignUp).toHaveBeenCalled(); + }); + + it('uses text passed as children', async () => { + const Button = defineComponent(() => { + return () => h(SignUpButton, () => 'text'); + }); + render(Button); + screen.getByText('text'); + }); + + it('throws if multiple children provided', async () => { + const Button = defineComponent(() => { + return () => h(SignUpButton, () => [h('button', { type: 'button' }, '1'), h('button', { type: 'button' }, '2')]); + }); + + expect(() => { + render(Button); + }).toThrow(); + }); +}); diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts new file mode 100644 index 00000000000..8b6eee4d0d5 --- /dev/null +++ b/packages/vue/src/components/controlComponents.ts @@ -0,0 +1,171 @@ +import type { + CheckAuthorizationWithCustomPermissions, + HandleOAuthCallbackParams as HandleOAuthCallbackParamsOriginal, + OrganizationCustomPermissionKey, + OrganizationCustomRoleKey, + RedirectOptions, +} from '@clerk/types'; +import { defineComponent } from 'vue'; + +import { useAuth } from '../composables/useAuth'; +import { useClerk } from '../composables/useClerk'; +import { useClerkContext } from '../composables/useClerkContext'; +import { useClerkLoaded } from '../utils/useClerkLoaded'; + +export const SignedIn = defineComponent((_, { slots }) => { + const { userId } = useAuth(); + + return () => (userId.value ? slots.default?.() : null); +}); + +export const SignedOut = defineComponent((_, { slots }) => { + const { userId } = useAuth(); + + return () => (userId.value === null ? slots.default?.() : null); +}); + +export const ClerkLoaded = defineComponent((_, { slots }) => { + const clerk = useClerk(); + + return () => (clerk.value?.loaded ? slots.default?.() : null); +}); + +export const ClerkLoading = defineComponent((_, { slots }) => { + const clerk = useClerk(); + + return () => (!clerk.value?.loaded ? slots.default?.() : null); +}); + +export const RedirectToSignIn = defineComponent((props: RedirectOptions) => { + const { sessionCtx, clientCtx } = useClerkContext(); + + useClerkLoaded(clerk => { + const hasActiveSessions = clientCtx.value?.activeSessions && clientCtx.value.activeSessions.length > 0; + + if (sessionCtx.value === null && hasActiveSessions) { + void clerk.redirectToAfterSignOut(); + } else { + void clerk.redirectToSignIn(props); + } + }); + + return () => null; +}); + +export const RedirectToSignUp = defineComponent((props: RedirectOptions) => { + useClerkLoaded(clerk => { + void clerk.redirectToSignUp(props); + }); + + return () => null; +}); + +export const RedirectToUserProfile = defineComponent(() => { + useClerkLoaded(clerk => { + void clerk.redirectToUserProfile(); + }); + + return () => null; +}); + +export const RedirectToOrganizationProfile = defineComponent(() => { + useClerkLoaded(clerk => { + void clerk.redirectToOrganizationProfile(); + }); + + return () => null; +}); + +export const RedirectToCreateOrganization = defineComponent(() => { + useClerkLoaded(clerk => { + void clerk.redirectToCreateOrganization(); + }); + + return () => null; +}); + +// TODO: Fix this later and export `Transferable` type from @clerk/types +// to fix the exported variable error in TS +type HandleOAuthCallbackParams = Omit & { + /** + * Indicates whether or not sign in attempts are transferable to the sign up flow. + * When set to false, prevents opaque sign ups when a user attempts to sign in via OAuth with an email that doesn't exist. + * @default true + */ + transferable?: boolean; +}; + +export const AuthenticateWithRedirectCallback = defineComponent((props: HandleOAuthCallbackParams) => { + useClerkLoaded(clerk => { + void clerk.handleRedirectCallback(props); + }); + + return () => null; +}); + +export type ProtectProps = + | { + condition?: never; + role: OrganizationCustomRoleKey; + permission?: never; + } + | { + condition?: never; + role?: never; + permission: OrganizationCustomPermissionKey; + } + | { + condition: (has: CheckAuthorizationWithCustomPermissions) => boolean; + role?: never; + permission?: never; + } + | { + condition?: never; + role?: never; + permission?: never; + }; + +export const Protect = defineComponent((props: ProtectProps, { slots }) => { + const { isLoaded, has, userId } = useAuth(); + + return () => { + /** + * Avoid flickering children or fallback while clerk is loading sessionId or userId + */ + if (!isLoaded.value) { + return null; + } + + /** + * Fallback to UI provided by user or `null` if authorization checks failed + */ + if (!userId.value) { + return slots.fallback?.(); + } + + /** + * Check against the results of `has` called inside the callback + */ + if (typeof props.condition === 'function') { + if (props.condition(has.value!)) { + return slots.default?.(); + } + + return slots.fallback?.(); + } + + if (props.role || props.permission) { + if (has.value?.(props)) { + return slots.default?.(); + } + + return slots.fallback?.(); + } + + /** + * If neither of the authorization params are passed behave as the ``. + * If fallback is present render that instead of rendering nothing. + */ + return slots.default?.(); + }; +}); diff --git a/packages/vue/src/components/index.ts b/packages/vue/src/components/index.ts new file mode 100644 index 00000000000..1fdcb0edc02 --- /dev/null +++ b/packages/vue/src/components/index.ts @@ -0,0 +1,31 @@ +export { + SignUp, + SignIn, + UserProfile, + UserButton, + OrganizationSwitcher, + OrganizationProfile, + CreateOrganization, + OrganizationList, + GoogleOneTap, + Waitlist, +} from './uiComponents'; + +export { + ClerkLoaded, + ClerkLoading, + SignedOut, + SignedIn, + Protect, + RedirectToSignIn, + RedirectToSignUp, + RedirectToUserProfile, + AuthenticateWithRedirectCallback, + RedirectToCreateOrganization, + RedirectToOrganizationProfile, +} from './controlComponents'; + +export { SignInButton } from './SignInButton'; +export { SignUpButton } from './SignUpButton'; +export { SignOutButton } from './SignOutButton'; +export { SignInWithMetamaskButton } from './SignInWithMetamaskButton'; diff --git a/packages/vue/src/components/uiComponents.ts b/packages/vue/src/components/uiComponents.ts new file mode 100644 index 00000000000..46b05b50d25 --- /dev/null +++ b/packages/vue/src/components/uiComponents.ts @@ -0,0 +1,176 @@ +import type { + CreateOrganizationProps, + GoogleOneTapProps, + OrganizationListProps, + OrganizationProfileProps, + OrganizationSwitcherProps, + SignInProps, + SignUpProps, + UserButtonProps, + UserProfileProps, + WaitlistProps, +} from '@clerk/types'; +import { computed, defineComponent, h, onScopeDispose, ref, watchEffect } from 'vue'; + +import { useClerk } from '../composables/useClerk'; +import { ClerkLoaded } from './controlComponents'; + +type AnyObject = Record; + +interface MountProps { + mount: ((node: HTMLDivElement, props: AnyObject) => void) | undefined; + unmount: ((node: HTMLDivElement) => void) | undefined; + updateProps?: (props: { node: HTMLDivElement; props: AnyObject | undefined }) => void; + props?: AnyObject; +} + +/** + * A utility component that handles mounting and unmounting of Clerk UI components. + * The component only mounts when Clerk is fully loaded and automatically + * handles cleanup on unmount. + */ +const Portal = defineComponent((props: MountProps) => { + const portalRef = ref(null); + const isPortalMounted = ref(false); + // Make the props reactive so the watcher can react to changes + const componentProps = computed(() => ({ ...props.props })); + + watchEffect(() => { + if (!portalRef.value) { + return; + } + + if (isPortalMounted.value) { + props.updateProps?.({ node: portalRef.value, props: componentProps.value }); + } else { + props.mount?.(portalRef.value, componentProps.value); + isPortalMounted.value = true; + } + }); + + onScopeDispose(() => { + if (isPortalMounted.value && portalRef.value) { + props.unmount?.(portalRef.value); + } + }); + + return () => h(ClerkLoaded, () => h('div', { ref: portalRef })); +}); + +export const UserProfile = defineComponent((props: UserProfileProps) => { + const clerk = useClerk(); + + return () => + h(Portal, { + mount: clerk.value?.mountUserProfile, + unmount: clerk.value?.unmountUserProfile, + updateProps: (clerk.value as any)?.__unstable__updateProps, + props, + }); +}); + +export const UserButton = defineComponent((props: UserButtonProps) => { + const clerk = useClerk(); + + return () => + h(Portal, { + mount: clerk.value?.mountUserButton, + unmount: clerk.value?.unmountUserButton, + updateProps: (clerk.value as any)?.__unstable__updateProps, + props, + }); +}); + +export const GoogleOneTap = defineComponent((props: GoogleOneTapProps) => { + const clerk = useClerk(); + + return () => + h(Portal, { + mount: () => clerk.value?.openGoogleOneTap(props), + unmount: clerk.value?.closeGoogleOneTap, + }); +}); + +export const SignIn = defineComponent((props: SignInProps) => { + const clerk = useClerk(); + + return () => + h(Portal, { + mount: clerk.value?.mountSignIn, + unmount: clerk.value?.unmountSignIn, + updateProps: (clerk.value as any)?.__unstable__updateProps, + props, + }); +}); + +export const SignUp = defineComponent((props: SignUpProps) => { + const clerk = useClerk(); + + return () => + h(Portal, { + mount: clerk.value?.mountSignUp, + unmount: clerk.value?.unmountSignUp, + updateProps: (clerk.value as any)?.__unstable__updateProps, + props, + }); +}); + +export const CreateOrganization = defineComponent((props: CreateOrganizationProps) => { + const clerk = useClerk(); + + return () => + h(Portal, { + mount: clerk.value?.mountCreateOrganization, + unmount: clerk.value?.unmountCreateOrganization, + updateProps: (clerk.value as any)?.__unstable__updateProps, + props, + }); +}); + +export const OrganizationSwitcher = defineComponent((props: OrganizationSwitcherProps) => { + const clerk = useClerk(); + + return () => + h(Portal, { + mount: clerk.value?.mountOrganizationSwitcher, + unmount: clerk.value?.unmountOrganizationSwitcher, + updateProps: (clerk.value as any)?.__unstable__updateProps, + props, + }); +}); + +export const OrganizationList = defineComponent((props: OrganizationListProps) => { + const clerk = useClerk(); + + return () => + h(Portal, { + mount: clerk.value?.mountOrganizationList, + unmount: clerk.value?.unmountOrganizationList, + updateProps: (clerk.value as any)?.__unstable__updateProps, + props, + }); +}); + +export const OrganizationProfile = defineComponent((props: OrganizationProfileProps) => { + const clerk = useClerk(); + + return () => + h(Portal, { + mount: clerk.value?.mountOrganizationProfile, + unmount: clerk.value?.unmountOrganizationProfile, + updateProps: (clerk.value as any)?.__unstable__updateProps, + props, + }); +}); + +export const Waitlist = defineComponent((props: WaitlistProps) => { + const clerk = useClerk(); + + return () => + h(Portal, { + mount: clerk.value?.mountWaitlist, + unmount: clerk.value?.unmountWaitlist, + updateProps: (clerk.value as any)?.__unstable__updateProps, + props, + }); +}); diff --git a/packages/vue/src/composables/index.ts b/packages/vue/src/composables/index.ts new file mode 100644 index 00000000000..9ca30fcc06c --- /dev/null +++ b/packages/vue/src/composables/index.ts @@ -0,0 +1,15 @@ +export { useAuth } from './useAuth'; + +export { useUser } from './useUser'; + +export { useSession } from './useSession'; + +export { useClerk } from './useClerk'; + +export { useSignIn } from './useSignIn'; + +export { useSignUp } from './useSignUp'; + +export { useSessionList } from './useSessionList'; + +export { useOrganization } from './useOrganization'; diff --git a/packages/vue/src/composables/useAuth.ts b/packages/vue/src/composables/useAuth.ts new file mode 100644 index 00000000000..e6d0fb07eed --- /dev/null +++ b/packages/vue/src/composables/useAuth.ts @@ -0,0 +1,233 @@ +import type { + ActJWTClaim, + CheckAuthorizationWithCustomPermissions, + Clerk, + GetToken, + OrganizationCustomRoleKey, + SignOut, +} from '@clerk/types'; +import { computed, type ShallowRef, watch } from 'vue'; + +import { errorThrower } from '../errors/errorThrower'; +import { invalidStateError, useAuthHasRequiresRoleOrPermission } from '../errors/messages'; +import type { ToComputedRefs } from '../utils'; +import { toComputedRefs } from '../utils'; +import { useClerkContext } from './useClerkContext'; + +type CheckAuthorizationSignedOut = undefined; +type CheckAuthorizationWithoutOrgOrUser = (params?: Parameters[0]) => false; + +/** + * @internal + */ +function clerkLoaded(clerk: ShallowRef) { + return new Promise(resolve => { + const unwatch = watch(clerk, value => { + if (value?.loaded) { + unwatch(); + resolve(clerk.value!); + } + }); + }); +} + +/** + * @internal + */ +function createGetToken(clerk: ShallowRef) { + return async (options: any) => { + const loadedClerk = await clerkLoaded(clerk); + if (!loadedClerk.session) { + return null; + } + + return loadedClerk.session.getToken(options); + }; +} + +/** + * @internal + */ +function createSignOut(clerk: ShallowRef) { + return async (...args: any) => { + const loadedClerk = await clerkLoaded(clerk); + return loadedClerk.signOut(...args); + }; +} + +type UseAuthReturn = + | { + isLoaded: false; + isSignedIn: undefined; + userId: undefined; + sessionId: undefined; + actor: undefined; + orgId: undefined; + orgRole: undefined; + orgSlug: undefined; + has: CheckAuthorizationSignedOut; + signOut: SignOut; + getToken: GetToken; + } + | { + isLoaded: true; + isSignedIn: false; + userId: null; + sessionId: null; + actor: null; + orgId: null; + orgRole: null; + orgSlug: null; + has: CheckAuthorizationWithoutOrgOrUser; + signOut: SignOut; + getToken: GetToken; + } + | { + isLoaded: true; + isSignedIn: true; + userId: string; + sessionId: string; + actor: ActJWTClaim | null; + orgId: null; + orgRole: null; + orgSlug: null; + has: CheckAuthorizationWithoutOrgOrUser; + signOut: SignOut; + getToken: GetToken; + } + | { + isLoaded: true; + isSignedIn: true; + userId: string; + sessionId: string; + actor: ActJWTClaim | null; + orgId: string; + orgRole: OrganizationCustomRoleKey; + orgSlug: string | null; + has: CheckAuthorizationWithCustomPermissions; + signOut: SignOut; + getToken: GetToken; + }; + +type UseAuth = () => ToComputedRefs; + +/** + * Returns the current auth state, the user and session ids and the `getToken` + * that can be used to retrieve the given template or the default Clerk token. + * + * Until Clerk loads, `isLoaded` will be set to `false`. + * Once Clerk loads, `isLoaded` will be set to `true`, and you can + * safely access the `userId` and `sessionId` variables. + * + * @example + * + * + * + * + */ +export const useAuth: UseAuth = () => { + const { clerk, authCtx } = useClerkContext(); + + const result = computed(() => { + const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = authCtx.value; + + const getToken: GetToken = createGetToken(clerk); + const signOut: SignOut = createSignOut(clerk); + + const has = (params: Parameters[0]) => { + if (!params?.permission && !params?.role) { + return errorThrower.throw(useAuthHasRequiresRoleOrPermission); + } + if (!orgId || !userId || !orgRole || !orgPermissions) { + return false; + } + + if (params.permission) { + return orgPermissions.includes(params.permission); + } + + if (params.role) { + return orgRole === params.role; + } + + return false; + }; + + if (sessionId === undefined && userId === undefined) { + return { + isLoaded: false, + isSignedIn: undefined, + sessionId, + userId, + actor: undefined, + orgId: undefined, + orgRole: undefined, + orgSlug: undefined, + has: undefined, + signOut, + getToken, + }; + } + + if (sessionId === null && userId === null) { + return { + isLoaded: true, + isSignedIn: false, + sessionId, + userId, + actor: null, + orgId: null, + orgRole: null, + orgSlug: null, + has: () => false, + signOut, + getToken, + }; + } + + if (!!sessionId && !!userId && !!orgId && !!orgRole) { + return { + isLoaded: true, + isSignedIn: true, + sessionId, + userId, + actor: actor || null, + orgId, + orgRole, + orgSlug: orgSlug || null, + has, + signOut, + getToken, + }; + } + + if (!!sessionId && !!userId && !orgId) { + return { + isLoaded: true, + isSignedIn: true, + sessionId, + userId, + actor: actor || null, + orgId: null, + orgRole: null, + orgSlug: null, + has: () => false, + signOut, + getToken, + }; + } + + return errorThrower.throw(invalidStateError); + }); + + return toComputedRefs(result); +}; diff --git a/packages/vue/src/composables/useClerk.ts b/packages/vue/src/composables/useClerk.ts new file mode 100644 index 00000000000..7b473528962 --- /dev/null +++ b/packages/vue/src/composables/useClerk.ts @@ -0,0 +1,25 @@ +import { useClerkContext } from './useClerkContext'; + +/** + * Returns the [`Clerk`](https://clerk.com/docs/references/javascript/clerk/clerk) instance which provides + * low-level access to Clerk's functionality, enabling custom authentication UI and flows. + * + * @example + * + * + * + * + */ +export const useClerk = () => { + const { clerk } = useClerkContext(); + + return clerk; +}; diff --git a/packages/vue/src/composables/useClerkContext.ts b/packages/vue/src/composables/useClerkContext.ts new file mode 100644 index 00000000000..6001a8fcb99 --- /dev/null +++ b/packages/vue/src/composables/useClerkContext.ts @@ -0,0 +1,15 @@ +import { inject } from 'vue'; + +import { ClerkInjectionKey } from '../keys'; + +export function useClerkContext() { + const ctx = inject(ClerkInjectionKey); + + if (!ctx) { + throw new Error( + 'This component/composable can only be used when the Vue plugin is installed. Learn more: https://clerk.com/docs/quickstarts/vue', + ); + } + + return ctx; +} diff --git a/packages/vue/src/composables/useOrganization.ts b/packages/vue/src/composables/useOrganization.ts new file mode 100644 index 00000000000..4765f97d069 --- /dev/null +++ b/packages/vue/src/composables/useOrganization.ts @@ -0,0 +1,97 @@ +import type { OrganizationMembershipResource, OrganizationResource } from '@clerk/types'; +import { computed } from 'vue'; + +import type { ToComputedRefs } from '../utils'; +import { toComputedRefs } from '../utils'; +import { useClerkContext } from './useClerkContext'; +import { useSession } from './useSession'; + +type UseOrganizationReturn = + | { + isLoaded: false; + organization: undefined; + membership: undefined; + } + | { + isLoaded: true; + organization: OrganizationResource; + membership: undefined; + } + | { + isLoaded: boolean; + organization: OrganizationResource | null; + membership: OrganizationMembershipResource | null | undefined; + }; + +type UseOrganization = () => ToComputedRefs; + +/** + * Returns the current [`Organization`](https://clerk.com/docs/references/javascript/organization/organization) object + * along with loading states and membership information. + * + * @example + * + * + * + * + */ +export const useOrganization: UseOrganization = () => { + const { clerk, organizationCtx } = useClerkContext(); + const { session } = useSession(); + + const result = computed(() => { + if (organizationCtx.value === undefined) { + return { isLoaded: false, organization: undefined, membership: undefined }; + } + + if (organizationCtx.value === null) { + return { isLoaded: true, organization: null, membership: null }; + } + + /** In SSR context we include only the organization object when loadOrg is set to true. */ + if (!clerk.value?.loaded) { + return { + isLoaded: true, + organization: organizationCtx.value, + membership: undefined, + }; + } + + return { + isLoaded: clerk.value.loaded, + organization: organizationCtx.value, + membership: getCurrentOrganizationMembership( + session.value!.user.organizationMemberships, + organizationCtx.value.id, + ), + }; + }); + + return toComputedRefs(result); +}; + +function getCurrentOrganizationMembership( + organizationMemberships: OrganizationMembershipResource[], + activeOrganizationId: string, +) { + return organizationMemberships.find( + organizationMembership => organizationMembership.organization.id === activeOrganizationId, + ); +} diff --git a/packages/vue/src/composables/useSession.ts b/packages/vue/src/composables/useSession.ts new file mode 100644 index 00000000000..b0fec7e536f --- /dev/null +++ b/packages/vue/src/composables/useSession.ts @@ -0,0 +1,57 @@ +import type { ActiveSessionResource } from '@clerk/types'; +import { computed } from 'vue'; + +import type { ToComputedRefs } from '../utils'; +import { toComputedRefs } from '../utils'; +import { useClerkContext } from './useClerkContext'; + +type UseSessionReturn = + | { isLoaded: false; isSignedIn: undefined; session: undefined } + | { isLoaded: true; isSignedIn: false; session: null } + | { isLoaded: true; isSignedIn: true; session: ActiveSessionResource }; + +type UseSession = () => ToComputedRefs; + +/** + * Returns the current [`Session`](https://clerk.com/docs/references/javascript/session) object which provides + * information about the active session and methods to manage it. + * + * @example + * + * + * + * + */ +export const useSession: UseSession = () => { + const { sessionCtx } = useClerkContext(); + + const result = computed(() => { + if (sessionCtx.value === undefined) { + return { isLoaded: false, isSignedIn: undefined, session: undefined }; + } + + if (sessionCtx.value === null) { + return { isLoaded: true, isSignedIn: false, session: null }; + } + + return { isLoaded: true, isSignedIn: true, session: sessionCtx.value }; + }); + + return toComputedRefs(result); +}; diff --git a/packages/vue/src/composables/useSessionList.ts b/packages/vue/src/composables/useSessionList.ts new file mode 100644 index 00000000000..e51bdb783c5 --- /dev/null +++ b/packages/vue/src/composables/useSessionList.ts @@ -0,0 +1,55 @@ +import type { SessionResource, SetActive } from '@clerk/types'; +import { computed } from 'vue'; + +import type { ToComputedRefs } from '../utils'; +import { toComputedRefs } from '../utils'; +import { useClerkContext } from './useClerkContext'; + +type UseSessionListReturn = + | { isLoaded: false; sessions: undefined; setActive: undefined } + | { isLoaded: true; sessions: SessionResource[]; setActive: SetActive }; + +type UseSessionList = () => ToComputedRefs; + +/** + * Returns an array of [`Session`](https://clerk.com/docs/references/javascript/session) objects that have been + * registered on the client device. + * + * @example + * + * + * + * + */ +export const useSessionList: UseSessionList = () => { + const { clerk, clientCtx } = useClerkContext(); + + const result = computed(() => { + if (!clientCtx.value) { + return { isLoaded: false, sessions: undefined, setActive: undefined }; + } + + return { + isLoaded: true, + sessions: clientCtx.value.sessions, + setActive: clerk.value!.setActive, + }; + }); + + return toComputedRefs(result); +}; diff --git a/packages/vue/src/composables/useSignIn.ts b/packages/vue/src/composables/useSignIn.ts new file mode 100644 index 00000000000..fb1e660abd6 --- /dev/null +++ b/packages/vue/src/composables/useSignIn.ts @@ -0,0 +1,60 @@ +import { eventMethodCalled } from '@clerk/shared/telemetry'; +import type { SetActive, SignInResource } from '@clerk/types'; +import { computed, watch } from 'vue'; + +import type { ToComputedRefs } from '../utils'; +import { toComputedRefs } from '../utils'; +import { useClerkContext } from './useClerkContext'; + +type UseSignInReturn = + | { isLoaded: false; signIn: undefined; setActive: undefined } + | { isLoaded: true; signIn: SignInResource; setActive: SetActive }; + +type UseSignIn = () => ToComputedRefs; + +/** + * Returns the current [`SignIn`](https://clerk.com/docs/references/javascript/sign-in/sign-in) object which provides + * methods and state for managing the sign-in flow. + * + * @example + * + * + * + * + */ +export const useSignIn: UseSignIn = () => { + const { clerk, clientCtx } = useClerkContext(); + + const unwatch = watch(clerk, value => { + if (value) { + value.telemetry?.record(eventMethodCalled('useSignIn')); + unwatch(); + } + }); + + const result = computed(() => { + if (!clientCtx.value) { + return { isLoaded: false, signIn: undefined, setActive: undefined }; + } + + return { + isLoaded: true, + signIn: clientCtx.value.signIn, + setActive: clerk.value!.setActive, + }; + }); + + return toComputedRefs(result); +}; diff --git a/packages/vue/src/composables/useSignUp.ts b/packages/vue/src/composables/useSignUp.ts new file mode 100644 index 00000000000..83550d30b0d --- /dev/null +++ b/packages/vue/src/composables/useSignUp.ts @@ -0,0 +1,60 @@ +import { eventMethodCalled } from '@clerk/shared/telemetry'; +import type { SetActive, SignUpResource } from '@clerk/types'; +import { computed, watch } from 'vue'; + +import type { ToComputedRefs } from '../utils'; +import { toComputedRefs } from '../utils'; +import { useClerkContext } from './useClerkContext'; + +type UseSignUpReturn = + | { isLoaded: false; signUp: undefined; setActive: undefined } + | { isLoaded: true; signUp: SignUpResource; setActive: SetActive }; + +type UseSignUp = () => ToComputedRefs; + +/** + * Returns the current [`SignUp`](https://clerk.com/docs/references/javascript/sign-up/sign-up) object which provides + * methods and state for managing the sign-up flow. + * + * @example + * + * + * + * + */ +export const useSignUp: UseSignUp = () => { + const { clerk, clientCtx } = useClerkContext(); + + const unwatch = watch(clerk, value => { + if (value) { + value.telemetry?.record(eventMethodCalled('useSignUp')); + unwatch(); + } + }); + + const result = computed(() => { + if (!clientCtx.value) { + return { isLoaded: false, signUp: undefined, setActive: undefined }; + } + + return { + isLoaded: true, + signUp: clientCtx.value.signUp, + setActive: clerk.value!.setActive, + }; + }); + + return toComputedRefs(result); +}; diff --git a/packages/vue/src/composables/useUser.ts b/packages/vue/src/composables/useUser.ts new file mode 100644 index 00000000000..66e916e2fc4 --- /dev/null +++ b/packages/vue/src/composables/useUser.ts @@ -0,0 +1,56 @@ +import type { UserResource } from '@clerk/types'; +import { computed } from 'vue'; + +import type { ToComputedRefs } from '../utils'; +import { toComputedRefs } from '../utils'; +import { useClerkContext } from './useClerkContext'; + +type UseUserReturn = + | { isLoaded: false; isSignedIn: undefined; user: undefined } + | { isLoaded: true; isSignedIn: false; user: null } + | { isLoaded: true; isSignedIn: true; user: UserResource }; + +type UseUser = () => ToComputedRefs; + +/** + * Returns the current user's [`User`](https://clerk.com/docs/references/javascript/user/user) object along with loading states. + * + * @example + * + * + * + * + */ +export const useUser: UseUser = () => { + const { userCtx } = useClerkContext(); + + const result = computed(() => { + if (userCtx.value === undefined) { + return { isLoaded: false, isSignedIn: undefined, user: undefined }; + } + + if (userCtx.value === null) { + return { isLoaded: true, isSignedIn: false, user: null }; + } + + return { isLoaded: true, isSignedIn: true, user: userCtx.value }; + }); + + return toComputedRefs(result); +}; diff --git a/packages/vue/src/errors/errorThrower.ts b/packages/vue/src/errors/errorThrower.ts new file mode 100644 index 00000000000..f6b7cdf8eda --- /dev/null +++ b/packages/vue/src/errors/errorThrower.ts @@ -0,0 +1,15 @@ +import type { ErrorThrowerOptions } from '@clerk/shared/error'; +import { buildErrorThrower } from '@clerk/shared/error'; + +const errorThrower = buildErrorThrower({ packageName: '@clerk/vue' }); + +export { errorThrower }; + +/** + * Overrides options of the internal errorThrower (eg setting packageName prefix). + * + * @internal + */ +export function setErrorThrowerOptions(options: ErrorThrowerOptions) { + errorThrower.setMessages(options).setPackageName(options); +} diff --git a/packages/vue/src/errors/messages.ts b/packages/vue/src/errors/messages.ts new file mode 100644 index 00000000000..786d1ec0d7f --- /dev/null +++ b/packages/vue/src/errors/messages.ts @@ -0,0 +1,8 @@ +export const multipleChildrenInButtonComponent = (name: string) => + `You've passed multiple children components to <${name}/>. You can only pass a single child component or text.`; + +export const invalidStateError = + 'Invalid state. Feel free to submit a bug or reach out to support here: https://clerk.com/support'; + +export const useAuthHasRequiresRoleOrPermission = + 'Missing parameters. `has` from `useAuth` requires a permission or role key to be passed. Example usage: `has({permission: "org:posts:edit"})`'; diff --git a/packages/vue/src/global.d.ts b/packages/vue/src/global.d.ts new file mode 100644 index 00000000000..b07387ba7cd --- /dev/null +++ b/packages/vue/src/global.d.ts @@ -0,0 +1,2 @@ +declare const PACKAGE_NAME: string; +declare const PACKAGE_VERSION: string; diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts new file mode 100644 index 00000000000..3c836dbbe50 --- /dev/null +++ b/packages/vue/src/index.ts @@ -0,0 +1,11 @@ +import { setClerkJsLoadingErrorPackageName } from '@clerk/shared/loadClerkJsScript'; + +import { setErrorThrowerOptions } from './errors/errorThrower'; + +export * from './components'; +export * from './composables'; + +export { clerkPlugin } from './plugin'; + +setErrorThrowerOptions({ packageName: PACKAGE_NAME }); +setClerkJsLoadingErrorPackageName(PACKAGE_NAME); diff --git a/packages/vue/src/keys.ts b/packages/vue/src/keys.ts new file mode 100644 index 00000000000..1a6a9e0a012 --- /dev/null +++ b/packages/vue/src/keys.ts @@ -0,0 +1,5 @@ +import type { InjectionKey } from 'vue'; + +import type { VueClerkInjectionKeyType } from './types'; + +export const ClerkInjectionKey = Symbol('clerk') as InjectionKey; diff --git a/packages/vue/src/plugin.ts b/packages/vue/src/plugin.ts new file mode 100644 index 00000000000..884c39917ac --- /dev/null +++ b/packages/vue/src/plugin.ts @@ -0,0 +1,90 @@ +import { inBrowser } from '@clerk/shared/browser'; +import { deriveState } from '@clerk/shared/deriveState'; +import { loadClerkJsScript, type LoadClerkJsScriptOptions } from '@clerk/shared/loadClerkJsScript'; +import type { Clerk, ClientResource, Resources } from '@clerk/types'; +import type { Plugin } from 'vue'; +import { computed, ref, shallowRef, triggerRef } from 'vue'; + +import { ClerkInjectionKey } from './keys'; + +export type PluginOptions = LoadClerkJsScriptOptions; + +/** + * Vue plugin for integrating Clerk. + * + * @example + * Basic usage in main.ts: + * + * ```ts + * import { createApp } from 'vue' + * import { clerkPlugin } from '@clerk/vue' + * import App from './App.vue' + * + * const app = createApp(App) + * + * app.use(clerkPlugin, { + * publishableKey: 'pk_' + * }) + * + * app.mount('#app') + * ``` + */ +export const clerkPlugin: Plugin = { + install(app, options: PluginOptions) { + // @ts-expect-error: Internal property for SSR frameworks like Nuxt + const { __internal_clerk_initial_state } = options; + + const loaded = shallowRef(false); + const clerk = shallowRef(null); + + const resources = ref({ + client: {} as ClientResource, + session: undefined, + user: undefined, + organization: undefined, + }); + + // We need this check for SSR apps like Nuxt as it will try to run this code on the server + // and loadClerkJsScript contains browser-specific code + if (inBrowser()) { + void loadClerkJsScript(options).then(async () => { + if (!window.Clerk) { + throw new Error('Failed to download latest ClerkJS. Contact support@clerk.com.'); + } + + clerk.value = window.Clerk; + await window.Clerk.load(options); + loaded.value = true; + + clerk.value.addListener(payload => { + resources.value = payload; + }); + + // When Clerk updates its state internally, Vue's reactivity system doesn't detect + // the change since it's an external object being mutated. triggerRef() forces Vue + // to re-evaluate all dependencies regardless of how the value was changed. + triggerRef(clerk); + }); + } + + const derivedState = computed(() => deriveState(loaded.value, resources.value, __internal_clerk_initial_state)); + + const authCtx = computed(() => { + const { sessionId, userId, orgId, actor, orgRole, orgSlug, orgPermissions } = derivedState.value; + return { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions }; + }); + const clientCtx = computed(() => resources.value.client); + const userCtx = computed(() => derivedState.value.user); + const sessionCtx = computed(() => derivedState.value.session); + const organizationCtx = computed(() => derivedState.value.organization); + + app.provide(ClerkInjectionKey, { + clerk, + authCtx, + clientCtx, + sessionCtx, + userCtx, + organizationCtx, + }); + }, +}; diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts new file mode 100644 index 00000000000..64230c0c795 --- /dev/null +++ b/packages/vue/src/types.ts @@ -0,0 +1,48 @@ +import type { + ActiveSessionResource, + ActJWTClaim, + Clerk, + ClerkOptions, + ClientResource, + OrganizationCustomPermissionKey, + OrganizationCustomRoleKey, + OrganizationResource, + UserResource, + Without, +} from '@clerk/types'; +import type { ComputedRef, ShallowRef } from 'vue'; + +export interface VueClerkInjectionKeyType { + clerk: ShallowRef; + authCtx: ComputedRef<{ + userId: string | null | undefined; + sessionId: string | null | undefined; + actor: ActJWTClaim | null | undefined; + orgId: string | null | undefined; + orgRole: OrganizationCustomRoleKey | null | undefined; + orgSlug: string | null | undefined; + orgPermissions: OrganizationCustomPermissionKey[] | null | undefined; + }>; + clientCtx: ComputedRef; + sessionCtx: ComputedRef; + userCtx: ComputedRef; + organizationCtx: ComputedRef; +} + +// Copied from `@clerk/clerk-react` +export interface HeadlessBrowserClerk extends Clerk { + load: (opts?: Without) => Promise; + updateClient: (client: ClientResource) => void; +} + +// Copied from `@clerk/clerk-react` +export interface BrowserClerk extends HeadlessBrowserClerk { + onComponentsReady: Promise; + components: any; +} + +declare global { + interface Window { + Clerk: BrowserClerk; + } +} diff --git a/packages/vue/src/utils/childrenUtils.ts b/packages/vue/src/utils/childrenUtils.ts new file mode 100644 index 00000000000..55b76b61c14 --- /dev/null +++ b/packages/vue/src/utils/childrenUtils.ts @@ -0,0 +1,32 @@ +import { h, Text, type VNode } from 'vue'; + +import { errorThrower } from '../errors/errorThrower'; +import { multipleChildrenInButtonComponent } from '../errors/messages'; + +type ButtonName = 'SignInButton' | 'SignUpButton' | 'SignOutButton' | 'SignInWithMetamaskButton'; + +export const normalizeWithDefaultValue = (slotContent: VNode[] | undefined, defaultValue: string) => { + // Render a button with the default value if no slot content is provided + if (!slotContent) { + return h('button', defaultValue); + } + + // Render a button with the slot content if it's a text node + if (slotContent[0].type === Text) { + return h('button', slotContent); + } + + // Render the slot content as is + return slotContent; +}; + +export const assertSingleChild = (slotContent: VNode | VNode[], name: ButtonName) => { + if (Array.isArray(slotContent)) { + if (slotContent.length > 1) { + return errorThrower.throw(multipleChildrenInButtonComponent(name)); + } + return slotContent[0]; + } + + return slotContent; +}; diff --git a/packages/vue/src/utils/index.ts b/packages/vue/src/utils/index.ts new file mode 100644 index 00000000000..3b0a7909589 --- /dev/null +++ b/packages/vue/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './childrenUtils'; +export * from './toComputedRefs'; +export * from './useClerkLoaded'; diff --git a/packages/vue/src/utils/toComputedRefs.ts b/packages/vue/src/utils/toComputedRefs.ts new file mode 100644 index 00000000000..a31c97c10fc --- /dev/null +++ b/packages/vue/src/utils/toComputedRefs.ts @@ -0,0 +1,19 @@ +import { computed, type ComputedRef } from 'vue'; + +export type ToComputedRefs = { + [K in keyof T]: ComputedRef; +}; + +/** + * Converts a computed ref to an object of computed refs. + * This will allow the composables to be destructurable and still maintain reactivity. + */ +export function toComputedRefs(objectRef: ComputedRef): ToComputedRefs { + const result = {} as any; + + for (const key in objectRef.value) { + result[key] = computed(() => objectRef.value[key]); + } + + return result; +} diff --git a/packages/vue/src/utils/useClerkLoaded.ts b/packages/vue/src/utils/useClerkLoaded.ts new file mode 100644 index 00000000000..8524f15f5ce --- /dev/null +++ b/packages/vue/src/utils/useClerkLoaded.ts @@ -0,0 +1,31 @@ +import type { LoadedClerk } from '@clerk/types'; +import { watch } from 'vue'; + +import { useClerk } from '../composables'; + +/** + * Executes a callback when Clerk is loaded. + * + * @param callback - Function to execute once Clerk is loaded + * @example + * ```ts + * useClerkLoaded((clerk) => { + * clerk.redirectToSignUp(props); + * }); + * ``` + */ +export const useClerkLoaded = (callback: (clerk: LoadedClerk) => void) => { + const clerk = useClerk(); + + watch( + clerk, + unwrappedClerk => { + if (!unwrappedClerk?.loaded) { + return; + } + + callback(unwrappedClerk as LoadedClerk); + }, + { immediate: true }, + ); +}; diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json new file mode 100644 index 00000000000..877c8e9f36d --- /dev/null +++ b/packages/vue/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "strict": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "noEmit": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": false, + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/packages/vue/tsup.config.ts b/packages/vue/tsup.config.ts new file mode 100644 index 00000000000..ef59b339bdf --- /dev/null +++ b/packages/vue/tsup.config.ts @@ -0,0 +1,31 @@ +import autoPropsPlugin from '@vue.ts/tsx-auto-props/esbuild'; +import { defineConfig, type Options } from 'tsup'; + +import { name, version } from './package.json'; + +type EsbuildPlugin = NonNullable[number]; + +export default defineConfig(() => { + return { + clean: true, + entry: ['./src/index.ts'], + format: ['esm'], + bundle: true, + sourcemap: true, + minify: false, + dts: true, + esbuildPlugins: [ + // Automatically generates runtime props from TypeScript types/interfaces for all + // control and UI components, adding them to Vue components during build via + // Object.defineProperty + autoPropsPlugin({ + include: ['**/*.ts'], + }) as EsbuildPlugin, + ], + define: { + PACKAGE_NAME: `"${name}"`, + PACKAGE_VERSION: `"${version}"`, + }, + external: ['vue'], + }; +}); diff --git a/packages/vue/vitest.config.ts b/packages/vue/vitest.config.ts new file mode 100644 index 00000000000..527089358ab --- /dev/null +++ b/packages/vue/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + }, +});