├── docs ├── .gitignore ├── guide │ ├── writing-data.md │ ├── global-options.md │ ├── app-check.md │ └── other-firebase-services.md ├── public │ ├── logo.png │ ├── favicon.ico │ ├── apple-touch-icon.png │ ├── _redirects │ ├── service-worker.js │ └── logo.svg ├── cookbook │ ├── prototyping.md │ ├── loading-state.md │ ├── rtdb-and-firestore.md │ ├── index.md │ ├── subscriptions-external.md │ └── vuex.md ├── nuxt │ ├── ssr.md │ ├── app-check.md │ ├── environment-variables.md │ └── getting-started.md ├── .vitepress │ ├── components │ │ ├── FirestoreLogo.vue │ │ └── RtdbLogo.vue │ ├── theme │ │ ├── index.ts │ │ └── components │ │ │ ├── HomeSponsors.vue │ │ │ ├── AsideSponsors.vue │ │ │ ├── sponsors.json │ │ │ └── HomeSponsorsGroup.vue │ ├── style │ │ └── vars.css │ └── meta.ts └── index.md ├── playground ├── remoteconfig.template.json ├── .firebaserc ├── functions │ ├── tsconfig.dev.json │ ├── .gitignore │ ├── tsconfig.json │ ├── src │ │ └── index.ts │ ├── .eslintrc.js │ └── package.json ├── public │ └── favicon.ico ├── env.d.ts ├── .vscode │ └── extensions.json ├── src │ ├── demos │ │ └── TodoList │ │ │ ├── types.ts │ │ │ └── components │ │ │ └── TodoItem.vue │ ├── App.vue │ ├── assets │ │ ├── logo.svg │ │ ├── main.css │ │ └── base.css │ ├── pages │ │ ├── pinia-store.vue │ │ ├── nested-refs-list.vue │ │ ├── index.vue │ │ ├── app-check.vue │ │ ├── converter-with-number.vue │ │ ├── nested-collections.vue │ │ ├── vuex-store.vue │ │ ├── config.vue │ │ ├── rtdb-todos.vue │ │ ├── todos.vue │ │ ├── storage.vue │ │ └── authentication.vue │ ├── firebase.ts │ ├── stores │ │ └── counter.ts │ └── main.ts ├── storage.rules ├── database.rules.json ├── tsconfig.config.json ├── index.html ├── firestore.indexes.json ├── .gitignore ├── tsconfig.json ├── vite.config.ts ├── package.json ├── firestore.rules ├── firebase.json ├── README.md └── typed-router.d.ts ├── .prettierignore ├── functions ├── .gitignore ├── package.json └── index.js ├── .npmrc ├── .github ├── funding.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── settings.yml ├── workflows │ ├── release-tag.yml │ ├── docs-prs.yml │ ├── docs.yml │ └── ci.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── commit-convention.md ├── size-checks ├── vuefire-full.js ├── vuefire-rtdb.js └── vuefire-firestore.js ├── .firebaserc ├── packages └── nuxt │ ├── .nuxtrc │ ├── .eslintignore │ ├── playground │ ├── .firebaserc │ ├── tsconfig.json │ ├── .gitignore │ ├── helpers │ │ └── auth.ts │ ├── pages │ │ ├── index.vue │ │ ├── firestore-getDoc-on-server.vue │ │ ├── app-check.vue │ │ ├── firestore-secure-doc.vue │ │ ├── bug-playground.vue │ │ ├── database-todo-list.vue │ │ ├── firestore-useDocument.vue │ │ └── storage.vue │ ├── plugins │ │ ├── test-vuefire.ts │ │ └── focus.ts │ ├── database.rules.json │ ├── storage.rules │ ├── middleware │ │ └── vuefire-auth.ts │ ├── firestore.indexes.json │ ├── package.json │ ├── firebase.json │ ├── app.vue │ ├── firestore.rules │ ├── components │ │ └── TodoItem.vue │ └── nuxt.config.ts │ ├── tests │ ├── fixtures │ │ └── basic │ │ │ ├── package.json │ │ │ ├── app.vue │ │ │ └── nuxt.config.ts │ └── basic.spec.ts │ ├── README.md │ ├── tsconfig.json │ ├── build.config.ts │ ├── .editorconfig │ ├── src │ └── runtime │ │ ├── utils.ts │ │ ├── app │ │ ├── composables.ts │ │ ├── plugin.client.ts │ │ ├── lru-cache.ts │ │ └── plugin.server.ts │ │ ├── constants.ts │ │ ├── auth │ │ ├── composables.ts │ │ ├── plugin.server.ts │ │ ├── plugin-user-token.server.ts │ │ ├── plugin.client.ejs │ │ ├── plugin-mint-cookie.client.ts │ │ ├── plugin-authenticate-user.server.ts │ │ └── api.session-verification.ts │ │ ├── analytics │ │ ├── composables.ts │ │ └── plugin.client.ts │ │ ├── logging.ts │ │ ├── app-check │ │ ├── index.ts │ │ ├── plugin.server.ts │ │ └── plugin.client.ts │ │ ├── emulators │ │ ├── storage.plugin.ts │ │ ├── database.plugin.ts │ │ ├── firestore.plugin.ts │ │ ├── functions.plugin.ts │ │ └── auth.plugin.ts │ │ ├── admin │ │ └── plugin.server.ts │ │ └── payload-plugin.ts │ ├── vitest.config.ts │ ├── eslint.config.mjs │ ├── templates │ └── plugin.ejs │ ├── .gitignore │ ├── .eslintrc │ └── package.json ├── .prettierrc ├── server.d.ts ├── .vscode ├── settings.json └── vitest.code-snippets ├── pnpm-workspace.yaml ├── src ├── global.d.ts ├── server │ ├── utils.ts │ ├── index.ts │ ├── logging.ts │ └── app-check.ts ├── database │ ├── unbind.ts │ ├── utils.ts │ └── index.ts ├── app │ └── index.ts ├── firestore │ └── unbind.ts ├── globals.ts ├── ssr │ ├── plugin.ts │ └── initialState.ts └── app-check │ └── index.ts ├── netlify.toml ├── vitest.workspace.ts ├── .gitignore ├── database.rules.json ├── storage.rules ├── examples ├── README.md └── rtdb.html ├── vitest.config.ts ├── tsconfig.json ├── scripts ├── docs-check.sh ├── run-typedoc.mjs ├── verifyCommit.mjs ├── typedoc.tsconfig.json └── typedoc-markdown.mjs ├── firestore.rules ├── firebase.json ├── LICENSE └── tests ├── database └── ssr.spec.ts ├── firestore └── ssr.spec.ts └── storage └── index.spec.ts /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .vitepress/cache 2 | -------------------------------------------------------------------------------- /playground/remoteconfig.template.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | typed-router.d.ts 2 | *.ejs 3 | -------------------------------------------------------------------------------- /docs/guide/writing-data.md: -------------------------------------------------------------------------------- 1 | # Writing Data 2 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.local -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: posva 2 | custom: https://www.paypal.me/posva 3 | -------------------------------------------------------------------------------- /size-checks/vuefire-full.js: -------------------------------------------------------------------------------- 1 | export * from '../dist/vuefire.esm-bundler' 2 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "vue-fire-store" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/nuxt/.nuxtrc: -------------------------------------------------------------------------------- 1 | imports.autoImport=false 2 | typescript.includeWorkspace=true 3 | -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/vuefire/HEAD/docs/public/logo.png -------------------------------------------------------------------------------- /packages/nuxt/.eslintignore: -------------------------------------------------------------------------------- 1 | playground 2 | dist 3 | node_modules 4 | typed-router.d.ts 5 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/vuefire/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /playground/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "vue-fire-store" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /size-checks/vuefire-rtdb.js: -------------------------------------------------------------------------------- 1 | export { rtdbBind, rtdbUnbind } from '../dist/vuefire.esm-bundler' 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "es5", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /docs/cookbook/prototyping.md: -------------------------------------------------------------------------------- 1 | # Prototyping 2 | 3 | TODO: maybe create a boilerplate with vite? 4 | -------------------------------------------------------------------------------- /playground/functions/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | ".eslintrc.js" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /playground/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/vuefire/HEAD/playground/public/favicon.ico -------------------------------------------------------------------------------- /server.d.ts: -------------------------------------------------------------------------------- 1 | // NOTE: for node10/node moduleResolution to work 2 | export * from './dist/server/index' 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "vuefire", 4 | "Vuex", 5 | "Vuexfire" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/vuefire/HEAD/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/nuxt/playground/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "vue-fire-store" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /size-checks/vuefire-firestore.js: -------------------------------------------------------------------------------- 1 | export { firestoreBind, firestoreUnbind } from '../dist/vuefire.esm-bundler' 2 | -------------------------------------------------------------------------------- /docs/cookbook/loading-state.md: -------------------------------------------------------------------------------- 1 | # Waiting for the data to be present 2 | 3 | > This section is a work in progress 4 | -------------------------------------------------------------------------------- /playground/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /playground/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - packages/* 4 | - packages/nuxt/playground 5 | - functions 6 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | // Global compile-time constants 2 | declare var __BROWSER__: boolean 3 | declare var __CI__: boolean 4 | -------------------------------------------------------------------------------- /packages/nuxt/tests/fixtures/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "basic", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /docs/cookbook/rtdb-and-firestore.md: -------------------------------------------------------------------------------- 1 | # Using Firebase Database and Firestore together 2 | 3 | > This section is a work in progress 4 | -------------------------------------------------------------------------------- /packages/nuxt/tests/fixtures/basic/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /playground/src/demos/TodoList/types.ts: -------------------------------------------------------------------------------- 1 | export interface Todo { 2 | created: Date 3 | finished: boolean 4 | text: string 5 | } 6 | -------------------------------------------------------------------------------- /packages/nuxt/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /packages/nuxt/playground/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | service-account.json 10 | -------------------------------------------------------------------------------- /packages/nuxt/playground/helpers/auth.ts: -------------------------------------------------------------------------------- 1 | import { GoogleAuthProvider } from 'firebase/auth' 2 | 3 | export const googleAuthProvider = new GoogleAuthProvider() 4 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NODE_VERSION = "16" 3 | 4 | [build] 5 | command = "pnpm run -w docs:build" 6 | ignore = "./scripts/docs-check.sh" 7 | publish = "docs/.vitepress/dist" 8 | -------------------------------------------------------------------------------- /packages/nuxt/playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from 'vitest/config' 2 | 3 | export default defineWorkspace([ 4 | // vuefire 5 | './', 6 | // nuxt-vuefire 7 | './packages/nuxt', 8 | ]) 9 | -------------------------------------------------------------------------------- /playground/storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | match /{allPaths=**} { 5 | allow read, write: if false; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/nuxt/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Module 2 | 3 | ## Development 4 | 5 | - Run `npm run dev:prepare` to generate type stubs. 6 | - Use `npm run dev` to start [playground](./playground) in development mode. 7 | -------------------------------------------------------------------------------- /packages/nuxt/playground/plugins/test-vuefire.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtPlugin((nuxt) => { 2 | const firebaseApp = useNuxtApp().$firebaseApp 3 | console.log('Has firebase App', !!firebaseApp) 4 | }) 5 | -------------------------------------------------------------------------------- /playground/functions/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled JavaScript files 2 | lib/**/*.js 3 | lib/**/*.js.map 4 | 5 | # TypeScript v1 declaration files 6 | typings/ 7 | 8 | # Node.js dependency directory 9 | node_modules/ 10 | -------------------------------------------------------------------------------- /packages/nuxt/playground/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "forbidden": { 4 | ".read": false, 5 | ".write": false 6 | }, 7 | ".read": true, 8 | ".write": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/nuxt/playground/plugins/focus.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtPlugin((nuxt) => { 2 | nuxt.vueApp.directive('focus', { 3 | async mounted(el) { 4 | await nextTick() 5 | el.focus() 6 | }, 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/nuxt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./playground/.nuxt/tsconfig.json", 3 | "include": [ 4 | // missing in the playground 5 | "./src", 6 | "./playground", 7 | "./templates" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /playground/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "forbidden": { 4 | ".read": false, 5 | ".write": false 6 | }, 7 | ".read": "auth != null", 8 | ".write": "auth != null" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | npm-debug.log 4 | yarn-error.log 5 | .nyc_output 6 | coverage.lcov 7 | dist 8 | package-lock.json 9 | .DS_Store 10 | temp 11 | roadmap.md 12 | *-debug.log 13 | docs/api 14 | .firebase 15 | -------------------------------------------------------------------------------- /packages/nuxt/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | clean: true, 5 | // explicitly externalize consola since Nuxt has it 6 | externals: ['consola'], 7 | }) 8 | -------------------------------------------------------------------------------- /playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /packages/nuxt/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "forbidden": { 4 | ".read": false, 5 | ".write": false 6 | }, 7 | "__tests": { 8 | ".read": true, 9 | ".write": true 10 | }, 11 | ".read": false, 12 | ".write": false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | match /tests/{allPaths=**} { 5 | allow read, write: if true; 6 | } 7 | match /{allPaths=**} { 8 | allow read, write: if false; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/nuxt/ssr.md: -------------------------------------------------------------------------------- 1 | # Server Side Rendering 2 | 3 | Nuxt VueFire works both with and without SSR. You don't need to do anything special to make it work with SSR, everything is handled automatically. 4 | 5 | You can read more about SSR and VueFire in general in the [VueFire SSR page](../guide/ssr) 6 | -------------------------------------------------------------------------------- /playground/tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*" 7 | ], 8 | "compilerOptions": { 9 | "composite": true, 10 | "types": [ 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /playground/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.vitepress/components/FirestoreLogo.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /playground/src/pages/pinia-store.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /playground/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2017" 10 | }, 11 | "compileOnSave": true, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Vuefire Examples 2 | 3 | Here are some examples using Firestore. For bigger projects, we recommend checking [the official documentation](https://vuefire.vuejs.org/) 4 | 5 | In order to run the examples make sure to have the dist version of vuefire. You can generate it by going into `packages/vuefire` folder and running `yarn build`/`npm run build` 6 | -------------------------------------------------------------------------------- /packages/nuxt/playground/storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | match /tests/{allPaths=**} { 5 | allow read, write: if true; 6 | } 7 | match /demo/{userId}/{allPaths=**} { 8 | allow read: if true; 9 | allow write: if request.auth.uid == userId; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/public/_redirects: -------------------------------------------------------------------------------- 1 | # These rules will change if you change your site’s custom domains or HTTPS settings 2 | 3 | # Redirect domain aliases to primary domain 4 | https://firebase.vuejs.org/* https://vuefire.vuejs.org/:splat 301! 5 | 6 | # Optional: Redirect default Netlify subdomain to primary domain 7 | https://vuefire.netlify.com/* https://vuefire.vuejs.org/:splat 301! 8 | -------------------------------------------------------------------------------- /src/server/utils.ts: -------------------------------------------------------------------------------- 1 | // FirebaseError is an interface here but is a class in firebase/app 2 | import type { FirebaseError } from 'firebase-admin' 3 | 4 | /** 5 | * Ensure that the error is a FirebaseError 6 | * 7 | * @param err - error to check 8 | */ 9 | export function isFirebaseError(err: any): err is FirebaseError { 10 | return err != null && 'code' in err 11 | } 12 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/utils.ts: -------------------------------------------------------------------------------- 1 | // FirebaseError is an interface here but is a class in firebase/app 2 | import type { FirebaseError } from 'firebase-admin' 3 | 4 | /** 5 | * Ensure that the error is a FirebaseError 6 | * 7 | * @param err - error to check 8 | */ 9 | export function isFirebaseError(err: any): err is FirebaseError { 10 | return err != null && 'code' in err 11 | } 12 | -------------------------------------------------------------------------------- /packages/nuxt/playground/middleware/vuefire-auth.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware(async (to, from) => { 2 | const user = await getCurrentUser() 3 | 4 | console.log('got user in middleware', user?.uid) 5 | 6 | if (!user) { 7 | return navigateTo({ 8 | path: '/authentication', 9 | query: { 10 | redirect: to.fullPath, 11 | }, 12 | }) 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /playground/functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions' 2 | 3 | // // Start writing Firebase Functions 4 | // // https://firebase.google.com/docs/functions/typescript 5 | // 6 | // export const helloWorld = functions.https.onRequest((request, response) => { 7 | // functions.logger.info("Hello logs!", {structuredData: true}); 8 | // response.send("Hello from Firebase!"); 9 | // }); 10 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/app/composables.ts: -------------------------------------------------------------------------------- 1 | import type { FirebaseApp } from 'firebase/app' 2 | import { useNuxtApp } from '#imports' 3 | 4 | /** 5 | * Gets the firebase instance from the current Nuxt App. This can be used anywhere the `useNuxtApp()` can be used. Differently from `vuefire`'s `useFirebaseApp()`, this doesn't accept a name. 6 | */ 7 | export const useFirebaseApp = (): FirebaseApp => useNuxtApp().$firebaseApp 8 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal Gets access to the user within the application. This is a symbol to keep it private for the moment. 3 | */ 4 | export const UserSymbol = Symbol('user') 5 | 6 | /** 7 | * @internal Gets access to the decoded token within the application. This is a symbol to keep it private for the moment. 8 | */ 9 | export const DECODED_ID_TOKEN_SYMBOL = Symbol('decodedToken') 10 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/auth/composables.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentUser as _getCurrentUser } from 'vuefire' 2 | import { useFirebaseApp } from '../app/composables' 3 | 4 | /** 5 | * @inheritDoc {getCurrentUser} 6 | */ 7 | export const getCurrentUser = (name?: string) => 8 | // This makes the `getCurrentUser()` function work by default in more places when using the Nuxt module 9 | _getCurrentUser(name ?? useFirebaseApp().name) 10 | -------------------------------------------------------------------------------- /playground/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionGroup": "todos", 5 | "queryScope": "COLLECTION", 6 | "fields": [ 7 | { 8 | "fieldPath": "finished", 9 | "order": "ASCENDING" 10 | }, 11 | { 12 | "fieldPath": "created", 13 | "order": "ASCENDING" 14 | } 15 | ] 16 | } 17 | ], 18 | "fieldOverrides": [] 19 | } 20 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | optimizeDeps: { 5 | exclude: ['vue-demi'], 6 | }, 7 | test: { 8 | environment: 'happy-dom', 9 | include: ['tests/**/*.spec.ts'], 10 | coverage: { 11 | include: ['src/**/*.ts'], 12 | reporter: ['text', 'json', 'html'], 13 | exclude: ['src/**/*.spec.ts', 'src/index.ts'], 14 | }, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /packages/nuxt/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import { defineConfig } from 'vitest/config' 4 | 5 | export default defineConfig({ 6 | test: { 7 | include: ['tests/**/*.spec.ts'], 8 | coverage: { 9 | include: ['src/**/*.ts'], 10 | reporter: ['text', 'json', 'html'], 11 | // exclude: ['src/**/*.spec.ts', 'src/index.ts'], 12 | }, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /packages/nuxt/playground/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionGroup": "todos", 5 | "queryScope": "COLLECTION", 6 | "fields": [ 7 | { 8 | "fieldPath": "finished", 9 | "order": "ASCENDING" 10 | }, 11 | { 12 | "fieldPath": "created", 13 | "order": "ASCENDING" 14 | } 15 | ] 16 | } 17 | ], 18 | "fieldOverrides": [] 19 | } 20 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /playground/src/pages/nested-refs-list.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | -------------------------------------------------------------------------------- /docs/public/service-worker.js: -------------------------------------------------------------------------------- 1 | // force clearing previous service worker 2 | self.addEventListener('install', function (e) { 3 | self.skipWaiting() 4 | }) 5 | 6 | self.addEventListener('activate', function (e) { 7 | self.registration 8 | .unregister() 9 | .then(function () { 10 | return self.clients.matchAll() 11 | }) 12 | .then(function (clients) { 13 | clients.forEach((client) => client.navigate(client.url)) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { ensureAdminApp } from './admin' 2 | export { VueFireAppCheckServer } from './app-check' 3 | export { 4 | VueFireAuthServer, 5 | createServerUser, 6 | AUTH_COOKIE_NAME, 7 | decodeSessionCookie, 8 | decodeUserToken, 9 | } from './auth' 10 | export { ensureAdminApp } from './admin' 11 | /** 12 | * @deprecated use `ensureAdminApp` instead. 13 | */ 14 | export const getAdminApp = ensureAdminApp 15 | export { isFirebaseError } from './utils' 16 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/analytics/composables.ts: -------------------------------------------------------------------------------- 1 | import { getAnalytics } from 'firebase/analytics' 2 | import { useFirebaseApp } from '../app/composables' 3 | 4 | /** 5 | * Retrieves the Firebase analytics instance. **Returns `null` on the server** and when the platform isn't supported. 6 | * 7 | * @param name - name of the application 8 | * @returns the Analytics instance 9 | */ 10 | export function useAnalytics() { 11 | return import.meta.client ? getAnalytics(useFirebaseApp()) : null 12 | } 13 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": [ 4 | "env.d.ts", 5 | "./typed-router.d.ts", 6 | "src/**/*", 7 | "src/**/*.vue" 8 | ], 9 | "compilerOptions": { 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": [ 13 | "./src/*" 14 | ], 15 | "vuefire": [ 16 | "../src/index.ts" 17 | ] 18 | } 19 | }, 20 | "references": [ 21 | { 22 | "path": "./tsconfig.config.json" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src", 4 | "./*.d.ts", 5 | "tests", 6 | ], 7 | "exclude": [ 8 | "dist" 9 | ], 10 | "compilerOptions": { 11 | "target": "ESNext", 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "esModuleInterop": true, 15 | "strict": true, 16 | "skipLibCheck": true, 17 | "resolveJsonModule": true, 18 | "noEmit": true, 19 | "jsx": "preserve", 20 | "lib": [ 21 | "ESNext", 22 | "DOM" 23 | ], 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import VueRouter from 'unplugin-vue-router/vite' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [VueRouter(), vue()], 10 | resolve: { 11 | alias: { 12 | '@': fileURLToPath(new URL('./src', import.meta.url)), 13 | vuefire: fileURLToPath(new URL('../src/index.ts', import.meta.url)), 14 | }, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Question 4 | url: https://github.com/vuejs/vuefire/discussions/new?category=Q-A 5 | about: Ask a question or discuss about VueFire 6 | - name: Ideas 7 | url: https://github.com/vuejs/vuefire/discussions/new?category=Ideas 8 | about: Start a discussion to improve VueFire 9 | - name: GitHub Sponsors 10 | url: https://github.com/sponsors/posva 11 | about: Like this project? Please consider supporting the author. 12 | -------------------------------------------------------------------------------- /packages/nuxt/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview", 8 | "prepare": "nuxi prepare" 9 | }, 10 | "devDependencies": { 11 | "@nuxt/devtools": "^1.0.8", 12 | "nuxt": "^3.12.4" 13 | }, 14 | "dependencies": { 15 | "@firebase/app-types": "^0.9.3", 16 | "firebase": "^11.1.0", 17 | "nuxt-vuefire": "workspace:*", 18 | "vuefire": "workspace:*" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/nuxt/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat' 3 | 4 | // Run `npx @eslint/config-inspector` to inspect the resolved config interactively 5 | export default createConfigForNuxt({ 6 | features: { 7 | // Rules for module authors 8 | tooling: true, 9 | // Rules for formatting 10 | stylistic: true, 11 | }, 12 | dirs: { 13 | src: ['./playground'], 14 | }, 15 | }).append({ 16 | rules: { 17 | '@typescript-eslint/no-explicit-any': 'off', 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /packages/nuxt/playground/pages/firestore-getDoc-on-server.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/app/plugin.client.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from 'firebase/app' 2 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports' 3 | 4 | /** 5 | * Initializes the app and provides it to others. 6 | */ 7 | export default defineNuxtPlugin(() => { 8 | const runtimeConfig = useRuntimeConfig() 9 | 10 | // NOTE: the presence of the config is ensured by the module 11 | const firebaseApp = initializeApp(runtimeConfig.public.vuefire!.config!) 12 | 13 | return { 14 | provide: { 15 | firebaseApp, 16 | }, 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /playground/src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "private": true, 4 | "description": "Cloud Functions for Firebase", 5 | "scripts": { 6 | "serve": "firebase emulators:start --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "22" 14 | }, 15 | "main": "index.js", 16 | "dependencies": { 17 | "firebase-admin": "^13.0.0", 18 | "firebase-functions": "^6.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | labels: 2 | - name: bug 3 | color: ee0701 4 | - name: contribution welcome 5 | color: 0e8a16 6 | - name: discussion 7 | color: 4935ad 8 | - name: docs 9 | color: 8be281 10 | - name: enhancement 11 | color: a2eeef 12 | - name: good first issue 13 | color: 7057ff 14 | - name: help wanted 15 | color: 008672 16 | - name: question 17 | color: d876e3 18 | - name: wontfix 19 | color: ffffff 20 | - name: WIP 21 | color: ffffff 22 | - name: need repro 23 | color: c9581c 24 | - name: feature request 25 | color: fbca04 26 | -------------------------------------------------------------------------------- /playground/src/pages/app-check.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /playground/src/pages/converter-with-number.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /packages/nuxt/playground/pages/app-check.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /src/database/unbind.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue-demi' 2 | import type { UnbindWithReset, ResetOption } from '../shared' 3 | 4 | export const databaseUnbinds = new WeakMap< 5 | object, 6 | Record 7 | >() 8 | 9 | export function internalUnbind( 10 | key: string, 11 | unbinds: Record | undefined, 12 | reset?: ResetOption 13 | ) { 14 | if (unbinds && unbinds[key]) { 15 | unbinds[key](reset) 16 | delete unbinds[key] 17 | } 18 | } 19 | 20 | export const unbind = (target: Ref, reset?: ResetOption) => 21 | internalUnbind('', databaseUnbinds.get(target), reset) 22 | -------------------------------------------------------------------------------- /packages/nuxt/tests/basic.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import { describe, it, expect } from 'vitest' 4 | import { setup, $fetch } from '@nuxt/test-utils' 5 | 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 7 | 8 | describe.skip('ssr', async () => { 9 | await setup({ 10 | rootDir: path.join(__dirname, './fixtures/basic'), 11 | }) 12 | 13 | it('renders the index page', async () => { 14 | // Get response to a server-rendered page with `$fetch`. 15 | const html = await $fetch('/') 16 | expect(html).toContain('
basic
') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /scripts/docs-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # check if doc files changes for netlify 4 | # needed because we cannot use && in netlify.toml 5 | 6 | # exit 0 will skip the build while exit 1 will build 7 | 8 | # - check any change in docs 9 | # - check for new version of vite related deps 10 | # - changes in netlify conf 11 | # - a commit message that starts with docs like docs: ... or docs(nuxt): ... 12 | 13 | # All paths are relative to packages/docs 14 | 15 | git diff --quiet 'HEAD^' HEAD ./docs && ! git diff 'HEAD^' HEAD ./pnpm-lock.yaml | grep --quiet vite && git diff --quiet 'HEAD^' HEAD ./netlify.toml && ! git log -1 --pretty=%B | grep '^docs' 16 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import "./base.css"; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/run-typedoc.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { createTypeDocApp } from './typedoc-markdown.mjs' 3 | 4 | const __dirname = path.dirname(new URL(import.meta.url).pathname) 5 | 6 | createTypeDocApp({ 7 | name: 'API Documentation', 8 | tsconfig: path.resolve(__dirname, './typedoc.tsconfig.json'), 9 | categorizeByGroup: true, 10 | githubPages: false, 11 | disableSources: true, // some links are in node_modules and it's ugly 12 | plugin: ['typedoc-plugin-markdown'], 13 | entryPoints: [ 14 | path.resolve(__dirname, '../src/index.ts'), 15 | path.resolve(__dirname, '../packages/nuxt/src/module.ts'), 16 | ], 17 | }).then((app) => app.build()) 18 | -------------------------------------------------------------------------------- /playground/src/firebase.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from 'firebase/app' 2 | import { GoogleAuthProvider } from 'firebase/auth' 3 | 4 | export function createFirebaseApp() { 5 | return initializeApp({ 6 | apiKey: 'AIzaSyAkUKe36TPWL2eZTshgk-Xl4bY_R5SB97U', 7 | authDomain: 'vue-fire-store.firebaseapp.com', 8 | databaseURL: 'https://vue-fire-store.firebaseio.com', 9 | projectId: 'vue-fire-store', 10 | storageBucket: 'vue-fire-store.appspot.com', 11 | messagingSenderId: '998674887640', 12 | appId: '1:998674887640:web:1e2bb2cc3e5eb2fc3478ad', 13 | measurementId: 'G-RL4BTWXKJ7', 14 | }) 15 | } 16 | 17 | export const googleAuthProvider = new GoogleAuthProvider() 18 | -------------------------------------------------------------------------------- /packages/nuxt/templates/plugin.ejs: -------------------------------------------------------------------------------- 1 | import { 2 | VueFire, 3 | useSSRInitialState, 4 | } from 'vuefire' 5 | import { defineNuxtPlugin } from '#imports' 6 | 7 | export default defineNuxtPlugin((nuxtApp) => { 8 | const firebaseApp = nuxtApp.$firebaseApp 9 | 10 | nuxtApp.vueApp.use(VueFire, { firebaseApp }) 11 | 12 | <% if(options.ssr) { %> 13 | if (import.meta.server) { 14 | // collect the initial state 15 | nuxtApp.payload.vuefire = useSSRInitialState(undefined, firebaseApp) 16 | } else if (nuxtApp.payload?.vuefire) { 17 | // hydrate the plugin state from nuxtApp.payload.vuefire 18 | useSSRInitialState(nuxtApp.payload.vuefire, firebaseApp) 19 | } 20 | <% } %> 21 | }) 22 | -------------------------------------------------------------------------------- /packages/nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode 38 | 39 | # Intellij idea 40 | *.iml 41 | .idea 42 | 43 | # OSX 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | 53 | LICENSE 54 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | import { FirebaseApp, getApp } from 'firebase/app' 2 | import { getCurrentInstance, inject, InjectionKey } from 'vue-demi' 3 | 4 | // @internal 5 | export const _FirebaseAppInjectionKey: InjectionKey = 6 | Symbol('firebaseApp') 7 | 8 | /** 9 | * Gets the firebase app instance. 10 | * 11 | * @param name - optional firebase app name 12 | * @returns the firebase app 13 | */ 14 | export function useFirebaseApp(name?: string): FirebaseApp { 15 | return ( 16 | (getCurrentInstance() && 17 | inject( 18 | _FirebaseAppInjectionKey, 19 | // avoid the inject not found warning 20 | null 21 | )) || 22 | getApp(name) 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/firestore/unbind.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue-demi' 2 | import type { UnbindWithReset } from '../shared' 3 | import type { FirestoreRefOptions } from './bind' 4 | 5 | export const firestoreUnbinds = new WeakMap< 6 | object, 7 | Record 8 | >() 9 | 10 | export function internalUnbind( 11 | key: string, 12 | unbinds: Record | undefined, 13 | reset?: FirestoreRefOptions['reset'] 14 | ) { 15 | if (unbinds && unbinds[key]) { 16 | unbinds[key](reset) 17 | delete unbinds[key] 18 | } 19 | } 20 | 21 | export const unbind = (target: Ref, reset?: FirestoreRefOptions['reset']) => 22 | internalUnbind('', firestoreUnbinds.get(target), reset) 23 | -------------------------------------------------------------------------------- /.github/workflows/release-tag.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 5 | 6 | name: Create Release 7 | 8 | jobs: 9 | build: 10 | name: Create Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@master 15 | - name: Create Release for Tag 16 | id: release_tag 17 | uses: yyx990803/release-tag@master 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | tag_name: ${{ github.ref }} 22 | body: | 23 | Please refer to [CHANGELOG.md](https://github.com/vuejs/vuefire/blob/main/CHANGELOG.md) for details. 24 | -------------------------------------------------------------------------------- /docs/cookbook/index.md: -------------------------------------------------------------------------------- 1 | # What is the Cookbook 2 | 3 | Here are some recipes that you may find useful. If you find a use-case that is missing, you can check if it isn't yet on [the roadmap](https://github.com/vuejs/vuefire/issues/145) or open an issue. You could also directly open a Pull request, but I recommend you to open an issue first so you don't end up wasting your time writing something that won't get published. 4 | 5 | ## Recipes 6 | 7 | - [Migration from VueFire 2](./migration-v2-v3.md) 8 | - [Vuex](./vuex.md) 9 | - [Binding to existing refs](./subscriptions-external.md) 10 | 11 | ## Useful links 12 | 13 | Here are some useful links like articles or videos that are free and can help you 14 | 15 | ## Built with Vuefire/Vuexfire 16 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /__tests/{document=**} { 5 | allow read, write; 6 | } 7 | 8 | match /todos/{todoId} { 9 | allow read, write; 10 | } 11 | match /tweets/{todoId} { 12 | allow read, write; 13 | } 14 | match /users/{todoId} { 15 | allow read: if request.auth.uid != null; 16 | allow write: if false; 17 | } 18 | match /configs/jORwjIykFo2NmkdzTkhU { 19 | allow read, write; 20 | } 21 | 22 | match /playground/pinia-counter { 23 | allow read, write; 24 | } 25 | 26 | match /{document=**} { 27 | allow read, write: if false; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/.vitepress/components/RtdbLogo.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /playground/functions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:import/errors', 10 | 'plugin:import/warnings', 11 | 'plugin:import/typescript', 12 | 'google', 13 | 'plugin:@typescript-eslint/recommended', 14 | ], 15 | parser: '@typescript-eslint/parser', 16 | parserOptions: { 17 | project: ['tsconfig.json', 'tsconfig.dev.json'], 18 | sourceType: 'module', 19 | }, 20 | ignorePatterns: [ 21 | '/lib/**/*', // Ignore built files. 22 | ], 23 | plugins: ['@typescript-eslint', 'import'], 24 | rules: { 25 | quotes: ['error', 'double'], 26 | 'import/no-unresolved': 0, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /src/server/logging.ts: -------------------------------------------------------------------------------- 1 | import { consola as consolaBase } from 'consola' 2 | export type { LogType } from 'consola' 3 | 4 | const vuefireConsola = consolaBase.withTag('vuefire') 5 | 6 | export { vuefireConsola as logger } 7 | 8 | // run this to have more levels of logging 9 | // https://github.com/unjs/consola#log-level 10 | // CONSOLA_LEVEL=5 nr dev 11 | 12 | // NOTE: used to test 13 | // vuefireConsola.trace('trace', 'i am a test') 14 | // vuefireConsola.debug('debug', 'i am a test') 15 | // vuefireConsola.log('log', 'i am a test') 16 | // vuefireConsola.info('info', 'i am a test') 17 | // vuefireConsola.success('success', 'i am a test') 18 | // vuefireConsola.warn('warn', 'i am a test') 19 | // vuefireConsola.error('error', 'i am a test', new Error('haha')) 20 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import function triggers from their respective submodules: 3 | * 4 | * const {onCall} = require("firebase-functions/v2/https"); 5 | * const {onDocumentWritten} = require("firebase-functions/v2/firestore"); 6 | * 7 | * See a full list of supported triggers at https://firebase.google.com/docs/functions 8 | */ 9 | 10 | const { onRequest } = require('firebase-functions/v2/https') 11 | const logger = require('firebase-functions/logger') 12 | 13 | // Create and deploy your first functions 14 | // https://firebase.google.com/docs/functions/get-started 15 | 16 | // exports.helloWorld = onRequest((request, response) => { 17 | // logger.info("Hello logs!", {structuredData: true}); 18 | // response.send("Hello from Firebase!"); 19 | // }); 20 | -------------------------------------------------------------------------------- /playground/src/pages/nested-collections.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | -------------------------------------------------------------------------------- /packages/nuxt/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "@nuxt/eslint-config" 5 | ], 6 | "rules": { 7 | // TS already checks this 8 | "no-use-before-define": "off", 9 | "@typescript-eslint/no-unused-vars": [ 10 | "off" 11 | ], 12 | "space-before-function-paren": [ 13 | "error", 14 | { 15 | "anonymous": "always", 16 | "named": "never", 17 | "asyncArrow": "always" 18 | } 19 | ], 20 | "comma-dangle": [ 21 | "error", 22 | { 23 | "arrays": "only-multiline", 24 | "objects": "only-multiline", 25 | "imports": "only-multiline", 26 | "exports": "only-multiline", 27 | "functions": "only-multiline" 28 | } 29 | ], 30 | "vue/require-v-for-key": "off" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/logging.ts: -------------------------------------------------------------------------------- 1 | import { consola as consolaBase } from 'consola' 2 | export type { LogType } from 'consola' 3 | 4 | const nuxtVueFireConsola = consolaBase.withTag('nuxt-vuefire') 5 | 6 | export { nuxtVueFireConsola as logger } 7 | 8 | // run this to have more levels of logging 9 | // https://github.com/unjs/consola#log-level 10 | // CONSOLA_LEVEL=5 nr dev 11 | 12 | // NOTE: used to test 13 | // nuxtVueFireConsola.trace('trace', 'i am a test') 14 | // nuxtVueFireConsola.debug('debug', 'i am a test') 15 | // nuxtVueFireConsola.log('log', 'i am a test') 16 | // nuxtVueFireConsola.info('info', 'i am a test') 17 | // nuxtVueFireConsola.success('success', 'i am a test') 18 | // nuxtVueFireConsola.warn('warn', 'i am a test') 19 | // nuxtVueFireConsola.error('error', 'i am a test', new Error('haha')) 20 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/app-check/index.ts: -------------------------------------------------------------------------------- 1 | import type { VueFireAppCheckOptions } from 'vuefire' 2 | 3 | /** 4 | * @internal 5 | */ 6 | export interface _NuxtVueFireAppCheckOptionsBase 7 | extends Omit { 8 | provider: 'ReCaptchaV3' | 'ReCaptchaEnterprise' | 'Custom' 9 | } 10 | 11 | export interface NuxtVueFireAppCheckOptionsReCaptchaV3 12 | extends _NuxtVueFireAppCheckOptionsBase { 13 | provider: 'ReCaptchaV3' 14 | key: string 15 | } 16 | 17 | export interface NuxtVueFireAppCheckOptionsReCaptchaEnterprise 18 | extends _NuxtVueFireAppCheckOptionsBase { 19 | provider: 'ReCaptchaEnterprise' 20 | key: string 21 | } 22 | 23 | // TODO: Custom provider 24 | 25 | export type NuxtVueFireAppCheckOptions = 26 | | NuxtVueFireAppCheckOptionsReCaptchaV3 27 | | NuxtVueFireAppCheckOptionsReCaptchaEnterprise 28 | -------------------------------------------------------------------------------- /docs/nuxt/app-check.md: -------------------------------------------------------------------------------- 1 | # App Check 2 | 3 | Nuxt VueFire integrates with [Firebase App Check](https://firebase.google.com/docs/app-check#web) by specifying the `appCheck` option in `nuxt.config.ts`: 4 | 5 | ```ts{4-10} 6 | export default defineNuxtConfig({ 7 | // ... 8 | vuefire: { 9 | appCheck: { 10 | debug: process.env.NODE_ENV !== 'production', 11 | isTokenAutoRefreshEnabled: true, 12 | provider: 'ReCaptchaV3', 13 | // Find the instructions in the Firebase documentation, link above 14 | key: '...', 15 | }, 16 | }, 17 | }) 18 | ``` 19 | 20 | You can find more information in the [VueFire App Check documentation](/guide/app-check). 21 | 22 | ## Debug Tokens 23 | 24 | Instead of specifying `debug: true`, you can specify a debug token [using Environment variables](https://vuefire.vuejs.org/nuxt/environment-variables.html#AppCheck). 25 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/analytics/plugin.client.ts: -------------------------------------------------------------------------------- 1 | import type { FirebaseApp } from 'firebase/app' 2 | import { isSupported, initializeAnalytics } from 'firebase/analytics' 3 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports' 4 | 5 | /** 6 | * Plugin to initialize the analytics module. 7 | * @experimental: NOT YET RELEASED 8 | */ 9 | export default defineNuxtPlugin(async (nuxtApp) => { 10 | const runtimeConfig = useRuntimeConfig() 11 | // @ts-expect-error: not implemented yet, needs to be added to the type 12 | const options = runtimeConfig.public.vuefire.analytics 13 | const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp 14 | 15 | if (await isSupported()) { 16 | initializeAnalytics(firebaseApp, options) 17 | } else { 18 | console.info( 19 | '[nuxt-vuefire]: Firebase Analytics is not supported on this platform.' 20 | ) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/auth/plugin.server.ts: -------------------------------------------------------------------------------- 1 | import type { FirebaseApp } from 'firebase/app' 2 | import { debugErrorMap, prodErrorMap, type User } from 'firebase/auth' 3 | import { _VueFireAuthInit } from 'vuefire' 4 | import { defineNuxtPlugin } from '#imports' 5 | 6 | /** 7 | * Setups VueFireAuth for the client. This version creates some listeners that shouldn't be set on server. 8 | */ 9 | export default defineNuxtPlugin((nuxtApp) => { 10 | const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp 11 | 12 | const [_user, auth] = _VueFireAuthInit( 13 | firebaseApp, 14 | nuxtApp.vueApp, 15 | nuxtApp.payload.vuefireUser as User | undefined, 16 | { 17 | errorMap: 18 | process.env.NODE_ENV === 'production' ? prodErrorMap : debugErrorMap, 19 | } 20 | ) 21 | 22 | return { 23 | provide: { 24 | firebaseAuth: auth, 25 | }, 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/auth/plugin-user-token.server.ts: -------------------------------------------------------------------------------- 1 | import type { App as AdminApp } from 'firebase-admin/app' 2 | import { decodeSessionCookie, AUTH_COOKIE_NAME } from 'vuefire/server' 3 | import { getCookie } from 'h3' 4 | import { DECODED_ID_TOKEN_SYMBOL } from '../constants' 5 | import { defineNuxtPlugin, useRequestEvent } from '#imports' 6 | 7 | /** 8 | * Decodes the user token if any. Should only be added on the server and before the firebase/app 9 | */ 10 | export default defineNuxtPlugin(async (nuxtApp) => { 11 | const event = useRequestEvent() 12 | const adminApp = nuxtApp.$firebaseAdminApp as AdminApp 13 | 14 | const decodedToken = await decodeSessionCookie( 15 | getCookie(event, AUTH_COOKIE_NAME), 16 | adminApp 17 | ) 18 | 19 | nuxtApp[ 20 | // we cannot use symbol to index 21 | DECODED_ID_TOKEN_SYMBOL as unknown as string 22 | ] = decodedToken 23 | }) 24 | -------------------------------------------------------------------------------- /playground/src/pages/vuex-store.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 35 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/app-check/plugin.server.ts: -------------------------------------------------------------------------------- 1 | import type { App as FirebaseAdminApp } from 'firebase-admin/app' 2 | import type { FirebaseApp } from 'firebase/app' 3 | import { VueFireAppCheckServer } from 'vuefire/server' 4 | import { defineNuxtPlugin } from '#imports' 5 | 6 | /** 7 | * Makes AppCheck work on the server. This requires SSR and the admin SDK to be available 8 | */ 9 | export default defineNuxtPlugin((nuxtApp) => { 10 | const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp 11 | const adminApp = nuxtApp.$firebaseAdminApp as FirebaseAdminApp 12 | 13 | // NOTE: necessary in VueFireAppCheckServer 14 | if (!firebaseApp.options.appId) { 15 | throw new Error( 16 | '[nuxt-vuefire]: Missing "appId" in firebase config. This is necessary to use the app-check module on the server.' 17 | ) 18 | } 19 | 20 | VueFireAppCheckServer(nuxtApp.vueApp, adminApp, firebaseApp) 21 | }) 22 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "firebase:emulators": "firebase emulators:start", 9 | "build": "run-p type-check build-only", 10 | "preview": "vite preview --port 4173", 11 | "build-only": "vite build", 12 | "type-check": "vue-tsc --noEmit" 13 | }, 14 | "dependencies": { 15 | "pinia": "^2.2.4", 16 | "vue": "^3.5.12", 17 | "vue-router": "^4.4.5", 18 | "vuefire": "workspace:*", 19 | "vuex": "^4.1.0" 20 | }, 21 | "devDependencies": { 22 | "@firebase/app-types": "^0.9.3", 23 | "@types/node": "^20.11.20", 24 | "@vitejs/plugin-vue": "^5.1.4", 25 | "@vue/tsconfig": "^0.5.1", 26 | "@vueuse/core": "^10.8.0", 27 | "npm-run-all": "^4.1.5", 28 | "typescript": "~5.5.4", 29 | "unplugin-vue-router": "^0.10.8", 30 | "vite": "^5.4.10", 31 | "vue-tsc": "^2.1.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules" 4 | }, 5 | "database": { 6 | "rules": "database.rules.json" 7 | }, 8 | "storage": { 9 | "rules": "storage.rules" 10 | }, 11 | "hosting": { 12 | "public": "docs/.vitepress/dist" 13 | }, 14 | "extensions": {}, 15 | "emulators": { 16 | "auth": { 17 | "port": 9099 18 | }, 19 | "firestore": { 20 | "port": 8080 21 | }, 22 | "database": { 23 | "port": 8081 24 | }, 25 | "storage": { 26 | "port": 9199 27 | }, 28 | "hosting": { 29 | "port": 5050 30 | }, 31 | "ui": { 32 | "enabled": true 33 | }, 34 | "singleProjectMode": true 35 | }, 36 | "functions": [ 37 | { 38 | "source": "functions", 39 | "codebase": "default", 40 | "ignore": [ 41 | "node_modules", 42 | ".git", 43 | "firebase-debug.log", 44 | "firebase-debug.*.log", 45 | "*.local" 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /playground/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "eslint --ext .js,.ts .", 5 | "build": "tsc", 6 | "build:watch": "tsc --watch", 7 | "serve": "npm run build && firebase emulators:start --only functions", 8 | "shell": "npm run build && firebase functions:shell", 9 | "start": "npm run shell", 10 | "deploy": "firebase deploy --only functions", 11 | "logs": "firebase functions:log" 12 | }, 13 | "engines": { 14 | "node": ">=20" 15 | }, 16 | "main": "lib/index.js", 17 | "dependencies": { 18 | "firebase-admin": "^11.11.1", 19 | "firebase-functions": "^4.5.0" 20 | }, 21 | "devDependencies": { 22 | "@typescript-eslint/eslint-plugin": "^6.17.0", 23 | "@typescript-eslint/parser": "^6.17.0", 24 | "eslint": "^8.56.0", 25 | "eslint-config-google": "^0.14.0", 26 | "eslint-plugin-import": "^2.29.1", 27 | "firebase-functions-test": "^3.1.0", 28 | "typescript": "^5.3.3" 29 | }, 30 | "private": true 31 | } 32 | -------------------------------------------------------------------------------- /playground/src/pages/config.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 34 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/app/lru-cache.ts: -------------------------------------------------------------------------------- 1 | import { deleteApp, type FirebaseApp } from 'firebase/app' 2 | import { LRUCache } from '@posva/lru-cache' 3 | import { logger } from '../logging' 4 | 5 | // TODO: allow customizing 6 | // TODO: find sensible defaults. Should they change depending on the platform? 7 | 8 | // copied from https://github.com/FirebaseExtended/firebase-framework-tools/blob/e69f5bdd44695274ad88dbb4e21aac778ba60cc8/src/constants.ts 9 | export const LRU_MAX_INSTANCES = 100 10 | export const LRU_TTL = 1_000 * 60 * 5 11 | export const appCache = new LRUCache({ 12 | max: LRU_MAX_INSTANCES, 13 | ttl: LRU_TTL, 14 | allowStale: true, 15 | // by default the cache deletes the app when getting it and it's stale 16 | // which creates errors about using a deleted app 17 | noDeleteOnStaleGet: true, 18 | updateAgeOnGet: true, 19 | updateAgeOnHas: true, 20 | dispose: (firebaseApp) => { 21 | logger.debug('Deleting Firebase app', firebaseApp.name) 22 | deleteApp(firebaseApp) 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { type Theme } from 'vitepress' 3 | import DefaultTheme from 'vitepress/theme' 4 | import '../style/vars.css' 5 | // import Layout from './Layout.vue' 6 | // import HomeSponsors from '../components/HomeSponsors.vue' 7 | import AsideSponsors from './components/AsideSponsors.vue' 8 | import FirebaseExample from '../components/FirebaseExample.vue' 9 | import RtdbLogo from '../components/RtdbLogo.vue' 10 | import FirestoreLogo from '../components/FirestoreLogo.vue' 11 | 12 | export default { 13 | extends: DefaultTheme, 14 | Layout() { 15 | return h(DefaultTheme.Layout, null, { 16 | // 'home-features-after': () => h(HomeSponsors), 17 | // 'aside-ads-before': () => h(AsideSponsors), 18 | }) 19 | }, 20 | enhanceApp({ app }) { 21 | // app.component('HomeSponsors', HomeSponsors) 22 | app.component('FirebaseExample', FirebaseExample) 23 | app.component('RtdbLogo', RtdbLogo) 24 | app.component('FirestoreLogo', FirestoreLogo) 25 | }, 26 | } satisfies Theme 27 | -------------------------------------------------------------------------------- /playground/firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /todos/{todoId} { 5 | allow read, write; 6 | } 7 | match /demo-todos/{todoId} { 8 | allow read, write; 9 | } 10 | match /vuexfireItems1/{id} { 11 | allow read, write; 12 | } 13 | match /vuexfireItems2/{id} { 14 | allow read, write; 15 | } 16 | match /configs/jORwjIykFo2NmkdzTkhU { 17 | allow read, write; 18 | } 19 | match /empty/{emptyId} { 20 | allow read; 21 | } 22 | match /none/{noneId} { 23 | allow read; 24 | } 25 | match /comentedTodos/{id} { 26 | allow read, write; 27 | } 28 | match /configs/jORwjIykFo2NmkdzTkhU { 29 | allow read, write; 30 | } 31 | match /bug-reports/{issue}/demo-files/{id} { 32 | allow read, write; 33 | } 34 | match /bug-reports/{issue}/demo/{id} { 35 | allow read, write; 36 | } 37 | match /{document=**} { 38 | allow read; 39 | allow write: if false; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /playground/src/stores/counter.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue' 2 | import { acceptHMRUpdate, defineStore } from 'pinia' 3 | import { doc, setDoc, updateDoc, type DocumentData } from 'firebase/firestore' 4 | import { useDocument, useFirestore } from 'vuefire' 5 | 6 | export const useCounterStore = defineStore('counter', () => { 7 | const count = ref(0) 8 | const doubleCount = computed(() => count.value * 2) 9 | 10 | const db = useFirestore() 11 | 12 | const countRef = doc(db, 'playground', 'pinia-counter').withConverter< 13 | number, 14 | DocumentData 15 | >({ 16 | toFirestore(n) { 17 | return { n } 18 | }, 19 | fromFirestore(snapshot) { 20 | return snapshot.data().n as number 21 | }, 22 | }) 23 | 24 | useDocument(countRef, { 25 | target: count, 26 | }) 27 | 28 | async function increment() { 29 | await setDoc(countRef, count.value + 1) 30 | } 31 | 32 | return { count, doubleCount, increment } 33 | }) 34 | 35 | if (import.meta.hot) { 36 | import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot)) 37 | } 38 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/emulators/storage.plugin.ts: -------------------------------------------------------------------------------- 1 | import { getStorage, connectStorageEmulator } from 'firebase/storage' 2 | import type { FirebaseApp } from 'firebase/app' 3 | import { logger } from '../logging' 4 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports' 5 | 6 | /** 7 | * Setups the Storage Emulators 8 | */ 9 | export default defineNuxtPlugin((nuxtApp) => { 10 | const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp 11 | if (connectedEmulators.has(firebaseApp)) { 12 | return 13 | } 14 | 15 | const { 16 | public: { vuefire }, 17 | } = useRuntimeConfig() 18 | 19 | const host = vuefire?.emulators?.storage?.host 20 | const port = vuefire?.emulators?.storage?.port 21 | 22 | if (!host || !port) { 23 | logger.warn('Storage emulator not connected, missing host or port') 24 | return 25 | } 26 | 27 | connectStorageEmulator(getStorage(firebaseApp), host, port) 28 | logger.info(`Storage emulator connected to http://${host}:${port}`) 29 | connectedEmulators.set(firebaseApp, true) 30 | }) 31 | 32 | const connectedEmulators = new WeakMap() 33 | -------------------------------------------------------------------------------- /scripts/verifyCommit.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { readFileSync } from 'node:fs' 3 | import path from 'node:path' 4 | 5 | // Define raw escape codes for colors 6 | const reset = '\x1b[0m' 7 | const red = '\x1b[31m' 8 | const green = '\x1b[32m' 9 | const bgRedWhite = '\x1b[41m\x1b[37m' 10 | 11 | const msgPath = path.resolve('.git/COMMIT_EDITMSG') 12 | const msg = readFileSync(msgPath, 'utf-8').trim() 13 | 14 | const commitRE = 15 | /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?: .{1,50}/ 16 | 17 | if (!commitRE.test(msg)) { 18 | console.log() 19 | console.error( 20 | ` ${bgRedWhite} ERROR ${reset} ${red}invalid commit message format.${reset}\n\n` + 21 | `${red} Proper commit message format is required for automated changelog generation. Examples:\n\n` + 22 | ` ${green}feat: add disableRoot option${reset}\n` + 23 | ` ${green}fix(view): handle keep-alive with aborted navigations (close #28)${reset}\n\n` + 24 | `${red} See .github/commit-convention.md for more details.${reset}\n` 25 | ) 26 | process.exit(1) 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/docs-prs.yml: -------------------------------------------------------------------------------- 1 | # From https://github.com/marketplace/actions/deploy-to-firebase-hosting 2 | 3 | name: Deploy Docs Preview on PR 4 | 5 | on: 6 | pull_request: 7 | paths: 8 | - 'docs/**' 9 | - 'src/**' # for api changes 10 | - '.github/workflows/docs.yml' 11 | 12 | jobs: 13 | build_and_preview: 14 | if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - run: corepack enable 19 | - name: Install Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 20 23 | cache: 'pnpm' 24 | 25 | - name: Install dependencies 26 | run: pnpm install --frozen-lockfile 27 | 28 | - name: Build docs 29 | run: pnpm run docs:build 30 | 31 | - uses: FirebaseExtended/action-hosting-deploy@v0 32 | with: 33 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 34 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_VUEFIREDOCS }}' 35 | projectId: vuefiredocs 36 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/emulators/database.plugin.ts: -------------------------------------------------------------------------------- 1 | import { getDatabase, connectDatabaseEmulator } from 'firebase/database' 2 | import type { FirebaseApp } from 'firebase/app' 3 | import { logger } from '../logging' 4 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports' 5 | 6 | /** 7 | * Setups the Database Emulators 8 | */ 9 | export default defineNuxtPlugin((nuxtApp) => { 10 | const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp 11 | if (connectedEmulators.has(firebaseApp)) { 12 | return 13 | } 14 | 15 | const { 16 | public: { vuefire }, 17 | } = useRuntimeConfig() 18 | 19 | const host = vuefire?.emulators?.database?.host 20 | const port = vuefire?.emulators?.database?.port 21 | 22 | if (!host || !port) { 23 | logger.warn('Database emulator not connected, missing host or port') 24 | return 25 | } 26 | 27 | connectDatabaseEmulator(getDatabase(firebaseApp), host, port) 28 | logger.info(`Database emulator connected to http://${host}:${port}`) 29 | connectedEmulators.set(firebaseApp, true) 30 | }) 31 | 32 | const connectedEmulators = new WeakMap() 33 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # From https://github.com/marketplace/actions/deploy-to-firebase-hosting 2 | 3 | name: Deploy Docs 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - 'docs/**' 11 | - 'src/**' # for api changes 12 | - 'package.json' # for version changes 13 | - '.github/workflows/docs.yml' 14 | 15 | jobs: 16 | deploy_live_website: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - run: corepack enable 21 | - name: Install Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 20 25 | cache: 'pnpm' 26 | 27 | - name: Install dependencies 28 | run: pnpm install --frozen-lockfile 29 | 30 | - name: Build docs 31 | run: pnpm run docs:build 32 | 33 | - uses: FirebaseExtended/action-hosting-deploy@v0 34 | with: 35 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 36 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_VUEFIREDOCS }}' 37 | projectId: vuefiredocs 38 | channelId: live 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 New feature proposal" 2 | description: Suggest an idea for VueFire 3 | labels: ['feature request'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for your interest in the project and taking the time to fill out this feature report! 9 | - type: textarea 10 | id: feature-description 11 | attributes: 12 | label: What problem is this solving 13 | description: 'A clear and concise description of what the problem is. Ex. when using the function X we cannot do Y.' 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: proposed-solution 18 | attributes: 19 | label: Proposed solution 20 | description: 'A clear and concise description of what you want to happen with an API proposal when applicable' 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: alternative 25 | attributes: 26 | label: Describe alternatives you've considered 27 | description: A clear and concise description of any alternative solutions or features you've considered. 28 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/emulators/firestore.plugin.ts: -------------------------------------------------------------------------------- 1 | import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore' 2 | import type { FirebaseApp } from 'firebase/app' 3 | import { logger } from '../logging' 4 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports' 5 | 6 | /** 7 | * Setups the Firestore Emulators 8 | */ 9 | export default defineNuxtPlugin((nuxtApp) => { 10 | const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp 11 | if (connectedEmulators.has(firebaseApp)) { 12 | return 13 | } 14 | 15 | const { 16 | public: { vuefire }, 17 | } = useRuntimeConfig() 18 | 19 | const host = vuefire?.emulators?.firestore?.host 20 | const port = vuefire?.emulators?.firestore?.port 21 | 22 | if (!host || !port) { 23 | logger.warn('Firestore emulator not connected, missing host or port') 24 | return 25 | } 26 | 27 | connectFirestoreEmulator(getFirestore(firebaseApp), host, port) 28 | logger.info(`Firestore emulator connected to http://${host}:${port}`) 29 | connectedEmulators.set(firebaseApp, true) 30 | }) 31 | 32 | const connectedEmulators = new WeakMap() 33 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/emulators/functions.plugin.ts: -------------------------------------------------------------------------------- 1 | import { getFunctions, connectFunctionsEmulator } from 'firebase/functions' 2 | import type { FirebaseApp } from 'firebase/app' 3 | import { logger } from '../logging' 4 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports' 5 | 6 | /** 7 | * Setups the Functions Emulators 8 | */ 9 | export default defineNuxtPlugin((nuxtApp) => { 10 | const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp 11 | if (connectedEmulators.has(firebaseApp)) { 12 | return 13 | } 14 | 15 | const { 16 | public: { vuefire }, 17 | } = useRuntimeConfig() 18 | 19 | const host = vuefire?.emulators?.functions?.host 20 | const port = vuefire?.emulators?.functions?.port 21 | 22 | if (!host || !port) { 23 | logger.warn('Functions emulator not connected, missing host or port') 24 | return 25 | } 26 | 27 | connectFunctionsEmulator(getFunctions(firebaseApp), host, port) 28 | logger.info(`Functions emulator connected to http://${host}:${port}`) 29 | connectedEmulators.set(firebaseApp, true) 30 | }) 31 | 32 | const connectedEmulators = new WeakMap() 33 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/admin/plugin.server.ts: -------------------------------------------------------------------------------- 1 | import { type App as AdminApp } from 'firebase-admin/app' 2 | import { ensureAdminApp } from 'vuefire/server' 3 | import { defineNuxtPlugin, useRequestEvent, useRuntimeConfig } from '#imports' 4 | 5 | export default defineNuxtPlugin(() => { 6 | const event = useRequestEvent() 7 | const { vuefire } = useRuntimeConfig() 8 | 9 | const firebaseAdminApp = ensureAdminApp(vuefire?.admin?.options) 10 | 11 | // TODO: Is this accessible within middlewares and api routes? or should we use a middleware to add it 12 | event.context.firebaseApp = firebaseAdminApp 13 | 14 | return { 15 | provide: { 16 | firebaseAdminApp, 17 | }, 18 | } 19 | }) 20 | 21 | // TODO: should the type extensions be added in a different way to the module? 22 | declare module 'h3' { 23 | interface H3EventContext { 24 | /** 25 | * Firebase Admin User Record. `null` if the user is not logged in or their token is no longer valid and requires a 26 | * refresh. 27 | * @experimental This API is experimental and may change in future releases. 28 | */ 29 | firebaseApp: AdminApp 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scripts/typedoc.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["../src/**/*.ts", "../packages/nuxt/src/**/*.ts"], 3 | "exclude": [ 4 | "**/*.spec.ts", 5 | "../packages/nuxt/playground", 6 | "../packages/nuxt/src/runtime" 7 | ], 8 | "compilerOptions": { 9 | "baseUrl": ".", 10 | "rootDir": "..", 11 | "outDir": "dist", 12 | "sourceMap": false, 13 | "noEmit": true, 14 | "paths": { 15 | "vuefire": ["../src"], 16 | "nuxt-vuefire": ["../packages/nuxt/src/module.ts"] 17 | }, 18 | 19 | "target": "esnext", 20 | "module": "esnext", 21 | "moduleResolution": "Bundler", 22 | "allowJs": false, 23 | "skipLibCheck": true, 24 | 25 | "noUnusedLocals": false, 26 | "strictNullChecks": true, 27 | "noImplicitAny": true, 28 | "noImplicitThis": true, 29 | "noImplicitReturns": false, 30 | "strict": true, 31 | "isolatedModules": true, 32 | 33 | "experimentalDecorators": true, 34 | "resolveJsonModule": true, 35 | "esModuleInterop": true, 36 | "removeComments": false, 37 | "jsx": "preserve", 38 | "lib": ["esnext", "dom"], 39 | "types": ["node"] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-present Eduardo San Martin Morote 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/.vitepress/style/vars.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Colors 3 | * -------------------------------------------------------------------------- */ 4 | 5 | :root { 6 | --vp-c-accent: #f78200; 7 | 8 | --vp-c-brand-1: var(--vp-c-green-1); 9 | --vp-c-brand-2: var(--vp-c-green-2); 10 | --vp-c-brand-3: var(--vp-c-green-3); 11 | --vp-c-brand-soft: var(--vp-c-green-soft); 12 | } 13 | 14 | /** 15 | * Component: Home 16 | * -------------------------------------------------------------------------- */ 17 | 18 | :root { 19 | --vp-home-hero-name-color: transparent; 20 | --vp-home-hero-name-background: -webkit-linear-gradient( 21 | 120deg, 22 | var(--vp-c-brand) 25%, 23 | var(--vp-c-accent) 24 | ); 25 | --vp-home-hero-image-background-image: linear-gradient( 26 | -45deg, 27 | /* var(--vp-c-brand) 30%, */ #3ab98260 25%, 28 | /* var(--vp-c-accent) */ #f7820090 29 | ); 30 | --vp-home-hero-image-filter: blur(30px); 31 | } 32 | 33 | @media (min-width: 640px) { 34 | :root { 35 | --vp-home-hero-image-filter: blur(56px); 36 | } 37 | } 38 | 39 | @media (min-width: 960px) { 40 | :root { 41 | --vp-home-hero-image-filter: blur(72px); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/globals.ts: -------------------------------------------------------------------------------- 1 | import type { FirebaseApp } from 'firebase/app' 2 | import { App, EffectScope, effectScope } from 'vue-demi' 3 | 4 | // @internal 5 | const scopeMap = new WeakMap() 6 | 7 | /** 8 | * Gets the VueFire global scope for the current app. Creates one if it doesn't exist. 9 | * @internal 10 | * 11 | * @param app - Vue App 12 | * @returns 13 | */ 14 | export function getGlobalScope(firebaseApp: FirebaseApp, app: App) { 15 | // we use the firebaseApp as a key because we are more likely to have access to it and it's supposed to be also unique 16 | // per app since it contains user data. 17 | if (!scopeMap.has(firebaseApp)) { 18 | const scope = effectScope(true) 19 | scopeMap.set(firebaseApp, scope) 20 | // TODO: only Vue 3.5+ 21 | // app.onUnmount(() => { 22 | // scope.stop() 23 | // scopeMap.delete(firebaseApp) 24 | // }) 25 | const { unmount } = app 26 | // dispose up the scope when the app is unmounted 27 | app.unmount = () => { 28 | unmount.call(app) 29 | scope.stop() 30 | scopeMap.delete(firebaseApp) 31 | } 32 | } 33 | 34 | return scopeMap.get(firebaseApp)! 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/vitest.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your vuefire workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | "New Describe case": { 10 | "scope": "javascript,typescript", 11 | "prefix": "describe", 12 | "body": [ 13 | "describe('$1', () => {", 14 | " $0", 15 | "})" 16 | ], 17 | "description": "Add a new describe case" 18 | }, 19 | "New test case": { 20 | "scope": "javascript,typescript", 21 | "prefix": "vit", 22 | "body": [ 23 | "it('$1', ${2:async }() => {", 24 | " $0", 25 | "})" 26 | ], 27 | "description": "Add a new test case" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/emulators/auth.plugin.ts: -------------------------------------------------------------------------------- 1 | import { connectAuthEmulator, getAuth } from 'firebase/auth' 2 | import type { FirebaseApp } from 'firebase/app' 3 | import { logger } from '../logging' 4 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports' 5 | 6 | /** 7 | * Setups the auth Emulators 8 | */ 9 | export default defineNuxtPlugin((nuxtApp) => { 10 | const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp 11 | if (connectedEmulators.has(firebaseApp)) { 12 | return 13 | } 14 | 15 | const { 16 | public: { vuefire }, 17 | } = useRuntimeConfig() 18 | 19 | const host = vuefire?.emulators?.auth?.host 20 | const port = vuefire?.emulators?.auth?.port 21 | 22 | if (!host || !port) { 23 | logger.warn('Auth emulator not connected, missing host or port') 24 | return 25 | } 26 | 27 | connectAuthEmulator( 28 | // NOTE: it's fine to use getAuth here because emulators are for dev only 29 | getAuth(firebaseApp), 30 | `http://${host}:${port}`, 31 | vuefire?.emulators?.auth?.options 32 | ) 33 | logger.info(`Auth emulator connected to http://${host}:${port}`) 34 | connectedEmulators.set(firebaseApp, true) 35 | }) 36 | 37 | const connectedEmulators = new WeakMap() 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - 'playground/**' 8 | - 'examples/**' 9 | 10 | pull_request: 11 | paths-ignore: 12 | - 'docs/**' 13 | - 'playground/**' 14 | - 'examples/**' 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: pnpm/action-setup@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: '>=20' 29 | cache: 'pnpm' 30 | 31 | - name: Install 32 | run: pnpm install --frozen-lockfile 33 | 34 | - name: Install firebase-tools 35 | run: pnpm add -g firebase-tools 36 | 37 | - name: Lint 38 | run: pnpm run lint 39 | 40 | - name: Types 41 | run: pnpm run test:types 42 | 43 | - name: Test 44 | run: firebase emulators:exec 'pnpm run test:unit' 45 | 46 | - name: Build 47 | run: pnpm run build 48 | 49 | - name: Nuxt module build 50 | working-directory: ./packages/nuxt 51 | run: pnpm run build 52 | 53 | - uses: codecov/codecov-action@v4 54 | with: 55 | token: ${{ secrets.CODECOV_TOKEN }} 56 | -------------------------------------------------------------------------------- /packages/nuxt/playground/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "runtime": "nodejs20", 4 | "source": ".output/server" 5 | }, 6 | "hosting": { 7 | "public": ".output/public", 8 | "cleanUrls": true, 9 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 10 | "rewrites": [ 11 | { 12 | "source": "**", 13 | "function": "server" 14 | } 15 | ] 16 | }, 17 | "database": { 18 | "rules": "database.rules.json" 19 | }, 20 | "firestore": { 21 | "rules": "firestore.rules", 22 | "indexes": "firestore.indexes.json" 23 | }, 24 | "storage": { 25 | "rules": "storage.rules" 26 | }, 27 | "emulators": { 28 | "auth": { 29 | "host": "127.0.0.1", 30 | "port": 9099 31 | }, 32 | "functions": { 33 | "host": "127.0.0.1", 34 | "port": 5001 35 | }, 36 | "firestore": { 37 | "host": "127.0.0.1", 38 | "port": 8080 39 | }, 40 | "database": { 41 | "host": "127.0.0.1", 42 | "port": 8081 43 | }, 44 | "hosting": { 45 | "host": "127.0.0.1", 46 | "port": 5050 47 | }, 48 | "storage": { 49 | "host": "127.0.0.1", 50 | "port": 9199 51 | }, 52 | "ui": { 53 | "enabled": true 54 | }, 55 | "singleProjectMode": true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/nuxt/playground/app.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 39 | 40 | 59 | -------------------------------------------------------------------------------- /packages/nuxt/playground/firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /todos/{todoId} { 5 | allow read, write; 6 | } 7 | match /tweets/{todoId} { 8 | allow read, write; 9 | } 10 | match /users/{todoId} { 11 | allow read: if request.auth.uid != null; 12 | allow write: if false; 13 | } 14 | match /demo-todos/{todoId} { 15 | allow read, write; 16 | } 17 | match /vuexfireItems1/{id} { 18 | allow read, write; 19 | } 20 | match /vuexfireItems2/{id} { 21 | allow read, write; 22 | } 23 | match /configs/jORwjIykFo2NmkdzTkhU { 24 | allow read, write; 25 | } 26 | match /empty/{emptyId} { 27 | allow read; 28 | } 29 | match /none/{noneId} { 30 | allow read; 31 | } 32 | match /comentedTodos/{id} { 33 | allow read, write; 34 | } 35 | match /configs/jORwjIykFo2NmkdzTkhU { 36 | allow read, write; 37 | } 38 | 39 | match /secrets/{userId} { 40 | allow read, write: if request.auth.uid == userId; 41 | } 42 | 43 | match /bug-reports/{issueId}/objects/{documentId} { 44 | allow read, update; 45 | allow create: if false; 46 | allow delete: if false; 47 | } 48 | 49 | match /{document=**} { 50 | allow read, write: if false; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/nuxt/playground/pages/firestore-secure-doc.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 44 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/HomeSponsors.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 39 | 40 | 53 | -------------------------------------------------------------------------------- /packages/nuxt/playground/pages/bug-playground.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 52 | -------------------------------------------------------------------------------- /playground/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "firestore": { 6 | "rules": "firestore.rules", 7 | "indexes": "firestore.indexes.json" 8 | }, 9 | "functions": [ 10 | { 11 | "source": "functions", 12 | "codebase": "default", 13 | "ignore": [ 14 | "node_modules", 15 | ".git", 16 | "firebase-debug.log", 17 | "firebase-debug.*.log" 18 | ], 19 | "predeploy": [ 20 | "npm --prefix \"$RESOURCE_DIR\" run lint", 21 | "npm --prefix \"$RESOURCE_DIR\" run build" 22 | ] 23 | } 24 | ], 25 | "hosting": { 26 | "public": "dist", 27 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 28 | "rewrites": [ 29 | { 30 | "source": "**", 31 | "destination": "/index.html" 32 | } 33 | ] 34 | }, 35 | "storage": { 36 | "rules": "storage.rules" 37 | }, 38 | "emulators": { 39 | "auth": { 40 | "port": 9099 41 | }, 42 | "functions": { 43 | "port": 5001 44 | }, 45 | "firestore": { 46 | "port": 8080 47 | }, 48 | "database": { 49 | "port": 8081 50 | }, 51 | "storage": { 52 | "port": 9199 53 | }, 54 | "ui": { 55 | "enabled": true 56 | }, 57 | "singleProjectMode": true 58 | }, 59 | "remoteconfig": { 60 | "template": "remoteconfig.template.json" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: 'home' 3 | 4 | title: VueFire 5 | titleTemplate: Official Firebase bindings for Vue.js 6 | 7 | hero: 8 | name: VueFire 9 | text: Official Firebase bindings for Vue.js 10 | tagline: Idiomatic composables for realtime data and other Firebase services 11 | image: 12 | src: /logo.svg 13 | alt: VueFire logo 14 | actions: 15 | - theme: brand 16 | text: Get Started 17 | link: /guide/getting-started 18 | - theme: alt 19 | text: View on GitHub 20 | link: https://github.com/vuejs/vuefire 21 | 22 | features: 23 | - title: Idiomatic 24 | details: Use composables that align with the declarative approach of Vue. Everything that can be automatically handled by VueFire is. Nested Collections, Document References, and more, are all handled for you. 25 | - title: Performant 26 | details: VueFire only handles the binding for you so your state is always up to date with the server. You'll still be able to use the Firebase JS SDK to its full potential! 27 | - title: Flexible 28 | details: Use Firebase Database, Firestore, Authentication, etc. VueFire exposes tree-shakable APIs that are built on top of the Firebase modular JS SDK. 29 | 30 | footer: MIT Licensed | Copyright © 2016-present Eduardo San Martin Morote 31 | --- 32 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /packages/nuxt/tests/fixtures/basic/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import MyModule from '../../../src/module' 2 | 3 | export default defineNuxtConfig({ 4 | modules: [MyModule], 5 | 6 | vuefire: { 7 | auth: { 8 | enabled: true, 9 | sessionCookie: true, 10 | // popupRedirectResolver: false, 11 | // persistence: ['indexedDBLocal'] 12 | }, 13 | appCheck: { 14 | // TODO: could automatically pick up a debug token defined as an env variable 15 | debug: process.env.NODE_ENV !== 'production', 16 | isTokenAutoRefreshEnabled: true, 17 | provider: 'ReCaptchaV3', 18 | key: '6LfJ0vgiAAAAAHheQE7GQVdG_c9m8xipBESx_SKI', 19 | }, 20 | 21 | emulators: { 22 | enabled: true, 23 | 24 | auth: { 25 | options: { 26 | // removes the HTML footer and console warning 27 | disableWarnings: process.env.NODE_ENV === 'development', 28 | }, 29 | }, 30 | }, 31 | 32 | config: { 33 | apiKey: 'AIzaSyAkUKe36TPWL2eZTshgk-Xl4bY_R5SB97U', 34 | authDomain: 'vue-fire-store.firebaseapp.com', 35 | databaseURL: 'https://vue-fire-store.firebaseio.com', 36 | projectId: 'vue-fire-store', 37 | storageBucket: 'vue-fire-store.appspot.com', 38 | messagingSenderId: '998674887640', 39 | appId: '1:998674887640:web:1e2bb2cc3e5eb2fc3478ad', 40 | measurementId: 'G-RL4BTWXKJ7', 41 | }, 42 | 43 | // admin: {}, 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited! 4 | 5 | We accept contributions via Pull Requests on [Github]({{ githubAccount }}/{{ name }}). 6 | 7 | ## Setup 8 | 9 | - Use `pnpm@8` 10 | - Install `firebase-tools` globally: `npm install -g firebase-tools` 11 | 12 | ## Pull Requests 13 | 14 | Here are some guidelines to make the process smoother: 15 | 16 | - **Add a test** - New features and bugfixes need tests. If you find it difficult to test, please tell us in the pull request and we will try to help you! 17 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 18 | - **Run `npm test` locally** - This will allow you to go faster 19 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 20 | - **Send coherent history** - Make sure your commits message means something 21 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 22 | 23 | ## Project structure 24 | 25 | ### Root 26 | 27 | The root folder includes the VueFire library 28 | 29 | ### `packages/nuxt` 30 | 31 | This folder includes the Nuxt module. To have proper TS support in your IDE, you might need to open this folder individually rather than opening the root folder of the VueFire project. 32 | 33 | ### `playground` 34 | 35 | Includes a Vue 3 playground to test out with a real app. 36 | -------------------------------------------------------------------------------- /docs/cookbook/subscriptions-external.md: -------------------------------------------------------------------------------- 1 | # Binding Firebase Reference to existing Vue Refs 2 | 3 | Sometimes, you might want to reuse an existing `ref()`. This might be because you want to write the Firebase data to a _custom ref_ coming from a composable or because the ref comes from an existing reactivity source like a Vuex/Pinia store. This can be achieved by passing the `ref()` to the `target` option of the composable: 4 | 5 | ```ts 6 | todos // given an existing Ref 7 | const { pending } = useCollection(todoListRef, { target: todos }) 8 | ``` 9 | 10 | When passing a target ref, the composable will not create a new `ref()` for you, but will instead use the one you passed. It will also not return the `ref()` as a result, but instead return an object with some useful properties. You can find more about this in [the declarative subscriptions](../guide/realtime-data.md) section. 11 | 12 | ## Pinia 13 | 14 | If you are using [Pinia](https://pinia.vuejs.org), you can directly use the `useCollection()` function within [setup stores](https://pinia.vuejs.org/cookbook/composables.html#setup-stores): 15 | 16 | ```ts 17 | import { defineStore } from 'pinia' 18 | 19 | export const useTodoStore = defineStore('todos', () => { 20 | const todos = useCollection(todoListRef) 21 | 22 | return { todos } 23 | }) 24 | ``` 25 | 26 | Note you will still have to follow the [Firebase API](https://firebase.google.com/docs/firestore/manage-data/structure-data) (e.g. `addDoc()`, `updateDoc()`, etc) to update the data and you will also need [to wait for the data to be loaded on the server](../guide/ssr.md#usage-outside-of-components). 27 | -------------------------------------------------------------------------------- /docs/.vitepress/meta.ts: -------------------------------------------------------------------------------- 1 | // noinspection ES6PreferShortImport: IntelliJ IDE hint to avoid warning to use `~/contributors`, will fail on build if changed 2 | 3 | /* Texts */ 4 | export const headTitle = 'VueFire' 5 | export const headSubtitle = 'VueFire' 6 | export const headDescription = 'Official Firebase bindings for Vue.js' 7 | 8 | /* CDN fonts and styles */ 9 | export const googleapis = 'https://fonts.googleapis.com' 10 | export const gstatic = 'https://fonts.gstatic.com' 11 | export const font = `${googleapis}/css2?family=Readex+Pro:wght@200;400;600&display=swap` 12 | 13 | /* vitepress head */ 14 | export const ogUrl = 'https://vuefire.vuejs.org/' 15 | export const ogImage = `${ogUrl}og.png` 16 | 17 | /* GitHub and social links */ 18 | export const github = 'https://github.com/vuejs/vuefire' 19 | export const releases = 'https://github.com/vuejs/vuefire/releases' 20 | export const contributing = 21 | 'https://github.com/vuejs/vuefire/blob/main/.github/CONTRIBUTING.md' 22 | export const discord = '' 23 | export const twitter = 'https://twitter.com/posva' 24 | 25 | /* Avatar/Image/Sponsors servers */ 26 | export const preconnectLinks = [googleapis, gstatic] 27 | export const preconnectHomeLinks = [googleapis, gstatic] 28 | 29 | /* PWA runtime caching urlPattern regular expressions */ 30 | export const pwaFontsRegex = new RegExp(`^${googleapis}/.*`, 'i') 31 | export const pwaFontStylesRegex = new RegExp(`^${gstatic}/.*`, 'i') 32 | // eslint-disable-next-line prefer-regex-literals 33 | export const githubusercontentRegex = new RegExp( 34 | '^https://((i.ibb.co)|((raw|user-images).githubusercontent.com))/.*', 35 | 'i' 36 | ) 37 | -------------------------------------------------------------------------------- /docs/cookbook/vuex.md: -------------------------------------------------------------------------------- 1 | # Usage with Vuex 2 | 3 | Vuex is supported but due to its nature with mutations, it's a bit more verbose to use than Pinia. It's recommended to use Pinia instead of Vuex if you can, your DX will also improve. 4 | 5 | You must set the `strict` option to `false` in order to use Vuex with VueFire. 6 | 7 | You can can call `useCollection()` and other composables from VueFire within your components to connect to your store: 8 | 9 | ```ts 10 | import { doc } from 'firebase/firestore' 11 | import { toRef } from 'vue' 12 | import { useStore } from 'vuex' 13 | import { useDocument } from 'vuefire' 14 | 15 | const store = useStore() 16 | const userDataRef = doc(firestore, 'users', userId) 17 | 18 | const user = toRef(store.state, 'user') 19 | 20 | useDocument(userDataRef, { target: user }) 21 | ``` 22 | 23 | In this scenario, the Firebase subscription will stop when the component is unmounted. In order to keep the subscription alive after the component gets unmounted, use [an `effectScope()`](https://vuejs.org/api/reactivity-advanced.html#effectscope) within an action: 24 | 25 | ```ts 26 | // create and export a detached effect scope next to where you create your store 27 | export const scope = effectScope(true) 28 | 29 | export store = createStore({ 30 | // ... 31 | }) 32 | ``` 33 | 34 | Then you must call the `useDocument()`, `useCollection()` and other composables from VueFire within that effect scope like this: 35 | 36 | ```ts 37 | scope.run(() => { 38 | useDocument(userDataRef, { target: user }) 39 | }) 40 | ``` 41 | 42 | The good thing is you can call this **anywhere in your app**, you are not limited to doing this inside `setup()`. 43 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/auth/plugin.client.ejs: -------------------------------------------------------------------------------- 1 | import { VueFireAuthWithDependencies, _VueFireAuthKey } from 'vuefire' 2 | import { defineNuxtPlugin } from '#imports' 3 | import { inject } from 'vue' 4 | import { 5 | <% if(options.persistence) { %> 6 | <% options.persistence.forEach((persistenceName) => { %> 7 | <%= persistenceName %>Persistence, 8 | <% }) %> 9 | <% } %> 10 | 11 | <% if(options.popupRedirectResolver) { %> 12 | <%= options.popupRedirectResolver %>PopupRedirectResolver, 13 | <% } %> 14 | 15 | <% if(options.errorMap) { %> 16 | <%= options.errorMap %>ErrorMap, 17 | <% } %> 18 | } from 'firebase/auth' 19 | 20 | /** 21 | * Setups VueFireAuth for the client. This version creates some listeners that shouldn't be set on server. 22 | */ 23 | export default defineNuxtPlugin((nuxtApp) => { 24 | const firebaseApp = nuxtApp.$firebaseApp 25 | 26 | VueFireAuthWithDependencies({ 27 | initialUser: nuxtApp.payload.vuefireUser, 28 | dependencies: { 29 | <% if(options.errorMap) { %> 30 | errorMap: <%= options.errorMap %>ErrorMap, 31 | <% } %> 32 | 33 | <% if(options.persistence) { %> 34 | persistence: [ 35 | <% options.persistence.forEach((persistenceName) => { %> 36 | <%= persistenceName %>Persistence, 37 | <% }) %> 38 | ], 39 | <% } %> 40 | 41 | <% if(options.popupRedirectResolver) { %> 42 | popupRedirectResolver: <%= options.popupRedirectResolver %>PopupRedirectResolver, 43 | <% } %> 44 | }, 45 | })(firebaseApp, nuxtApp.vueApp) 46 | 47 | return { 48 | provide: { 49 | firebaseAuth: nuxtApp.vueApp.runWithContext(() => inject(_VueFireAuthKey)) 50 | }, 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /packages/nuxt/playground/pages/database-todo-list.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 66 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/app-check/plugin.client.ts: -------------------------------------------------------------------------------- 1 | import type { FirebaseApp } from 'firebase/app' 2 | import { 3 | ReCaptchaV3Provider, 4 | ReCaptchaEnterpriseProvider, 5 | CustomProvider, 6 | type AppCheckOptions, 7 | } from 'firebase/app-check' 8 | import { VueFireAppCheck } from 'vuefire' 9 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports' 10 | 11 | /** 12 | * Plugin to initialize the appCheck module on the server. 13 | */ 14 | export default defineNuxtPlugin((nuxtApp) => { 15 | const runtimeConfig = useRuntimeConfig() 16 | // NOTE: appCheck is present because of the check in module.ts 17 | const options = runtimeConfig.public.vuefire!.appCheck! 18 | const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp 19 | 20 | let provider: AppCheckOptions['provider'] 21 | 22 | if (options.provider === 'ReCaptchaV3') { 23 | provider = new ReCaptchaV3Provider(options.key) 24 | } else if (options.provider === 'ReCaptchaEnterprise') { 25 | provider = new ReCaptchaEnterpriseProvider(options.key) 26 | } else { 27 | // default provider that fails 28 | provider = new CustomProvider({ 29 | getToken: () => 30 | Promise.reject( 31 | process.env.NODE_ENV !== 'production' 32 | ? new Error( 33 | `[nuxt-vuefire]: Unknown Provider "${ 34 | // @ts-expect-error: options.provider is never here 35 | options.provider 36 | }".` 37 | // eslint-disable-next-line indent 38 | ) 39 | : new Error('app-check/invalid-provider') 40 | ), 41 | }) 42 | } 43 | 44 | VueFireAppCheck({ 45 | ...options, 46 | provider, 47 | })(firebaseApp, nuxtApp.vueApp) 48 | }) 49 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/auth/plugin-mint-cookie.client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | onIdTokenChanged, 3 | beforeAuthStateChanged, 4 | type User, 5 | type Auth, 6 | } from 'firebase/auth' 7 | import { defineNuxtPlugin } from '#imports' 8 | 9 | /** 10 | * Sets up a watcher that mints a cookie based auth session. On the server, it reads the cookie to 11 | * generate the proper auth state. **Must be added after the firebase auth plugin.** 12 | */ 13 | export default defineNuxtPlugin((nuxtApp) => { 14 | const auth = nuxtApp.$firebaseAuth as Auth 15 | 16 | // send a post request to the server when auth state changes to mint a cookie 17 | beforeAuthStateChanged( 18 | auth, 19 | // if this fails, we rollback the auth state 20 | mintCookie, 21 | () => { 22 | // rollback the auth state 23 | mintCookie(auth.currentUser) 24 | } 25 | ) 26 | 27 | // we need both callback to avoid some race conditions 28 | onIdTokenChanged(auth, mintCookie) 29 | }) 30 | 31 | // TODO: should this be throttled to avoid multiple calls 32 | /** 33 | * Sends a post request to the server to mint a cookie based auth session. The name of the cookie is defined in the 34 | * api.session.ts file. 35 | * 36 | * @param user - the user to mint a cookie for 37 | */ 38 | async function mintCookie(user: User | null) { 39 | const jwtToken = await user?.getIdToken(/* forceRefresh */ true) 40 | // throws if the server returns an error so that beforeAuthStateChanged can catch it to cancel 41 | await $fetch( 42 | // '/api/__session-server', 43 | '/api/__session', 44 | { 45 | method: 'POST', 46 | // if the token is undefined, the server will delete the cookie 47 | body: { token: jwtToken }, 48 | } 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | # playground 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | pnpm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | pnpm dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | pnpm build 40 | ``` 41 | -------------------------------------------------------------------------------- /playground/src/pages/rtdb-todos.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 70 | -------------------------------------------------------------------------------- /playground/src/demos/TodoList/components/TodoItem.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 70 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/AsideSponsors.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 75 | -------------------------------------------------------------------------------- /packages/nuxt/playground/components/TodoItem.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 78 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/payload-plugin.ts: -------------------------------------------------------------------------------- 1 | import { Timestamp, GeoPoint } from 'firebase/firestore' 2 | import { markRaw } from 'vue' 3 | import { 4 | definePayloadPlugin, 5 | definePayloadReducer, 6 | definePayloadReviver, 7 | } from '#imports' 8 | 9 | /** 10 | * Handles Firestore Timestamps, GeoPoint, and other types that needs special handling for serialization. 11 | */ 12 | export default definePayloadPlugin(() => { 13 | definePayloadReducer( 14 | 'FirebaseTimestamp', 15 | (data: unknown) => data instanceof Timestamp && data.toJSON() 16 | ) 17 | definePayloadReviver( 18 | 'FirebaseTimestamp', 19 | (data: ReturnType) => { 20 | return markRaw(new Timestamp(data.seconds, data.nanoseconds)) 21 | } 22 | ) 23 | 24 | definePayloadReducer( 25 | 'FirebaseGeoPoint', 26 | (data: unknown) => data instanceof GeoPoint && data.toJSON() 27 | ) 28 | definePayloadReviver( 29 | 'FirebaseGeoPoint', 30 | (data: ReturnType) => { 31 | return markRaw(new GeoPoint(data.latitude, data.longitude)) 32 | } 33 | ) 34 | 35 | // to handle the `id` non-enumerable property 36 | definePayloadReducer('DocumentData', (data: any) => { 37 | if (data && typeof data === 'object') { 38 | const idProp = Object.getOwnPropertyDescriptor(data, 'id') 39 | // we need the non enumerable id property as it's likely used 40 | if (idProp && !idProp.enumerable) { 41 | return { 42 | ...data, 43 | id: data.id, 44 | } 45 | } 46 | } 47 | }) 48 | definePayloadReviver( 49 | 'DocumentData', 50 | (data: string | Record) => { 51 | const parsed = typeof data === 'string' ? JSON.parse(data) : data 52 | // preserve the non-enumerable property 53 | // we need to delete it first 54 | const idValue = parsed.id 55 | delete parsed.id 56 | return Object.defineProperty(parsed, 'id', { 57 | value: idValue, 58 | }) 59 | } 60 | ) 61 | }) 62 | -------------------------------------------------------------------------------- /playground/typed-router.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️ 5 | // It's recommended to commit this file. 6 | // Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry. 7 | 8 | declare module 'vue-router/auto-routes' { 9 | import type { 10 | RouteRecordInfo, 11 | ParamValue, 12 | ParamValueOneOrMore, 13 | ParamValueZeroOrMore, 14 | ParamValueZeroOrOne, 15 | } from 'vue-router' 16 | 17 | /** 18 | * Route name map generated by unplugin-vue-router 19 | */ 20 | export interface RouteNamedMap { 21 | '/': RouteRecordInfo<'/', '/', Record, Record>, 22 | '/app-check': RouteRecordInfo<'/app-check', '/app-check', Record, Record>, 23 | '/authentication': RouteRecordInfo<'/authentication', '/authentication', Record, Record>, 24 | '/config': RouteRecordInfo<'/config', '/config', Record, Record>, 25 | '/converter-with-number': RouteRecordInfo<'/converter-with-number', '/converter-with-number', Record, Record>, 26 | '/nested-collections': RouteRecordInfo<'/nested-collections', '/nested-collections', Record, Record>, 27 | '/nested-refs-list': RouteRecordInfo<'/nested-refs-list', '/nested-refs-list', Record, Record>, 28 | '/pinia-store': RouteRecordInfo<'/pinia-store', '/pinia-store', Record, Record>, 29 | '/rtdb-todos': RouteRecordInfo<'/rtdb-todos', '/rtdb-todos', Record, Record>, 30 | '/storage': RouteRecordInfo<'/storage', '/storage', Record, Record>, 31 | '/todos': RouteRecordInfo<'/todos', '/todos', Record, Record>, 32 | '/vuex-store': RouteRecordInfo<'/vuex-store', '/vuex-store', Record, Record>, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/guide/global-options.md: -------------------------------------------------------------------------------- 1 | # Firestore and Database global Options 2 | 3 | If you find yourself passing around the same options to `useDocument()`, `useDatabaseObject()`, ..., you can use the global options to avoid repeating yourself: 4 | 5 | 6 | 7 | ```ts 8 | import { globalDatabaseOptions } from 'vuefire' 9 | 10 | globalDatabaseOptions.serialize = ... 11 | ``` 12 | 13 | ```ts 14 | import { globalFirestoreOptions } from 'vuefire' 15 | 16 | globalFirestoreOptions.converter = ... 17 | ``` 18 | 19 | 20 | 21 | Changing these options will affect **all calls** to `useDocument()`, `useDatabaseObject()`, ... in your application **as well as Options API calls** (`$firestoreBind()`, `$rtdbBind()`). 22 | 23 | ## Custom `serialize`/`converter` 24 | 25 | When adapting `serialize`/`converter` or using `.withConverter()`, **you need to make sure the returned objects contain their original `id`** so other VueFire functionalities can work correctly. The easies way to do this is by reusing the default `serialize`/`converter`: 26 | 27 | 28 | 29 | ```ts 30 | import { databaseDefaultSerializer, globalDatabaseOptions } from 'vuefire' 31 | 32 | globalDatabaseOptions.serialize = (snapshot) => { 33 | const data = databaseDefaultSerializer(snapshot) 34 | // add anything custom to the returned object 35 | data.metadata = snapshot.metadata 36 | return data 37 | } 38 | ``` 39 | 40 | ```ts 41 | import { firestoreDefaultConverter, globalFirestoreOptions } from 'vuefire' 42 | 43 | globalFirestoreOptions.converter = { 44 | // the default converter just returns the data: (data) => data 45 | toFirestore: firestoreDefaultConverter.toFirestore, 46 | fromFirestore: (snapshot, options) => { 47 | const data = firestoreDefaultConverter.fromFirestore(snapshot, options) 48 | // if the document doesn't exist, return null 49 | if (!data) return null 50 | // add anything custom to the returned object 51 | data.metadata = snapshot.metadata 52 | return data 53 | }, 54 | } 55 | ``` 56 | 57 | 58 | -------------------------------------------------------------------------------- /tests/database/ssr.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment node 3 | */ 4 | import { beforeEach, describe, it, expect } from 'vitest' 5 | import { firebaseApp, setupDatabaseRefs } from '../utils' 6 | import { ShallowUnwrapRef } from 'vue' 7 | import { useDatabaseObject } from '../../src' 8 | import { createSSRApp } from 'vue' 9 | import { renderToString, ssrInterpolate } from '@vue/server-renderer' 10 | import { clearPendingPromises } from '../../src/ssr/plugin' 11 | import { _initialStatesMap } from '../../src/ssr/initialState' 12 | 13 | describe('Database SSR', async () => { 14 | const { databaseRef, set } = setupDatabaseRefs() 15 | 16 | beforeEach(() => { 17 | clearPendingPromises(firebaseApp) 18 | // delete any ssr state 19 | _initialStatesMap.delete(firebaseApp) 20 | }) 21 | 22 | function createMyApp( 23 | setup: () => T, 24 | render: (ctx: ShallowUnwrapRef>) => unknown 25 | ) { 26 | const App = { 27 | ssrRender(ctx: any, push: any, _parent: any) { 28 | push(`

${ssrInterpolate(render(ctx))}

`) 29 | }, 30 | setup, 31 | } 32 | 33 | const app = createSSRApp(App) 34 | 35 | return { app } 36 | } 37 | 38 | it('can await within setup', async () => { 39 | const docRef = databaseRef() 40 | await set(docRef, { name: 'a' }) 41 | const { app } = createMyApp( 42 | async () => { 43 | const { data, promise } = useDatabaseObject<{ name: string }>(docRef) 44 | await promise.value 45 | return { data } 46 | }, 47 | ({ data }) => data!.name 48 | ) 49 | 50 | expect(await renderToString(app)).toBe(`

a

`) 51 | }) 52 | 53 | it('works without await', async () => { 54 | const docRef = databaseRef() 55 | await set(docRef, { name: 'hello' }) 56 | const { app } = createMyApp( 57 | () => { 58 | const data = useDatabaseObject<{ name: string }>(docRef) 59 | return { data } 60 | }, 61 | ({ data }) => data!.name 62 | ) 63 | 64 | expect(await renderToString(app)).toBe(`

hello

`) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /docs/guide/app-check.md: -------------------------------------------------------------------------------- 1 | # Firebase App Check 2 | 3 | [Firebase App Check](https://firebase.google.com/docs/app-check#web) helps protect your API resources from abuse by preventing unauthorized clients from accessing your backend resources. It works with both Firebase services, Google Cloud services, and your own APIs to keep your resources safe. 4 | 5 | ## Installation 6 | 7 | Start by adding the `VueFireAppCheck` module to the `VueFire` plugin: 8 | 9 | ```ts 10 | import { VueFire, VueFireAppCheck } from 'vuefire' 11 | app.use(VueFire, { 12 | firebaseApp: createFirebaseApp(), 13 | modules: [ 14 | // ... other modules 15 | VueFireAppCheck({ 16 | // app check options 17 | }), 18 | ], 19 | }) 20 | ``` 21 | 22 | In order to use App Check you need to enable it in the Firebase Console > App Check. You also need to setup [a reCAPTCHA provider](https://firebase.google.com/docs/app-check#web), then provide it in the `VueFireAppCheck` module: 23 | 24 | ```ts{2,9} 25 | import { VueFire, VueFireAppCheck } from 'vuefire' 26 | import { ReCaptchaV3Provider } from 'firebase/app-check' 27 | 28 | app.use(VueFire, { 29 | firebaseApp: createFirebaseApp(), 30 | modules: [ 31 | // ... other modules 32 | VueFireAppCheck({ 33 | provider: new ReCaptchaV3Provider('...') 34 | isTokenAutoRefreshEnabled: true, 35 | }), 36 | ], 37 | }) 38 | ``` 39 | 40 | During development, it might be convenient to use a debug token by setting `debug` to `true`. You can then add it to your debug tokens in the Firebase Console > App Check > Apps > Manage Debug Tokens. 41 | 42 | ```ts{10-11} 43 | import { VueFire, VueFireAppCheck } from 'vuefire' 44 | import { ReCaptchaV3Provider } from 'firebase/app-check' 45 | 46 | app.use(VueFire, { 47 | firebaseApp: createFirebaseApp(), 48 | modules: [ 49 | // ... other modules 50 | VueFireAppCheck({ 51 | provider: new ReCaptchaV3Provider('...') 52 | // Only use debug during development 53 | debug: process.env.NODE_ENV !== 'production', 54 | isTokenAutoRefreshEnabled: true, 55 | }), 56 | ], 57 | }) 58 | ``` 59 | -------------------------------------------------------------------------------- /tests/firestore/ssr.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @vitest-environment node 3 | */ 4 | import { beforeEach, describe, it, expect } from 'vitest' 5 | import { setupFirestoreRefs, firebaseApp } from '../utils' 6 | import { ShallowUnwrapRef } from 'vue' 7 | import { useDocument } from '../../src' 8 | import { createSSRApp } from 'vue' 9 | import { renderToString, ssrInterpolate } from '@vue/server-renderer' 10 | import { clearPendingPromises } from '../../src/ssr/plugin' 11 | import { _initialStatesMap } from '../../src/ssr/initialState' 12 | 13 | describe('Firestore SSR', async () => { 14 | const { collection, query, addDoc, setDoc, updateDoc, deleteDoc, doc } = 15 | setupFirestoreRefs() 16 | 17 | beforeEach(() => { 18 | clearPendingPromises(firebaseApp) 19 | // delete any ssr state 20 | _initialStatesMap.delete(firebaseApp) 21 | }) 22 | 23 | function createMyApp( 24 | setup: () => T, 25 | render: (ctx: ShallowUnwrapRef>) => unknown 26 | ) { 27 | const App = { 28 | ssrRender(ctx: any, push: any, _parent: any) { 29 | push(`

${ssrInterpolate(render(ctx))}

`) 30 | }, 31 | setup, 32 | } 33 | 34 | const app = createSSRApp(App) 35 | 36 | return { app } 37 | } 38 | 39 | it('can await within setup', async () => { 40 | const docRef = doc<{ name: string }>() 41 | await setDoc(docRef, { name: 'a' }) 42 | const { app } = createMyApp( 43 | async () => { 44 | const { data, promise } = useDocument(docRef) 45 | await promise.value 46 | return { data } 47 | }, 48 | ({ data }) => data?.name 49 | ) 50 | 51 | expect(await renderToString(app)).toBe(`

a

`) 52 | }) 53 | 54 | it('works without await', async () => { 55 | const docRef = doc<{ name: string }>() 56 | await setDoc(docRef, { name: 'hello' }) 57 | const { app } = createMyApp( 58 | () => { 59 | const data = useDocument(docRef) 60 | return { data } 61 | }, 62 | ({ data }) => data?.name 63 | ) 64 | 65 | expect(await renderToString(app)).toBe(`

hello

`) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/database/utils.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '../shared' 2 | import type { DataSnapshot } from 'firebase/database' 3 | import type { _RefWithState, _Simplify } from '../shared' 4 | 5 | /** 6 | * Convert firebase Database snapshot of a ref **that exists** into a bindable data record. 7 | * 8 | * @param snapshot 9 | * @return 10 | */ 11 | export function createRecordFromDatabaseSnapshot( 12 | snapshot: DataSnapshot 13 | ): VueDatabaseDocumentData { 14 | if (!snapshot.exists()) return null 15 | 16 | const value: unknown = snapshot.val() 17 | return isObject(value) 18 | ? (Object.defineProperty(value, 'id', { 19 | // allow destructuring without interfering without using the `id` property 20 | value: snapshot.key, 21 | }) as VueDatabaseDocumentData) 22 | : { 23 | // if the value is a primitive we can just return a regular object, it's easier to debug 24 | // @ts-expect-error: $value doesn't exist 25 | $value: value, 26 | id: snapshot.key, 27 | } 28 | } 29 | 30 | export interface DatabaseSnapshotSerializer { 31 | (snapshot: DataSnapshot): VueDatabaseDocumentData 32 | } 33 | 34 | /** 35 | * Find the index for an object with given key. 36 | * 37 | * @param array 38 | * @param key 39 | * @return the index where the key was found 40 | */ 41 | export function indexForKey( 42 | array: NonNullable[], 43 | key: string | null | number 44 | ): number { 45 | for (let i = 0; i < array.length; i++) { 46 | if (array[i].id === key) return i 47 | } 48 | 49 | return -1 50 | } 51 | 52 | export interface _RefDatabase extends _RefWithState {} 53 | 54 | /** 55 | * Type used by default by the `serialize` option. 56 | */ 57 | export type VueDatabaseDocumentData = 58 | | null 59 | | (T & { 60 | /** 61 | * id of the document 62 | */ 63 | readonly id: string 64 | }) 65 | 66 | /** 67 | * Same as VueDatabaseDocumentData but for a query. 68 | */ 69 | export type VueDatabaseQueryData = Array< 70 | _Simplify>> 71 | > 72 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/sponsors.json: -------------------------------------------------------------------------------- 1 | { 2 | "platinum": [], 3 | "gold": [], 4 | "silver": [ 5 | { 6 | "alt": "Vue Mastery", 7 | "href": "https://www.vuemastery.com", 8 | "imgSrcDark": "https://posva-sponsors.pages.dev/logos/vuemastery-dark.png", 9 | "imgSrcLight": "https://posva-sponsors.pages.dev/logos/vuemastery-light.svg" 10 | }, 11 | { 12 | "alt": "Prefect", 13 | "href": "https://www.prefect.io/", 14 | "imgSrcDark": "https://posva-sponsors.pages.dev/logos/prefectlogo-dark.svg", 15 | "imgSrcLight": "https://posva-sponsors.pages.dev/logos/prefectlogo-light.svg" 16 | }, 17 | { 18 | "alt": "Route Optimizer and Route Planner Software", 19 | "href": "https://route4me.com", 20 | "imgSrcDark": "https://posva-sponsors.pages.dev/logos/route4me.png", 21 | "imgSrcLight": "https://posva-sponsors.pages.dev/logos/route4me.png" 22 | } 23 | ], 24 | "bronze": [ 25 | { 26 | "alt": "Storyblok", 27 | "href": "https://storyblok.com", 28 | "imgSrcDark": "https://posva-sponsors.pages.dev/logos/storyblok.png", 29 | "imgSrcLight": "https://posva-sponsors.pages.dev/logos/storyblok.png" 30 | }, 31 | { 32 | "alt": "Nuxt UI Pro Templates", 33 | "href": "https://ui.nuxt.com/pro", 34 | "imgSrcDark": "https://posva-sponsors.pages.dev/logos/nuxt-dark.svg", 35 | "imgSrcLight": "https://posva-sponsors.pages.dev/logos/nuxt-light.svg" 36 | }, 37 | { 38 | "alt": "Antony Konstantinidis", 39 | "href": "https://www.vuejs.de", 40 | "imgSrcDark": "https://avatars.githubusercontent.com/u/4183726?u=6b50a8ea16de29d2982f43c5640b1db9299ebcd1&v=4", 41 | "imgSrcLight": "https://avatars.githubusercontent.com/u/4183726?u=6b50a8ea16de29d2982f43c5640b1db9299ebcd1&v=4" 42 | }, 43 | { 44 | "alt": "Stanislas Ormières", 45 | "href": "https://stormier.ninja", 46 | "imgSrcDark": "https://avatars.githubusercontent.com/u/2486424?u=7b0c73ae5d090ce53bf59473094e9606fe082c59&v=4", 47 | "imgSrcLight": "https://avatars.githubusercontent.com/u/2486424?u=7b0c73ae5d090ce53bf59473094e9606fe082c59&v=4" 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E Bug report" 2 | description: Report an issue with VueFire 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thanks for taking the time to fill out this bug report! 8 | - type: input 9 | id: reproduction 10 | attributes: 11 | label: Reproduction 12 | description: "If possible, provide a boiled down editable reproduction based off [this vite template](https://github.com/posva/vuefire-repro-template) or [this nuxt template](https://github.com/posva/nuxt--vuefire-repro-template) with minimal dependencies (avoid testing, linting, and other unrelated dependencies). A failing unit test is even better! Otherwise provide as much information as possible to reproduce the problem. If no reproduction is provided and the information is not enough to reproduce the problem, we won't be able to give it a look **and the issue will be converted into a question and moved to discussions**." 13 | placeholder: Reproduction 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: steps 18 | attributes: 19 | label: Steps to reproduce the bug 20 | description: | 21 | 1. Click on ... 22 | 2. Check log 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: expected-behavior 27 | attributes: 28 | label: Expected behavior 29 | description: A clear and concise description of what you expected to happen. 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: actual-behavior 34 | attributes: 35 | label: Actual behavior 36 | description: 'A clear and concise description of what actually happens.' 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: other-info 41 | attributes: 42 | label: Additional information 43 | description: Add any other context about the problem here. 44 | - type: markdown 45 | attributes: 46 | value: | 47 | ## Before creating an issue make sure that: 48 | - This hasn't been [reported before](https://github.com/vuejs/vuefire/issues). 49 | - The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug **with no external dependencies (e.g. vuetify, pinia)** 50 | -------------------------------------------------------------------------------- /playground/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/HomeSponsorsGroup.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 55 | 56 | 100 | -------------------------------------------------------------------------------- /packages/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-vuefire", 3 | "description": "Nuxt.js module for VueFire", 4 | "version": "1.1.0", 5 | "license": "MIT", 6 | "type": "module", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/types.d.mts", 10 | "import": "./dist/module.mjs", 11 | "require": "./dist/module.cjs" 12 | }, 13 | "./templates/*": "./templates/*", 14 | "./runtime/*": { 15 | "types": "./dist/runtime/*", 16 | "import": "./dist/runtime/*" 17 | } 18 | }, 19 | "main": "./dist/module.cjs", 20 | "module": "./dist/module.mjs", 21 | "types": "./dist/types.d.mts", 22 | "files": [ 23 | "templates", 24 | "dist" 25 | ], 26 | "author": { 27 | "name": "Eduardo San Martin Morote", 28 | "email": "posva13@gmail.com" 29 | }, 30 | "funding": "https://github.com/sponsors/posva", 31 | "scripts": { 32 | "build": "nuxt-module-build build", 33 | "lint": "eslint src", 34 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . -l nuxt-vuefire -r 1", 35 | "test": "vitest", 36 | "dev": "nuxi dev playground", 37 | "dev:build": "nuxi build playground" 38 | }, 39 | "dependencies": { 40 | "@nuxt/kit": "^3.14.1592", 41 | "@posva/lru-cache": "^10.0.1", 42 | "lodash-es": "^4.17.21", 43 | "strip-json-comments": "^5.0.1" 44 | }, 45 | "peerDependencies": { 46 | "@firebase/app-types": ">=0.8.1", 47 | "firebase": "^9.0.0 || ^10.0.0 || ^11.1.0 || ^12.0.0", 48 | "firebase-admin": "^11.3.0 || ^12.2.0 || ^13.0.1", 49 | "firebase-functions": "^4.1.0 || ^5.0.0 || ^6.1.2", 50 | "vuefire": ">=3.2.2" 51 | }, 52 | "peerDependenciesMeta": { 53 | "@firebase/app-types": { 54 | "optional": true 55 | }, 56 | "firebase-admin": { 57 | "optional": true 58 | }, 59 | "firebase-functions": { 60 | "optional": true 61 | } 62 | }, 63 | "devDependencies": { 64 | "@firebase/app-types": "^0.9.3", 65 | "@nuxt/eslint-config": "^0.7.3", 66 | "@nuxt/module-builder": "^0.8.4", 67 | "@nuxt/schema": "^3.14.1592", 68 | "@nuxt/test-utils": "^3.15.1", 69 | "@types/lodash-es": "^4.17.12", 70 | "eslint": "^9.17.0", 71 | "firebase": "^11.1.0", 72 | "firebase-admin": "^13.0.1", 73 | "firebase-functions": "^6.1.2", 74 | "nuxt": "^3.14.1592", 75 | "vuefire": "workspace:*" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/nuxt/playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | 3 | export default defineNuxtConfig({ 4 | app: { 5 | pageTransition: false, 6 | layoutTransition: false, 7 | }, 8 | 9 | devtools: { 10 | enabled: true, 11 | }, 12 | 13 | nitro: { 14 | preset: 'firebase', 15 | firebase: { 16 | gen: 2, 17 | functions: { 18 | httpsOptions: { 19 | region: 'europe-west1', 20 | }, 21 | }, 22 | }, 23 | }, 24 | 25 | alias: { 26 | // import the dev version directly 27 | 'vuefire/server': fileURLToPath( 28 | new URL('../../../src/server/index.ts', import.meta.url) 29 | ), 30 | 'vuefire/*': fileURLToPath(new URL('../../../src/*', import.meta.url)), 31 | vuefire: fileURLToPath(new URL('../../../src/index.ts', import.meta.url)), 32 | }, 33 | 34 | modules: [ 35 | // 36 | [ 37 | '../src/module', 38 | { 39 | auth: { 40 | enabled: true, 41 | sessionCookie: true, 42 | // popupRedirectResolver: false, 43 | // persistence: ['indexedDBLocal'] 44 | }, 45 | appCheck: { 46 | // TODO: could automatically pick up a debug token defined as an env variable 47 | debug: process.env.NODE_ENV !== 'production', 48 | isTokenAutoRefreshEnabled: true, 49 | provider: 'ReCaptchaV3', 50 | key: '6LfJ0vgiAAAAAHheQE7GQVdG_c9m8xipBESx_SKI', 51 | }, 52 | 53 | emulators: { 54 | enabled: true, 55 | 56 | auth: { 57 | options: { 58 | // removes the HTML footer and console warning 59 | disableWarnings: process.env.NODE_ENV === 'development', 60 | }, 61 | }, 62 | }, 63 | 64 | config: { 65 | apiKey: 'AIzaSyAkUKe36TPWL2eZTshgk-Xl4bY_R5SB97U', 66 | authDomain: 'vue-fire-store.firebaseapp.com', 67 | databaseURL: 'https://vue-fire-store.firebaseio.com', 68 | projectId: 'vue-fire-store', 69 | storageBucket: 'vue-fire-store.appspot.com', 70 | messagingSenderId: '998674887640', 71 | appId: '1:998674887640:web:1e2bb2cc3e5eb2fc3478ad', 72 | measurementId: 'G-RL4BTWXKJ7', 73 | }, 74 | 75 | // admin: {}, 76 | }, 77 | ], 78 | ], 79 | 80 | compatibilityDate: '2024-12-18', 81 | }) 82 | -------------------------------------------------------------------------------- /playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp, nextTick } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import { 4 | VueFire, 5 | VueFireAppCheck, 6 | VueFireFirestoreOptionsAPI, 7 | VueFireDatabaseOptionsAPI, 8 | getCurrentUser, 9 | } from 'vuefire' 10 | import App from './App.vue' 11 | import { createFirebaseApp } from './firebase' 12 | import { createWebHistory, createRouter } from 'vue-router/auto' 13 | import { createStore } from 'vuex' 14 | import { ReCaptchaV3Provider } from 'firebase/app-check' 15 | import { VueFireAuthWithDependencies } from '../../src/auth' 16 | import { 17 | browserLocalPersistence, 18 | browserPopupRedirectResolver, 19 | debugErrorMap, 20 | indexedDBLocalPersistence, 21 | prodErrorMap, 22 | } from 'firebase/auth' 23 | import { routes } from 'vue-router/auto-routes' 24 | 25 | const router = createRouter({ 26 | history: createWebHistory(), 27 | routes, 28 | }) 29 | 30 | router.beforeEach(async () => { 31 | await getCurrentUser() 32 | }) 33 | 34 | const store = createStore({ 35 | // can't work with vuefire 36 | // strict: import.meta.env.DEV, 37 | state: () => ({ 38 | count: 0, 39 | todos: [], 40 | }), 41 | }) 42 | 43 | const app = createApp(App) 44 | app 45 | .directive('focus', { 46 | mounted: async (el) => { 47 | await nextTick() 48 | el.focus() 49 | }, 50 | }) 51 | .use(createPinia()) 52 | .use(VueFire, { 53 | firebaseApp: createFirebaseApp(), 54 | modules: [ 55 | VueFireAuthWithDependencies({ 56 | dependencies: { 57 | errorMap: 58 | process.env.NODE_ENV !== 'production' 59 | ? debugErrorMap 60 | : prodErrorMap, 61 | popupRedirectResolver: browserPopupRedirectResolver, 62 | persistence: [ 63 | indexedDBLocalPersistence, 64 | browserLocalPersistence, 65 | // browserSessionPersistence, 66 | // inMemoryPersistence, 67 | ], 68 | }, 69 | }), 70 | VueFireAppCheck({ 71 | debug: process.env.NODE_ENV !== 'production', 72 | isTokenAutoRefreshEnabled: true, 73 | provider: new ReCaptchaV3Provider( 74 | '6LfJ0vgiAAAAAHheQE7GQVdG_c9m8xipBESx_SKI' 75 | ), 76 | }), 77 | VueFireDatabaseOptionsAPI(), 78 | VueFireFirestoreOptionsAPI(), 79 | ], 80 | }) 81 | .use(store) 82 | .use(router) 83 | 84 | app.mount('#app') 85 | -------------------------------------------------------------------------------- /playground/src/pages/todos.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 88 | -------------------------------------------------------------------------------- /src/ssr/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { FirebaseApp } from 'firebase/app' 2 | import { Query as DatabaseQuery } from 'firebase/database' 3 | import { 4 | CollectionReference, 5 | DocumentReference, 6 | Query as FirestoreQuery, 7 | } from 'firebase/firestore' 8 | import { StorageReference } from 'firebase/storage' 9 | import { useFirebaseApp, _FirebaseAppInjectionKey } from '../app' 10 | import { noop } from '../shared' 11 | import { deferInitialValueSetup } from './initialState' 12 | 13 | export const appPendingPromises = new WeakMap< 14 | FirebaseApp, 15 | Map> 16 | >() 17 | 18 | export function clearPendingPromises(app: FirebaseApp) { 19 | appPendingPromises.delete(app) 20 | } 21 | 22 | export function addPendingPromise( 23 | promise: Promise, 24 | dataSource: 25 | | DocumentReference 26 | | FirestoreQuery 27 | | CollectionReference 28 | | DatabaseQuery 29 | | StorageReference, 30 | ssrKey?: string | null | undefined 31 | ) { 32 | const app = useFirebaseApp() 33 | if (!appPendingPromises.has(app)) { 34 | appPendingPromises.set(app, new Map()) 35 | } 36 | const pendingPromises = appPendingPromises.get(app)! 37 | 38 | // TODO: skip this outside of SSR 39 | const key = deferInitialValueSetup(dataSource, ssrKey, promise, app) 40 | if (key) { 41 | pendingPromises.set(key, promise) 42 | } else { 43 | // TODO: warn if in SSR context in other contexts than vite 44 | if (process.env.NODE_ENV !== 'production' /* && import.meta.env?.SSR */) { 45 | console.warn('[VueFire SSR]: Could not get the path of the data source') 46 | } 47 | } 48 | 49 | return key ? () => pendingPromises.delete(key!) : noop 50 | } 51 | 52 | /** 53 | * Allows awaiting for all pending data sources. Useful to wait for SSR 54 | * 55 | * @param app - the firebase app 56 | * @returns - a Promise that resolves with an array of all the resolved pending promises 57 | */ 58 | export function usePendingPromises(app?: FirebaseApp) { 59 | app = app || useFirebaseApp() 60 | const pendingPromises = appPendingPromises.get(app) 61 | const p = pendingPromises 62 | ? Promise.all( 63 | Array.from(pendingPromises).map(([key, promise]) => 64 | promise.then((data) => [key, data] as const) 65 | ) 66 | ) 67 | : Promise.resolve([]) 68 | 69 | // consume the promises 70 | appPendingPromises.delete(app) 71 | 72 | return p 73 | } 74 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/app/plugin.server.ts: -------------------------------------------------------------------------------- 1 | import { type FirebaseApp, initializeApp } from 'firebase/app' 2 | import { type User } from 'firebase/auth' 3 | import { type DecodedIdToken } from 'firebase-admin/auth' 4 | import { logger } from '../logging' 5 | import { DECODED_ID_TOKEN_SYMBOL } from '../constants' 6 | import { appCache } from './lru-cache' 7 | import { defineNuxtPlugin, useRuntimeConfig } from '#imports' 8 | 9 | /** 10 | * Initializes the app and provides it to others. 11 | */ 12 | export default defineNuxtPlugin((nuxtApp) => { 13 | const runtimeConfig = useRuntimeConfig() 14 | 15 | const decodedToken = nuxtApp[ 16 | // we cannot use a symbol to index 17 | DECODED_ID_TOKEN_SYMBOL as unknown as string 18 | ] as DecodedIdToken | null | undefined 19 | 20 | const uid = decodedToken?.uid 21 | 22 | let firebaseApp: FirebaseApp | undefined 23 | 24 | // logger.debug('initializing app with', runtimeConfig.public.vuefire!.config!) 25 | if (uid) { 26 | firebaseApp = appCache.get(uid) 27 | if (!firebaseApp) { 28 | const randomId = Math.random().toString(36).slice(2) 29 | // TODO: do we need a randomId? 30 | const appName = `auth:${uid}:${randomId}` 31 | 32 | logger.debug('👤 creating new app', appName) 33 | 34 | firebaseApp = initializeApp( 35 | runtimeConfig.public.vuefire!.config!, 36 | appName 37 | ) 38 | appCache.set(uid, firebaseApp) 39 | // console.time('token') 40 | } else { 41 | logger.debug('👤 reusing authenticated app', firebaseApp.name) 42 | } 43 | } else { 44 | firebaseApp = appCache.get('') 45 | // TODO: is this safe? should we create a new one every time 46 | if (!firebaseApp) { 47 | firebaseApp = initializeApp(runtimeConfig.public.vuefire!.config!) 48 | appCache.set('', firebaseApp) 49 | } 50 | // anonymous session, just create a new app 51 | logger.debug('🥸 anonymous session') 52 | } 53 | 54 | return { 55 | provide: { 56 | firebaseApp: firebaseApp satisfies FirebaseApp, 57 | }, 58 | } 59 | }) 60 | 61 | // TODO: should the type extensions be added in a different way to the module? 62 | declare module 'h3' { 63 | interface H3EventContext { 64 | /** 65 | * Firebase Admin User Record. `null` if the user is not logged in or their token is no longer valid and requires a 66 | * refresh. 67 | * @experimental This API is experimental and may change in future releases. 68 | */ 69 | user: User | null 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/nuxt/playground/pages/firestore-useDocument.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 99 | -------------------------------------------------------------------------------- /tests/storage/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { it, describe, expect } from 'vitest' 3 | import { uploadString } from 'firebase/storage' 4 | import { defineComponent, nextTick, ref } from 'vue' 5 | import { 6 | useFirebaseStorage, 7 | useStorageFileMetadata, 8 | useStorageFile, 9 | useStorageFileUrl, 10 | } from '../../src' 11 | import { setupStorageRefs } from '../utils' 12 | 13 | // FIXME: receiving empty errors from the firebase emulators when doing `uploadString()` 14 | describe.skip('Storage', () => { 15 | const { storageRef } = setupStorageRefs() 16 | 17 | it('generates a URL', async () => { 18 | const objectRef = storageRef('my-text') 19 | await uploadString(objectRef, 'test', 'raw') 20 | const wrapper = mount( 21 | defineComponent({ 22 | template: 'no', 23 | setup() { 24 | const { url, promise } = useStorageFileUrl(objectRef) 25 | 26 | return { url, promise } 27 | }, 28 | }) 29 | ) 30 | 31 | await wrapper.vm.promise 32 | 33 | expect(wrapper.vm.url).toBeTypeOf('string') 34 | expect(wrapper.vm.url).toMatch(/my-url\.jpg/) 35 | }) 36 | 37 | it('generates the metadata', async () => { 38 | const objectRef = storageRef('my-url.jpg') 39 | await uploadString(objectRef, 'test', 'raw') 40 | const wrapper = mount( 41 | defineComponent({ 42 | template: 'no', 43 | setup() { 44 | const { metadata, promise } = useStorageFileMetadata(objectRef) 45 | 46 | return { metadata, promise } 47 | }, 48 | }) 49 | ) 50 | 51 | await wrapper.vm.promise 52 | 53 | expect(wrapper.vm.metadata).toBeTypeOf('object') 54 | expect(wrapper.vm.metadata).toMatchObject({ 55 | name: 'my-url.jpg', 56 | }) 57 | }) 58 | 59 | it('can create upload tasks', async () => { 60 | const objectRef = storageRef('my-url.jpg') 61 | await uploadString(objectRef, 'test', 'raw') 62 | const wrapper = mount( 63 | defineComponent({ 64 | template: 'no', 65 | setup() { 66 | const { uploadTask, upload, uploadProgress } = 67 | useStorageFile(objectRef) 68 | 69 | return { uploadTask, upload, uploadProgress } 70 | }, 71 | }) 72 | ) 73 | 74 | await nextTick() 75 | 76 | expect(wrapper.vm.uploadTask).toBeFalsy() 77 | expect(wrapper.vm.uploadProgress).toBe(null) 78 | 79 | // add a task 80 | const p = wrapper.vm.upload(new Uint8Array([0x48, 0x65])) 81 | expect(wrapper.vm.uploadTask).toBeTruthy() 82 | expect(wrapper.vm.uploadProgress).toBeTypeOf('number') 83 | 84 | await p 85 | expect(wrapper.vm.uploadTask).toBeFalsy() 86 | expect(wrapper.vm.uploadProgress).toBe(1) // 100% 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/auth/plugin-authenticate-user.server.ts: -------------------------------------------------------------------------------- 1 | import { signInWithCustomToken, getAuth, type Auth } from 'firebase/auth' 2 | import { 3 | type DecodedIdToken, 4 | getAuth as getAdminAuth, 5 | } from 'firebase-admin/auth' 6 | import type { FirebaseApp } from 'firebase/app' 7 | import type { App as AdminApp } from 'firebase-admin/app' 8 | import { VueFireAuthServer } from 'vuefire/server' 9 | import { DECODED_ID_TOKEN_SYMBOL, UserSymbol } from '../constants' 10 | import { logger } from '../logging' 11 | import { defineNuxtPlugin, useRequestEvent } from '#imports' 12 | 13 | /** 14 | * Setups the auth state based on the cookie. 15 | */ 16 | export default defineNuxtPlugin(async (nuxtApp) => { 17 | const event = useRequestEvent()! 18 | const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp 19 | const firebaseAdminApp = nuxtApp.$firebaseAdminApp as AdminApp 20 | const adminAuth = getAdminAuth(firebaseAdminApp) 21 | const auth = nuxtApp.$firebaseAuth as Auth 22 | 23 | const decodedToken = nuxtApp[ 24 | // we cannot use a symbol to index 25 | DECODED_ID_TOKEN_SYMBOL as unknown as string 26 | ] as DecodedIdToken | null | undefined 27 | 28 | const uid = decodedToken?.uid 29 | 30 | // this is also undefined if the user hasn't enabled the session cookie option 31 | if (uid) { 32 | // reauthenticate if the user is not the same (e.g. invalidated) 33 | if (auth.currentUser?.uid !== uid) { 34 | const customToken = await adminAuth 35 | .createCustomToken(uid) 36 | .catch((err) => { 37 | logger.error('Error creating custom token', err) 38 | return null 39 | }) 40 | // console.timeLog('token', `got token for ${user.uid}`) 41 | if (customToken) { 42 | logger.debug('Signing in with custom token') 43 | // TODO: allow user to handle error? 44 | await signInWithCustomToken(auth, customToken) 45 | // console.timeLog('token', `signed in with token for ${user.uid}`) 46 | // console.timeEnd('token') 47 | // TODO: token expiration (1h) 48 | } 49 | } 50 | 51 | // inject the current user into the app 52 | const user = auth.currentUser 53 | nuxtApp[ 54 | // we cannot use a symbol to index 55 | UserSymbol as unknown as string 56 | ] = user 57 | // expose the user to requests 58 | // FIXME: should be doable in nitro server routes too 59 | // use addServerPlugin 60 | event.context.user = user 61 | // Hydrates the user 62 | nuxtApp.payload.vuefireUser = user?.toJSON() 63 | } 64 | 65 | // logger.debug('setting up user for app', firebaseApp.name, user?.uid) 66 | 67 | // provide the user data to the app during ssr 68 | VueFireAuthServer(firebaseApp, nuxtApp.vueApp, auth.currentUser) 69 | }) 70 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref, MaybeRefOrGetter } from 'vue-demi' 2 | import { DatabaseReference, getDatabase, Query } from 'firebase/database' 3 | import { _Nullable, _RefWithState } from '../shared' 4 | import { 5 | VueDatabaseDocumentData, 6 | VueDatabaseQueryData, 7 | _RefDatabase, 8 | } from './utils' 9 | import { useFirebaseApp } from '../app' 10 | import { UseDatabaseRefOptions, _useDatabaseRef } from './useDatabaseRef' 11 | 12 | export { globalDatabaseOptions } from './bind' 13 | export type { UseDatabaseRefOptions } 14 | 15 | export type UseListOptions = UseDatabaseRefOptions 16 | 17 | /** 18 | * Creates a reactive variable connected to the database as an array. Each element in the array will contain an `id` 19 | * property. Note that if you override the `serialize` option, it should **also set an `id` property** in order for this 20 | * to work. 21 | * 22 | * @param reference - Reference or query to the database 23 | * @param options - optional options 24 | */ 25 | export function useDatabaseList( 26 | reference: MaybeRefOrGetter<_Nullable>, 27 | options?: UseListOptions 28 | ): _RefDatabase> { 29 | const data = ref([]) as Ref 30 | return _useDatabaseRef( 31 | reference, 32 | { 33 | target: data, 34 | ...options, 35 | }, 36 | true 37 | ) as _RefDatabase> 38 | } 39 | 40 | /** 41 | * @deprecated use `useDatabaseList()` instead 42 | */ 43 | export const useList = useDatabaseList 44 | 45 | export type UseObjectOptions = UseDatabaseRefOptions 46 | 47 | /** 48 | * Creates a reactive variable connected to the database as an object. If the reference is a primitive, it will be 49 | * converted to an object containing a `$value` property with the primitive value and an `id` property with the 50 | * reference's key. 51 | * 52 | * @param reference - Reference or query to the database 53 | * @param options - optional options 54 | */ 55 | export function useDatabaseObject( 56 | reference: MaybeRefOrGetter<_Nullable>, 57 | options?: UseObjectOptions 58 | ): _RefDatabase | undefined> { 59 | const data = ref() 60 | return _useDatabaseRef(reference, { 61 | target: data, 62 | ...options, 63 | }) as _RefDatabase> 64 | } 65 | 66 | /** 67 | * @deprecated use `useDatabaseObject()` instead 68 | */ 69 | export const useObject = useDatabaseObject 70 | 71 | /** 72 | * Retrieves the Database instance. 73 | * 74 | * @param name - name of the application 75 | * @returns the Database instance 76 | */ 77 | export function useDatabase(name?: string) { 78 | return getDatabase(useFirebaseApp(name)) 79 | } 80 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/auth/api.session-verification.ts: -------------------------------------------------------------------------------- 1 | import { getAuth as getAdminAuth } from 'firebase-admin/auth' 2 | import { 3 | readBody, 4 | setCookie, 5 | assertMethod, 6 | defineEventHandler, 7 | deleteCookie, 8 | setResponseStatus, 9 | } from 'h3' 10 | import { ensureAdminApp } from 'vuefire/server' 11 | import { logger } from '../logging' 12 | import { useRuntimeConfig } from '#imports' 13 | 14 | /** 15 | * Setups an API endpoint to be used by the client to mint a cookie based auth session. 16 | */ 17 | export default defineEventHandler(async (event) => { 18 | assertMethod(event, 'POST') 19 | const { token } = await readBody<{ token?: string }>(event) 20 | const runtimeConfig = useRuntimeConfig() 21 | 22 | const adminApp = ensureAdminApp( 23 | { 24 | // NOTE: ensured by the module 25 | projectId: runtimeConfig.public.vuefire!.config!.projectId, 26 | ...runtimeConfig.vuefire?.admin?.options, 27 | }, 28 | 'session-verification' 29 | ) 30 | const adminAuth = getAdminAuth(adminApp) 31 | 32 | logger.debug(token ? 'Verifying the token' : 'Deleting the session cookie') 33 | const verifiedIdToken = token ? await adminAuth.verifyIdToken(token) : null 34 | 35 | if (verifiedIdToken) { 36 | if (new Date().getTime() / 1_000 - verifiedIdToken.iat > ID_TOKEN_MAX_AGE) { 37 | setResponseStatus(event, 301) 38 | } else { 39 | const cookie = await adminAuth 40 | .createSessionCookie(token!, { expiresIn: AUTH_COOKIE_MAX_AGE }) 41 | .catch((e: any) => { 42 | logger.error('Error minting the cookie', e) 43 | }) 44 | if (cookie) { 45 | // logger.debug(`minted a session cookie for user ${verifiedIdToken.uid}`) 46 | setCookie(event, AUTH_COOKIE_NAME, cookie, { 47 | maxAge: AUTH_COOKIE_MAX_AGE, 48 | secure: true, 49 | httpOnly: true, 50 | path: '/', 51 | sameSite: 'lax', 52 | // add user overrides 53 | ...(typeof runtimeConfig.vuefire?.auth?.sessionCookie === 'object' 54 | ? runtimeConfig.vuefire?.auth?.sessionCookie 55 | : {}), 56 | }) 57 | setResponseStatus(event, 201) 58 | return '' 59 | } else { 60 | setResponseStatus(event, 401) 61 | return '' 62 | } 63 | } 64 | } else { 65 | // logger.debug('deleting the session cookie') 66 | deleteCookie(event, AUTH_COOKIE_NAME) 67 | setResponseStatus(event, 204) 68 | } 69 | 70 | // empty response 71 | return '' 72 | }) 73 | 74 | // these must be within this file because the handler gets inlined in dev mode 75 | const ID_TOKEN_MAX_AGE = 5 * 60 76 | const AUTH_COOKIE_MAX_AGE = 60 * 60 * 24 * 5 * 1_000 77 | // MUST be named session to be kept 78 | // https://firebase.google.com/docs/hosting/manage-cache#using_cookies 79 | const AUTH_COOKIE_NAME = '__session' 80 | -------------------------------------------------------------------------------- /docs/nuxt/environment-variables.md: -------------------------------------------------------------------------------- 1 | # Environment Variables 2 | 3 | Nuxt VueFire automatically picks up a few environment variables to configure Firebase from your `.env` file. These usually take precedence over other options defined in `nuxt.config.ts`. They usually try to support the existing Firebase environment variables better. 4 | 5 | Since VueFire config is treated as _Public Runtime Config_, it can also be overridden with [env variables by following the Nuxt convention](https://nuxt.com/docs/guide/going-further/runtime-config#environment-variables). For example, an environment variable named `NUXT_PUBLIC_VUEFIRE_CONFIG_API_KEY=xyz` will override the `config.apiKey`. 6 | Note you still need to provide empty string values to each `config` property that is defined this way. 7 | 8 | ## Admin SDK 9 | 10 | During development, if you are doing SSR, you must provide the `GOOGLE_APPLICATION_CREDENTIALS` environment variable with the path to the service account file. This is usually a JSON file you can download from the Firebase Console > Project Settings > Service Accounts > Generate new private key. 11 | 12 | ``` 13 | GOOGLE_APPLICATION_CREDENTIALS=service-account.json 14 | ``` 15 | 16 | Ensure **to exclude the `.env` and `service-account.json` files from your version control system**. This variable will be automatically set on Firebase and Google Cloud deployments. 17 | 18 | ::: tip 19 | 20 | When deploying to something other than Firebase or Google Cloud, the `GOOGLE_APPLICATION_CREDENTIALS` environment variable must be set manually. Instead of setting it to the path of the service account file, you can set it to the content of the file itself. Note it will have to fit in **one single line**: 21 | 22 | ``` 23 | GOOGLE_APPLICATION_CREDENTIALS='{"type":"service_account","project_id":"...","private_key_id":"...","private_key":"-----BEGIN PRIVATE KEY-----\n[redacted]\n-----END PRIVATE KEY-----\n"}' 24 | ``` 25 | 26 | ::: 27 | 28 | ## AppCheck 29 | 30 | If you are using AppCheck, you can specify the `FIREBASE_APPCHECK_DEBUG_TOKEN` environment variable to use a debug token in development. This is useful in **protected** CI environments or if you run multiple Firebase projects on your machine and don't want to rely on the local generation of the debug token. 31 | 32 | ``` 33 | FIREBASE_APPCHECK_DEBUG_TOKEN=********-****-****-****-************ 34 | ``` 35 | 36 | These can be generated on the Firebase Console > AppCheck > Apps > Manage Debug Tokens. 37 | 38 | This variable will not be used in production unless `debug: true` is passed during a build or generate command. This allows you to still test locally using a debug token without worrying about accidentally deploying it to production. 39 | 40 | ## Debugging utilities 41 | 42 | You can activate these while developing or building locally by setting them before running the command: 43 | 44 | ```bash 45 | VUEFIRE_APPCHECK_DEBUG=true VUEFIRE_EMULATORS=true pnpm run build 46 | ``` 47 | 48 | - `VUEFIRE_APPCHECK_DEBUG=true` will activate the AppCheck debug even in production. 49 | - `VUEFIRE_EMULATORS=true` will activate the Firebase Emulators even in production. 50 | -------------------------------------------------------------------------------- /.github/commit-convention.md: -------------------------------------------------------------------------------- 1 | ## Git Commit Message Convention 2 | 3 | > This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). 4 | 5 | #### TL;DR: 6 | 7 | Messages must be matched by the following regex: 8 | 9 | ```text 10 | /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip)(\(.+\))?: .{1,50}/ 11 | ``` 12 | 13 | #### Examples 14 | 15 | Appears under "Features" header, `link` subheader: 16 | 17 | ``` 18 | feat(link): add `force` option 19 | ``` 20 | 21 | Appears under "Bug Fixes" header, `view` subheader, with a link to issue #28: 22 | 23 | ``` 24 | fix(view): handle keep-alive with aborted navigations 25 | 26 | close #28 27 | ``` 28 | 29 | Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation: 30 | 31 | ``` 32 | perf: improve guard extraction 33 | 34 | BREAKING CHANGE: The 'beforeRouteEnter' option has been removed. 35 | ``` 36 | 37 | The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header. 38 | 39 | ``` 40 | revert: feat(compiler): add 'comments' option 41 | 42 | This reverts commit 667ecc1654a317a13331b17617d973392f415f02. 43 | ``` 44 | 45 | ### Full Message Format 46 | 47 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: 48 | 49 | ``` 50 | (): 51 | 52 | 53 | 54 |