├── 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 |
2 | basic
3 |
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 |
5 |
6 |
Select one of the links above
7 |
8 |
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 |
4 |
5 | << Home
6 |
7 |
8 |
9 |
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 |
2 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/playground/src/pages/pinia-store.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
count: {{ counter.count }}
10 |
increment
11 |
12 |
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 |
12 | List with nested refs
13 | {{ nestedList }}
14 |
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 |
16 | Hello
17 |
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 |
14 |
15 |
16 | {{
17 | href
18 | }}
19 |
20 |
21 |
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 |
19 |
20 |
App Check
21 |
Token: {{ token }}
22 |
23 |
24 |
--------------------------------------------------------------------------------
/playground/src/pages/converter-with-number.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 | numbers:
20 |
23 |
24 |
--------------------------------------------------------------------------------
/packages/nuxt/playground/pages/app-check.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
App Check
21 |
Token: {{ token }}
22 |
23 |
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 |
2 |
3 |
7 |
8 |
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 |
17 | collection with refs
18 |
19 |
22 |
23 | Original data
24 |
25 |
28 |
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 |
30 |
31 |
count: {{ count }}
32 |
increment
33 |
34 |
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 |
29 | config:
30 | finished: {{ isDoneFetching }}
31 | All finished: {{ isAllDoneFetching }}
32 | {{ config }}
33 |
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 |
18 |
19 |
20 |
21 |
22 |
23 | Home
24 |
25 |
26 |
27 |
28 | {{ route.label }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
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 |
26 |
27 |
Log in in the authentication page to test this.
28 |
29 | Loading...
30 |
31 |
32 | Secret Data for user {{ user.displayName }} ({{ user.uid }})
33 | {{ secret }}
34 |
35 |
You have no secret. Do you want to create one?
36 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/components/HomeSponsors.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
27 |
39 |
40 |
53 |
--------------------------------------------------------------------------------
/packages/nuxt/playground/pages/bug-playground.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 |
45 | Toggle the nested `ref` every
46 | 3s.
47 |
48 |
Actual: {{ thing }}
49 |
Actual deep: {{ thingDeep }}
50 |
51 |
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 |
48 |
49 |
Toggle todos
50 |
51 |
55 |
64 |
65 |
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 |
55 | Toggle todos
56 |
60 |
69 |
70 |
--------------------------------------------------------------------------------
/playground/src/demos/TodoList/components/TodoItem.vue:
--------------------------------------------------------------------------------
1 |
45 |
46 |
47 |
48 |
59 |
60 |
61 | {{ todo.text }}
66 |
67 | Delete
68 |
69 |
70 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/components/AsideSponsors.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 | Mastering Pinia
11 |
12 |
13 |
14 |
15 |
16 |
75 |
--------------------------------------------------------------------------------
/packages/nuxt/playground/components/TodoItem.vue:
--------------------------------------------------------------------------------
1 |
50 |
51 |
52 |
53 |
66 |
67 |
68 | {{ todo.text }}
72 |
73 |
74 | Delete
75 |
76 |
77 |
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 |
27 | {{ name }} Sponsors
28 |
29 |
30 |
53 |
54 |
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 |
73 | Toggle todos
74 |
78 |
87 |
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 |
78 |
79 |
config:
80 |
finished: {{ isDoneFetching }}
81 |
All finished: {{ isAllDoneFetching }}
82 |
Revive check:
83 |
84 |
85 | TimeStamp: {{ config?.time }}. toMillis:
86 | {{ (config?.time as Timestamp).toMillis() }}
87 |
88 |
89 | GeoPoint: {{ config?.loc }}. isEqual:
90 | {{ (config?.loc as GeoPoint).isEqual(new GeoPoint(0, 0)) }}
91 |
92 |
93 |
94 |
95 |
96 |
{{ config }}
97 |
98 |
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 |
55 | ```
56 |
57 | The **header** is mandatory and the **scope** of the header is optional.
58 |
59 | ### Revert
60 |
61 | If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body, it should say: `This reverts commit .`, where the hash is the SHA of the commit being reverted.
62 |
63 | ### Type
64 |
65 | If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However, if there is any [BREAKING CHANGE](#footer), the commit will always appear in the changelog.
66 |
67 | Other prefixes are up to your discretion. Suggested prefixes are `docs`, `chore`, `style`, `refactor`, and `test` for non-changelog related tasks.
68 |
69 | ### Scope
70 |
71 | The scope could be anything specifying the place of the commit change. For example `core`, `compiler`, `ssr`, `v-model`, `transition` etc...
72 |
73 | ### Subject
74 |
75 | The subject contains a succinct description of the change:
76 |
77 | - use the imperative, present tense: "change" not "changed" nor "changes"
78 | - don't capitalize the first letter
79 | - no dot (.) at the end
80 |
81 | ### Body
82 |
83 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
84 | The body should include the motivation for the change and contrast this with previous behavior.
85 |
86 | ### Footer
87 |
88 | The footer should contain any information about **Breaking Changes** and is also the place to
89 | reference GitHub issues that this commit **Closes**.
90 |
91 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
92 |
--------------------------------------------------------------------------------
/src/server/app-check.ts:
--------------------------------------------------------------------------------
1 | import type { App as FirebaseAdminApp } from 'firebase-admin/app'
2 | import { getAppCheck as getAdminAppCheck } from 'firebase-admin/app-check'
3 | import type { FirebaseApp } from 'firebase/app'
4 | import {
5 | CustomProvider,
6 | initializeAppCheck,
7 | type AppCheckToken,
8 | } from 'firebase/app-check'
9 | import { type App, ref } from 'vue-demi'
10 | import { AppCheckMap, AppCheckTokenInjectSymbol } from '../app-check'
11 | import { getGlobalScope } from '../globals'
12 | import { logger } from './logging'
13 |
14 | /**
15 | * Adds AppCheck using the Firebase Admin SDK. This is necessary on the Server if you have configured AppCheck on the
16 | * client.
17 | *
18 | * @param adminApp - firebase-admin app
19 | * @param firebaseApp - firebase/app initializeApp()
20 | * @param param2 options
21 | */
22 | export function VueFireAppCheckServer(
23 | app: App,
24 | adminApp: FirebaseAdminApp,
25 | firebaseApp: FirebaseApp,
26 | {
27 | // default to 1 week
28 | ttlMillis = 604_800_000,
29 | }: {
30 | ttlMillis?: number
31 | } = {}
32 | ) {
33 | // Inject an empty token ref so the same code works on the client and server
34 | const providedToken = getGlobalScope(firebaseApp, app).run(() =>
35 | ref()
36 | )!
37 | app.provide(AppCheckTokenInjectSymbol, providedToken)
38 |
39 | // FIXME: do we need to avoid creating the appcheck instance on the server?
40 | if (AppCheckMap.has(firebaseApp)) {
41 | logger.debug(
42 | 'AppCheck already initialized, skipping server initialization.'
43 | )
44 | return
45 | }
46 |
47 | logger.debug(
48 | `Initializing AppCheck on the server for app "${firebaseApp.name}".`
49 | )
50 |
51 | let currentToken: AppCheckToken | undefined
52 | const appCheck = initializeAppCheck(firebaseApp, {
53 | provider: new CustomProvider({
54 | getToken: () => {
55 | // FIXME: without this there is an infinite loop
56 | if (currentToken) {
57 | logger.debug('Using cached AppCheck token on server.')
58 | return Promise.resolve(currentToken)
59 | }
60 | logger.debug('Getting Admin AppCheck')
61 | const adminAppCheck = getAdminAppCheck(adminApp)
62 | // NOTE: appId is checked on the module
63 | logger.debug(`Creating token for app ${firebaseApp.options.appId!}.`)
64 |
65 | return adminAppCheck
66 | .createToken(firebaseApp.options.appId!, { ttlMillis })
67 | .then(({ token, ttlMillis: expireTimeMillis }) => {
68 | logger.debug(
69 | `Got AppCheck token from the server, expires in ${expireTimeMillis}ms.`
70 | )
71 | // expire the token after the ttl
72 | // TODO: verify this is okay
73 | setTimeout(() => {
74 | currentToken = undefined
75 | }, expireTimeMillis)
76 |
77 | currentToken = {
78 | token,
79 | expireTimeMillis,
80 | }
81 | return currentToken
82 | })
83 | .catch((reason) => {
84 | logger.error(
85 | 'Error getting AppCheck token from the server:',
86 | reason
87 | )
88 | throw reason
89 | })
90 | },
91 | }),
92 | isTokenAutoRefreshEnabled: false,
93 | })
94 | AppCheckMap.set(firebaseApp, appCheck)
95 | }
96 |
--------------------------------------------------------------------------------
/playground/src/pages/storage.vue:
--------------------------------------------------------------------------------
1 |
54 |
55 |
56 |
84 |
85 |
86 |
87 | Error: {{ error.name }} ({{ error.code }})
88 |
89 | {{ error.message }}
90 |
91 |
92 |
{{ error.stack }}
93 |
{{ error.customData }}
94 |
95 |
96 |
{{ progress * 100 }}%
97 |
98 |
99 | Success: {{ url }}
100 |
101 |
102 |
103 | File: {{ snapshot.ref.name }}
104 | {{ metadata }}
105 |
106 | Select a new file to simply update it
107 | Clear the input to delete the file.
108 |
109 | Delete the picture
110 |
111 |
112 |
--------------------------------------------------------------------------------
/packages/nuxt/playground/pages/storage.vue:
--------------------------------------------------------------------------------
1 |
54 |
55 |
56 |
84 |
85 |
86 |
87 | Error: {{ error.name }} ({{ error.code }})
88 |
89 | {{ error.message }}
90 |
91 |
92 |
{{ error.stack }}
93 |
{{ error.customData }}
94 |
95 |
96 |
{{ progress * 100 }}%
97 |
98 |
99 | Success: {{ url }}
100 |
101 |
102 |
103 | File: {{ snapshot.ref.name }}
104 | {{ metadata }}
105 |
106 | Select a new file to simply update it
107 | Clear the input to delete the file.
108 |
109 | Delete the picture
110 |
111 |
112 |
--------------------------------------------------------------------------------
/src/app-check/index.ts:
--------------------------------------------------------------------------------
1 | import type { FirebaseApp } from 'firebase/app'
2 | import {
3 | initializeAppCheck,
4 | onTokenChanged,
5 | AppCheckOptions,
6 | AppCheck,
7 | getToken,
8 | AppCheckTokenResult,
9 | } from 'firebase/app-check'
10 | import { App, inject, InjectionKey, Ref, ref } from 'vue-demi'
11 | import { useFirebaseApp } from '../app'
12 | import { getGlobalScope } from '../globals'
13 | import { isClient } from '../shared'
14 |
15 | export const AppCheckTokenInjectSymbol: InjectionKey[> =
16 | Symbol('app-check-token')
17 |
18 | /**
19 | * The current app-check token as a `Ref`. Note this ref is always undefined on the server.
20 | */
21 | export function useAppCheckToken() {
22 | return inject(AppCheckTokenInjectSymbol)!
23 | }
24 |
25 | export interface VueFireAppCheckOptions extends AppCheckOptions {
26 | /**
27 | * Setups the debug token global. See https://firebase.google.com/docs/app-check/web/debug-provider. Note you should
28 | * set to false in production (or not set it at all). It can be set to a string to force a specific debug token.
29 | */
30 | debug?: boolean | string
31 | }
32 |
33 | /**
34 | * VueFire AppCheck Module to be added to the `VueFire` Vue plugin options. This module **is client only** and shouldn't be added on server.
35 | *
36 | * @example
37 | *
38 | * ```ts
39 | * import { createApp } from 'vue'
40 | * import { VueFire, VueFireAppCheck } from 'vuefire'
41 | *
42 | * const app = createApp(App)
43 | * app.use(VueFire, {
44 | * modules: [VueFireAppCheck()],
45 | * })
46 | * ```
47 | */
48 | export function VueFireAppCheck(options: VueFireAppCheckOptions) {
49 | return (firebaseApp: FirebaseApp, app: App) => {
50 | // AppCheck requires special treatment on the server
51 | if (!isClient) return
52 |
53 | // provide this even on the server for simplicity of usage
54 | const token = getGlobalScope(firebaseApp, app).run(() => ref]())!
55 | app.provide(AppCheckTokenInjectSymbol, token)
56 |
57 | if (options.debug) {
58 | // @ts-expect-error: local override
59 | self.FIREBASE_APPCHECK_DEBUG_TOKEN = options.debug
60 | }
61 |
62 | const appCheck = initializeAppCheck(firebaseApp, options)
63 | onTokenChanged(appCheck, (newToken) => {
64 | token.value = newToken.token
65 | })
66 | AppCheckMap.set(firebaseApp, appCheck)
67 | }
68 | }
69 |
70 | // TODO: remove this as getAppCheck() already kinda does this
71 | /**
72 | * To retrieve the current app check
73 | * @internal
74 | */
75 | export const AppCheckMap = new WeakMap()
76 |
77 | /**
78 | * Retrieves the Firebase App Check instance.
79 | *
80 | * @param name - name of the application
81 | */
82 | export function useAppCheck(name?: string) {
83 | return AppCheckMap.get(useFirebaseApp(name))!
84 | }
85 |
86 | /**
87 | * Retrieves the current app check token. If there is no app check token, it will return an empty string token.
88 | *
89 | * @param name - name of the application
90 | * @param forceRefresh - force a refresh of the token
91 | */
92 | export function getAppCheckToken(
93 | name?: string,
94 | forceRefresh?: boolean
95 | ): Promise {
96 | const appCheck = useAppCheck(name)
97 | return appCheck
98 | ? getToken(appCheck, forceRefresh)
99 | : Promise.resolve({ token: '' })
100 | }
101 |
--------------------------------------------------------------------------------
/examples/rtdb.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | VueFire Todo App Demo
6 |
7 |
8 |
9 |
10 |
11 |
25 |
26 |
42 |
43 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/scripts/typedoc-markdown.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import fs from 'node:fs/promises'
3 | import path from 'node:path'
4 | import { Application, TSConfigReader, PageEvent } from 'typedoc'
5 |
6 | const __dirname = path.dirname(new URL(import.meta.url).pathname)
7 |
8 | const DEFAULT_OPTIONS = {
9 | // disableOutputCheck: true,
10 | cleanOutputDir: true,
11 | excludeInternal: true,
12 | readme: 'none',
13 | out: path.resolve(__dirname, '../docs/api'),
14 | entryDocument: 'index.md',
15 | hideBreadcrumbs: false,
16 | hideInPageTOC: true,
17 | preserveAnchorCasing: true,
18 | }
19 |
20 | /**
21 | *
22 | * @param {Partial} config
23 | */
24 | export async function createTypeDocApp(config = {}) {
25 | const options = {
26 | ...DEFAULT_OPTIONS,
27 | ...config,
28 | }
29 |
30 | const app = await Application.bootstrapWithPlugins(options)
31 |
32 | // If you want TypeDoc to load tsconfig.json / typedoc.json files
33 | app.options.addReader(new TSConfigReader())
34 |
35 | app.renderer.on(
36 | PageEvent.END,
37 | /**
38 | *
39 | * @param {import('typedoc').PageEvent} page
40 | */
41 | (page) => {
42 | if (!page.contents) {
43 | return
44 | }
45 | page.contents = prependYAML(page.contents, {
46 | // TODO: figure out a way to point to the source files?
47 | editLink: false,
48 | })
49 | }
50 | )
51 |
52 | async function serve() {
53 | app.convertAndWatch(handleProject)
54 | }
55 |
56 | async function build() {
57 | if (
58 | (await exists(options.out)) &&
59 | (await fs.stat(options.out)).isDirectory()
60 | ) {
61 | await fs.rm(options.out, { recursive: true })
62 | }
63 | const project = await app.convert()
64 | return handleProject(project)
65 | }
66 |
67 | /**
68 | *
69 | * @param {import('typedoc').ProjectReflection | undefined} project
70 | */
71 | async function handleProject(project) {
72 | if (project) {
73 | // Rendered docs
74 | try {
75 | await app.generateDocs(project, options.out)
76 | app.logger.info(`generated at ${options.out}.`)
77 | } catch (error) {
78 | app.logger.error(error)
79 | }
80 | } else {
81 | app.logger.error('No project')
82 | }
83 | }
84 |
85 | return {
86 | build,
87 | serve,
88 | }
89 | }
90 |
91 | async function exists(path) {
92 | try {
93 | await fs.access(path)
94 | return true
95 | } catch {
96 | return false
97 | }
98 | }
99 |
100 | /**
101 | * @typedef {Record} FrontMatterVars
102 | */
103 |
104 | /**
105 | * Prepends YAML block to a string
106 | * @param {string} contents - string to prepend to
107 | * @param {FrontMatterVars} vars - object of required front matter variables
108 | */
109 | function prependYAML(contents, vars) {
110 | return contents
111 | .replace(/^/, toYAML(vars) + '\n\n')
112 | .replace(/[\r\n]{3,}/g, '\n\n')
113 | }
114 |
115 | /**
116 | * Converts YAML object to a YAML string
117 | * @param {FrontMatterVars} vars
118 | */
119 | function toYAML(vars) {
120 | const yaml = `---
121 | ${Object.entries(vars)
122 | .map(
123 | ([key, value]) =>
124 | `${key}: ${
125 | typeof value === 'string' ? `"${escapeDoubleQuotes(value)}"` : value
126 | }`
127 | )
128 | .join('\n')}
129 | ---`
130 | return yaml
131 | }
132 |
133 | /**
134 | * Escapes double quotes in a string
135 | * @param {string} str - string to escape
136 | */
137 | function escapeDoubleQuotes(str) {
138 | return str.replace(/"/g, '\\"')
139 | }
140 |
--------------------------------------------------------------------------------
/docs/guide/other-firebase-services.md:
--------------------------------------------------------------------------------
1 | # Firebase Services
2 |
3 | VueFire provides a set of composable functions to access some of the different Firebase services. These are all exposed as composables starting with the word _use_:
4 |
5 | ```vue
6 |
22 | ```
23 |
24 | As [all composables](https://vuejs.org/guide/reusability/composables.html), these must be called within _Injectable Contexts_ like the _setup_ of a component, a Pinia store, or a Router navigation guard. However, you can call these specific Firebase Services composables anywhere in your application as long as you pass the **Firebase App name as the parameter**.
25 |
26 | ::: tip
27 | The Firebase Name parameter is only needed when using the composable outside of _setup_ and one of these condition are met:
28 |
29 | - You are doing SSR
30 | - You have multiple Firebase Apps
31 |
32 | **Omit the name in all other scenarios**, it's just not needed.
33 | :::
34 |
35 | ## Other Firebase Services
36 |
37 | For any other service not covered by VueFire, you should use the Firebase SDK directly by passing the firebase app as the first parameter:
38 |
39 | ```vue
40 |
47 | ```
48 |
49 | If you find yourself using this very often, you can create a composable for it:
50 |
51 | ::: code-group
52 |
53 | ```ts [composables/firebase-messaging.ts]
54 | import { getMessaging } from 'firebase/messaging'
55 | import { useFirebaseApp } from 'vuefire'
56 |
57 | export function useFirebaseMessaging() {
58 | return getMessaging(useFirebaseApp())
59 | }
60 | ```
61 |
62 | ```vue [MyComponent.vue]
63 |
68 | ```
69 |
70 | :::
71 |
72 | In the case of Services that require initialization, you should do it alongside the initialization of the Firebase App:
73 |
74 | ```ts
75 | import { initializeApp } from 'firebase/app'
76 | import { initializeAnalytics } from 'firebase/analytics'
77 |
78 | export const firebaseApp = initializeApp({
79 | // config
80 | })
81 | initializeAnalytics(firebaseApp)
82 | ```
83 |
84 | ## Nuxt
85 |
86 | In the context of Nuxt, you can create a plugin in the `plugins/` folder, it will be picked up automatically by Nuxt:
87 |
88 | ::: code-group
89 |
90 | ```ts [plugins/analytics.client.ts]
91 | import {
92 | type Analytics,
93 | initializeAnalytics,
94 | isSupported,
95 | } from 'firebase/analytics'
96 |
97 | export default defineNuxtPlugin(async () => {
98 | const firebaseApp = useFirebaseApp()
99 |
100 | console.log('Loading analytics')
101 |
102 | let analytics: Analytics | null = null
103 | if (await isSupported()) {
104 | analytics = initializeAnalytics(firebaseApp)
105 | console.log('Loaded analytics')
106 | } else {
107 | console.log('Analytics not supported')
108 | }
109 |
110 | return {
111 | provide: {
112 | analytics,
113 | },
114 | }
115 | })
116 | ```
117 |
118 | :::
119 |
120 | The `.client` suffix is important for services that only run on the client, like analytics. See the [Nuxt docs](https://nuxt.com/docs/guide/directory-structure/plugins) for more information.
121 |
--------------------------------------------------------------------------------
/docs/nuxt/getting-started.md:
--------------------------------------------------------------------------------
1 | # Nuxt.js
2 |
3 | VueFire comes with an official Nuxt module that automatically handles most of the hassle of setting up VueFire in your Nuxt project.
4 |
5 | ## Installation
6 | ```bash
7 | npm install firebase
8 | npx nuxi@latest module add vuefire
9 | ```
10 |
11 | Additionally, if you are using [SSR](https://nuxt.com/docs/api/configuration/nuxt-config/#ssr), you need to install `firebase-admin` and its peer dependencies:
12 |
13 | ::: code-group
14 |
15 | ```sh [pnpm]
16 | pnpm install firebase-admin firebase-functions @firebase/app-types
17 | ```
18 |
19 | ```sh [yarn]
20 | yarn add firebase-admin firebase-functions @firebase/app-types
21 | ```
22 |
23 | ```sh [npm]
24 | npm install firebase-admin firebase-functions @firebase/app-types
25 | ```
26 |
27 | :::
28 |
29 | ::: tip
30 |
31 | Depending on your needs, you might want to set up SSR or not. The final complexity of the project is really different. If you want a starter project see the existing templates:
32 |
33 | - [Spark Plan](https://github.com/posva/nuxt--vuefire-example-spark-plan)
34 | - [Blaze Plan](https://github.com/posva/nuxt--vuefire-example-blaze-plan)
35 |
36 | :::
37 |
38 | ## Configuration
39 |
40 | Next, add `nuxt-vuefire` to the `modules` section of `nuxt.config.js` and configure it with the credentials created in your app settings in your project overview (`https://console.firebase.google.com/project/YOUR_PROJECT_NAME/overview)`. You can find more details [in Firebase Documentation](https://firebase.google.com/docs/web/setup#create-project). It should look something like this:
41 |
42 | ```ts{4,7-15}
43 | export default defineNuxtConfig({
44 | modules: [
45 | // ... other modules
46 | 'nuxt-vuefire',
47 | ],
48 |
49 | vuefire: {
50 | config: {
51 | apiKey: '...',
52 | authDomain: '...',
53 | projectId: '...',
54 | appId: '...',
55 | // there could be other properties depending on the project
56 | },
57 | },
58 | })
59 | ```
60 |
61 | ### Configuring the Admin SDK
62 |
63 | If you are using SSR with any auth related feature, you will need to create a [service account](https://firebase.google.com/support/guides/service-accounts) and provide its content as an _environment variable_ named `GOOGLE_APPLICATION_CREDENTIALS` in the `.env` file.
64 |
65 | In local development it's more convenient to put the `service-account.json` file alongside other files and refer to its path in the environment variable:
66 |
67 | ```txt
68 | GOOGLE_APPLICATION_CREDENTIALS=service-account.json
69 | ```
70 |
71 | :::tip
72 | This service account file contains sensitive information and should **not be committed to your repository**. Make sure to exclude it from your version control system:
73 |
74 | ```sh
75 | echo service-account.json >> .gitignore
76 | ```
77 |
78 | :::
79 |
80 | ### Additional configuration
81 |
82 | If you are using the [Authentication](https://firebase.google.com/docs/auth) module or [AppCheck](https://firebase.google.com/docs/app-check#web), make sure to enable them as well:
83 |
84 | ```ts{5-7,8-13}
85 | export default defineNuxtConfig({
86 | // ...
87 | vuefire: {
88 | // ensures the auth module is enabled
89 | auth: {
90 | enabled: true
91 | },
92 | appCheck: {
93 | // Allows you to use a debug token in development
94 | debug: process.env.NODE_ENV !== 'production',
95 | isTokenAutoRefreshEnabled: true,
96 | provider: 'ReCaptchaV3',
97 | // Find the instructions in the Firebase documentation, link above
98 | key: '...',
99 | },
100 | },
101 | })
102 | ```
103 |
104 | ## Auto imports
105 |
106 | Nuxt VueFire will automatically import the most commonly used functions of `vuefire` so you don't even need to import them in your components ✨.
107 |
--------------------------------------------------------------------------------
/src/ssr/initialState.ts:
--------------------------------------------------------------------------------
1 | import type { FirebaseApp } from 'firebase/app'
2 | import { DatabaseReference, Query as DatabaseQuery } from 'firebase/database'
3 | import { StorageReference } from 'firebase/storage'
4 | import {
5 | isDatabaseReference,
6 | isFirestoreDataReference,
7 | isFirestoreQuery,
8 | isStorageReference,
9 | noop,
10 | _FirestoreDataSource,
11 | _Nullable,
12 | } from '../shared'
13 |
14 | export interface SSRStore {
15 | // firestore data
16 | f: Record
17 | // rtdb data
18 | r: Record
19 |
20 | // storage urls and metadata
21 | s: Record
22 |
23 | // auth user
24 | u: Record
25 | }
26 |
27 | // @internal
28 | export const _initialStatesMap = new WeakMap()
29 |
30 | /**
31 | * Allows getting the initial state set during SSR on the client.
32 | *
33 | * @param initialState - the initial state to set for the firebase app during SSR. Pass undefined to not set it
34 | * @param firebaseApp - the firebase app to get the initial state for
35 | * @returns the initial states for the current firebaseApp
36 | */
37 | export function useSSRInitialState(
38 | initialState: SSRStore | undefined,
39 | firebaseApp: FirebaseApp
40 | ): SSRStore {
41 | // get initial state based on the current firebase app
42 | if (!_initialStatesMap.has(firebaseApp)) {
43 | _initialStatesMap.set(
44 | firebaseApp,
45 | initialState || { f: {}, r: {}, s: {}, u: {} }
46 | )
47 | }
48 |
49 | return _initialStatesMap.get(firebaseApp)!
50 | }
51 |
52 | export function getInitialValue(
53 | dataSource: _Nullable<
54 | _FirestoreDataSource | DatabaseReference | DatabaseQuery | StorageReference
55 | >,
56 | ssrKey: string | undefined,
57 | fallbackValue: unknown,
58 | firebaseApp: FirebaseApp
59 | ) {
60 | if (!dataSource) return fallbackValue
61 |
62 | const [sourceType, path] = getDataSourceInfo(dataSource)
63 | if (!sourceType) return fallbackValue
64 |
65 | const initialState: Record =
66 | useSSRInitialState(undefined, firebaseApp)[sourceType] || {}
67 | const key = ssrKey || path
68 |
69 | // TODO: warn for queries on the client if there are other keys and this is during hydration
70 |
71 | // returns the fallback value if no key, otherwise initial state
72 | return key && key in initialState ? initialState[key] : fallbackValue
73 | }
74 |
75 | export function deferInitialValueSetup(
76 | dataSource: _Nullable<
77 | _FirestoreDataSource | DatabaseReference | DatabaseQuery | StorageReference
78 | >,
79 | ssrKey: string | undefined | null,
80 | promise: Promise,
81 | firebaseApp: FirebaseApp
82 | ) {
83 | if (!dataSource) return
84 |
85 | const [sourceType, path] = getDataSourceInfo(dataSource)
86 | if (!sourceType) return
87 |
88 | const initialState: Record = useSSRInitialState(
89 | undefined,
90 | firebaseApp
91 | )[sourceType]
92 | const key = ssrKey || path
93 |
94 | if (key) {
95 | promise
96 | .then((value) => {
97 | initialState[key] = value
98 | })
99 | // avoid permission errors in tests and others
100 | .catch(noop)
101 | return key
102 | }
103 | }
104 |
105 | function getDataSourceInfo(
106 | dataSource:
107 | | _FirestoreDataSource
108 | | DatabaseReference
109 | | DatabaseQuery
110 | | StorageReference
111 | ) {
112 | return isFirestoreDataReference(dataSource) || isFirestoreQuery(dataSource)
113 | ? (['f', dataSource.path] as const)
114 | : isDatabaseReference(dataSource)
115 | ? (['r', dataSource.toString()] as const)
116 | : isStorageReference(dataSource)
117 | ? (['s', dataSource.toString()] as const)
118 | : []
119 | }
120 |
--------------------------------------------------------------------------------
/playground/src/pages/authentication.vue:
--------------------------------------------------------------------------------
1 |
77 |
78 |
79 |
80 | Auth playground
81 | SignOut
82 | Anonymous signIn
83 | Signin Google (popup)
84 | Signin Google (redirect)
85 | Change User picture
86 |
87 |
100 |
101 |
114 |
115 |
116 | Name: {{ user.displayName }}
117 |
122 |
123 |
124 |
125 |
126 | Current User:
127 | {{ user }}
128 |
129 |
130 |
--------------------------------------------------------------------------------