11import { fileURLToPath } from 'node:url'
22import { normalize } from 'node:path'
3+ import { readFile , stat } from 'node:fs/promises'
4+ import stripJsonComments from 'strip-json-comments'
35import {
46 addImports ,
57 addPlugin ,
@@ -61,6 +63,18 @@ export interface VueFireNuxtModuleOptions {
6163 * Enables Authentication
6264 */
6365 auth ?: boolean
66+
67+ /**
68+ * Controls whether to use emulators or not. Pass `false` to disable emulators. When set to `true`, emulators are enabled when they are detected in the `firebase.json` file. You still need to run the emulators in parallel to your app.
69+ */
70+ emulators ?:
71+ | boolean
72+ | {
73+ /**
74+ * The host for the Firestore emulator. Defaults to `localhost`.
75+ */
76+ host ?: string
77+ }
6478}
6579
6680const logger = consola . withTag ( 'nuxt-vuefire module' )
@@ -76,9 +90,10 @@ export default defineNuxtModule<VueFireNuxtModuleOptions>({
7690
7791 defaults : {
7892 optionsApiPlugin : false ,
93+ emulators : true ,
7994 } ,
8095
81- setup ( options , nuxt ) {
96+ async setup ( options , nuxt ) {
8297 // ensure provided options are valid
8398 if ( ! options . config ) {
8499 throw new Error (
@@ -185,6 +200,35 @@ export default defineNuxtModule<VueFireNuxtModuleOptions>({
185200 ] )
186201 }
187202
203+ // Emulators must be enabled after the app is initialized but before some APIs like auth.signinWithCustomToken() are called
204+ if (
205+ // Disable emulators on production unless the user explicitly enables them
206+ ( process . env . NODE_ENV !== 'production' ||
207+ process . env . VUEFIRE_EMULATORS ) &&
208+ options . emulators
209+ ) {
210+ const emulators = await enableEmulators (
211+ options . emulators ,
212+ resolve ( nuxt . options . rootDir , 'firebase.json' ) ,
213+ logger
214+ )
215+
216+ nuxt . options . runtimeConfig . public . vuefire ??= { }
217+ nuxt . options . runtimeConfig . public . vuefire . emulators = emulators
218+
219+ for ( const serviceName in emulators ) {
220+ const { host, port } = emulators [ serviceName as keyof typeof emulators ]
221+ // set the env variables so they are picked up automatically by the admin SDK
222+ process . env [
223+ serviceName === 'firestore'
224+ ? 'FIRESTORE_EMULATOR_HOST'
225+ : `FIREBASE_${ serviceName . toUpperCase ( ) } _EMULATOR_HOST`
226+ ] = `${ host } :${ port } `
227+ logger . info ( `Enabling ${ serviceName } emulator at ${ host } :${ port } ` )
228+ addPlugin ( resolve ( runtimeDir , `emulators/${ serviceName } .plugin` ) )
229+ }
230+ }
231+
188232 // adds the firebase app to each application
189233 addPlugin ( resolve ( runtimeDir , 'app/plugin.client' ) )
190234 addPlugin ( resolve ( runtimeDir , 'app/plugin.server' ) )
@@ -292,6 +336,12 @@ interface VueFireRuntimeConfig {
292336 vuefireAdminOptions ?: Omit < AppOptions , 'credential' >
293337}
294338
339+ interface VueFirePublicRuntimeConfig {
340+ vuefire ?: {
341+ emulators ?: FirebaseEmulatorsToEnable
342+ }
343+ }
344+
295345interface VueFireAppConfig {
296346 /**
297347 * Firebase config to initialize the app.
@@ -309,6 +359,7 @@ interface VueFireAppConfig {
309359declare module '@nuxt/schema' {
310360 export interface AppConfig extends VueFireAppConfig { }
311361 export interface RuntimeConfig extends VueFireRuntimeConfig { }
362+ export interface PublicRuntimeConfig extends VueFirePublicRuntimeConfig { }
312363}
313364
314365// @ts -ignore: #app not found error when building
@@ -336,3 +387,160 @@ declare module '@vue/runtime-core' {
336387 $firebaseAdminApp : FirebaseAdminApp
337388 }
338389}
390+
391+ async function enableEmulators (
392+ emulatorOptions : VueFireNuxtModuleOptions [ 'emulators' ] ,
393+ firebaseJsonPath : string ,
394+ logger : typeof consola
395+ ) {
396+ const fileStats = await stat ( firebaseJsonPath )
397+ if ( ! fileStats . isFile ( ) ) {
398+ return
399+ }
400+ let firebaseJson : FirebaseEmulatorsJSON
401+ try {
402+ firebaseJson = JSON . parse (
403+ stripJsonComments ( await readFile ( firebaseJsonPath , 'utf8' ) , {
404+ trailingCommas : true ,
405+ } )
406+ )
407+ } catch ( err ) {
408+ logger . error ( 'Error parsing the `firebase.json` file' , err )
409+ logger . error ( 'Cannot enable Emulators' )
410+ return
411+ }
412+
413+ if ( ! firebaseJson . emulators ) {
414+ if ( emulatorOptions === true ) {
415+ logger . warn (
416+ 'You enabled emulators but there is no `emulators` key in your `firebase.json` file. Emulators will not be enabled.'
417+ )
418+ }
419+ return
420+ }
421+
422+ const services = [ 'auth' , 'database' , 'firestore' , 'functions' ] as const
423+
424+ const defaultHost =
425+ typeof emulatorOptions === 'object' ? emulatorOptions . host : 'localhost'
426+
427+ const emulatorsToEnable = services . reduce ( ( acc , service ) => {
428+ if ( firebaseJson . emulators ! [ service ] ) {
429+ // these env variables are automatically picked up by the admin SDK too
430+ // https://firebase.google.com/docs/emulator-suite/connect_rtdb?hl=en&authuser=0#admin_sdks
431+ const envKey =
432+ service === 'firestore'
433+ ? 'FIRESTORE_EMULATOR_HOST'
434+ : `FIREBASE_${ service . toUpperCase ( ) } _EMULATOR_HOST`
435+
436+ if ( process . env [ envKey ] ) {
437+ try {
438+ const url = new URL ( `http://${ process . env [ envKey ] } ` )
439+ acc [ service ] = {
440+ host : url . hostname ,
441+ port : Number ( url . port ) ,
442+ }
443+ return acc
444+ } catch ( err ) {
445+ logger . error (
446+ `The "${ envKey } " env variable is set but it is not a valid URL. It should be something like "localhost:8080" or "127.0.0.1:8080". It will be ignored.`
447+ )
448+ logger . error ( `Cannot enable the ${ service } Emulator.` )
449+ }
450+ }
451+ // take the values from the firebase.json file
452+ const serviceEmulatorConfig = firebaseJson . emulators ! [ service ]
453+ if ( serviceEmulatorConfig ?. host == null ) {
454+ logger . warn (
455+ `The "${ service } " emulator is enabled but there is no "host" key in the "emulators.${ service } " key of your "firebase.json" file. It is recommended to set it to avoid mismatches between origins. Set it to "${ defaultHost } ".`
456+ )
457+ }
458+
459+ const host = serviceEmulatorConfig ?. host || defaultHost
460+ const port = serviceEmulatorConfig ?. port
461+ if ( ! host || ! port ) {
462+ logger . error (
463+ `The "${ service } " emulator is enabled but there is no "host" or "port" key in the "emulators" key of your "firebase.json" file. You must specify *both*. It will be ignored.`
464+ )
465+ return acc
466+ }
467+ acc [ service ] = { host, port }
468+ }
469+ return acc
470+ } , { } as FirebaseEmulatorsToEnable )
471+
472+ return emulatorsToEnable
473+ }
474+
475+ /**
476+ * Extracted from as we cannot install firebase-tools just for the types
477+ * - https://github.com/firebase/firebase-tools/blob/master/src/firebaseConfig.ts#L183
478+ * - https://github.com/firebase/firebase-tools/blob/master/schema/firebase-config.json
479+ * @internal
480+ */
481+ interface FirebaseEmulatorsJSON {
482+ emulators ?: {
483+ auth ?: {
484+ host ?: string
485+ port ?: number
486+ }
487+ database ?: {
488+ host ?: string
489+ port ?: number
490+ }
491+ eventarc ?: {
492+ host ?: string
493+ port ?: number
494+ }
495+ extensions ?: {
496+ [ k : string ] : unknown
497+ }
498+ firestore ?: {
499+ host ?: string
500+ port ?: number
501+ websocketPort ?: number
502+ }
503+ functions ?: {
504+ host ?: string
505+ port ?: number
506+ }
507+ hosting ?: {
508+ host ?: string
509+ port ?: number
510+ }
511+ hub ?: {
512+ host ?: string
513+ port ?: number
514+ }
515+ logging ?: {
516+ host ?: string
517+ port ?: number
518+ }
519+ pubsub ?: {
520+ host ?: string
521+ port ?: number
522+ }
523+ singleProjectMode ?: boolean
524+ storage ?: {
525+ host ?: string
526+ port ?: number
527+ }
528+ ui ?: {
529+ enabled ?: boolean
530+ host ?: string
531+ port ?: string | number
532+ }
533+ }
534+ }
535+
536+ type FirebaseEmulatorService =
537+ | 'auth'
538+ | 'database'
539+ | 'firestore'
540+ | 'functions'
541+ // | 'hosting' we are the hosting emulator
542+ | 'storage'
543+
544+ type FirebaseEmulatorsToEnable = {
545+ [ key in FirebaseEmulatorService ] : { host : string ; port : number }
546+ }
0 commit comments