Skip to content

Commit 7f86fa7

Browse files
committed
feat: add emulators support
1 parent b41fffc commit 7f86fa7

13 files changed

+395
-14
lines changed

firebase.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
"database": {
2222
"port": 8081
2323
},
24+
"functions": {
25+
"port": 5001
26+
},
2427
"storage": {
2528
"port": 9199
2629
},

packages/nuxt/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
},
3838
"dependencies": {
3939
"@nuxt/kit": "^3.6.2",
40-
"lru-cache": "^10.0.0"
40+
"lru-cache": "^10.0.0",
41+
"strip-json-comments": "^5.0.1"
4142
},
4243
"peerDependencies": {
4344
"@firebase/app-types": ">=0.8.1",

packages/nuxt/playground/database.rules.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
"rules": {
33
"forbidden": {
44
".read": false,
5-
".write": false,
5+
".write": false
66
},
77
".read": true,
8-
".write": true,
8+
".write": true
99
}
10-
}
10+
}

packages/nuxt/playground/firebase.json

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55
"hosting": {
66
"public": ".output/public",
77
"cleanUrls": true,
8-
"ignore": [
9-
"firebase.json",
10-
"**/.*",
11-
"**/node_modules/**"
12-
],
8+
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
139
"rewrites": [
1410
{
1511
"source": "**",
@@ -28,19 +24,28 @@
2824
"rules": "storage.rules"
2925
},
3026
"emulators": {
31-
"functions": {
32-
"port": 5001
27+
"auth": {
28+
"host": "127.0.0.1",
29+
"port": 9099
3330
},
31+
// "functions": {
32+
// "host": "127.0.0.1",
33+
// "port": 5001
34+
// },
3435
"firestore": {
36+
"host": "127.0.0.1",
3537
"port": 8080
3638
},
3739
"database": {
38-
"port": 9050
40+
"host": "127.0.0.1",
41+
"port": 8081
3942
},
4043
"hosting": {
44+
"host": "127.0.0.1",
4145
"port": 5050
4246
},
4347
"storage": {
48+
"host": "127.0.0.1",
4449
"port": 9199
4550
},
4651
"ui": {

packages/nuxt/src/module.ts

Lines changed: 209 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { fileURLToPath } from 'node:url'
22
import { normalize } from 'node:path'
3+
import { readFile, stat } from 'node:fs/promises'
4+
import stripJsonComments from 'strip-json-comments'
35
import {
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

6680
const 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+
295345
interface VueFireAppConfig {
296346
/**
297347
* Firebase config to initialize the app.
@@ -309,6 +359,7 @@ interface VueFireAppConfig {
309359
declare 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+
}

packages/nuxt/src/runtime/auth/api.session-verification.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
1818
const { token } = await readBody<{ token?: string }>(event)
1919

2020
logger.debug('Getting the admin app')
21-
const adminApp = getAdminApp({}, 'session-verification')
21+
const adminApp = getAdminApp(undefined, 'session-verification')
2222
const adminAuth = getAdminAuth(adminApp)
2323

2424
logger.debug(token ? 'Verifying the token' : 'Deleting the session cookie')

packages/nuxt/src/runtime/auth/plugin.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
3636
// console.timeLog('token', `got token for ${user.uid}`)
3737
if (customToken) {
3838
const auth = getAuth(firebaseApp)
39+
logger.debug('Signing in with custom token')
3940
await signInWithCustomToken(auth, customToken)
4041
// console.timeLog('token', `signed in with token for ${user.uid}`)
4142
// console.timeEnd('token')
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { connectAuthEmulator, getAuth } from 'firebase/auth'
2+
import type { FirebaseApp } from 'firebase/app'
3+
import { logger } from '../logging'
4+
import { defineNuxtPlugin, useRuntimeConfig } from '#app'
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+
return
24+
}
25+
26+
connectAuthEmulator(getAuth(firebaseApp), `http://${host}:${port}`)
27+
logger.info(`Auth emulator connected to http://${host}:${port}`)
28+
connectedEmulators.set(firebaseApp, true)
29+
})
30+
31+
const connectedEmulators = new WeakMap<FirebaseApp, unknown>()

0 commit comments

Comments
 (0)