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
+
+
+
+
+[](https://clerk.com/discord)
+[](https://clerk.com/docs?utm_source=github&utm_medium=clerk_vue)
+[](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
+ *
+ *
+ *
+ *
+ * Sign in
+ *
+ */
+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
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
{{ organization.name }}
+ *
Your role: {{ membership.role }}
+ *
+ *
+ *
+ * No active organization
+ *
+ *
+ */
+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
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
This session has been active since {{ session.lastActiveAt.toLocaleString() }}
+ *
+ *
+ */
+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
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Welcome back. You have been here
+ * {{ sessions.length }} times before.
+ *
+ *
+ *
+ */
+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
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * The current sign in attempt status is {{ signIn.status }}.
+ *
+ *
+ */
+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
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * The current sign-up attempt status is {{ signUp.status }}.
+ *
+ *
+ */
+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
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Hello {{ user.fullName }}!
+ *
+ *
+ *
+ * Not signed in
+ *
+ *
+ */
+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',
+ },
+});