Skip to content

Commit 1e260b9

Browse files
committed
feat(emulators): auto detect with running API
Close #1429
1 parent 3a18102 commit 1e260b9

File tree

2 files changed

+68
-180
lines changed

2 files changed

+68
-180
lines changed

packages/nuxt/src/module.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ import {
2222
} from './module/options'
2323
import {
2424
type FirebaseEmulatorsToEnable,
25-
detectEmulators,
26-
willUseEmulators,
25+
autodetectEmulators,
2726
} from './module/emulators'
2827

2928
const logger = consola.withTag('nuxt-vuefire module')
@@ -97,11 +96,7 @@ export default defineNuxtModule<VueFireNuxtModuleOptions>({
9796
const templatesDir = fileURLToPath(new URL('../templates', import.meta.url))
9897

9998
// we need this to avoid some warnings about missing credentials and ssr
100-
const emulatorsConfig = await willUseEmulators(
101-
options,
102-
resolve(nuxt.options.rootDir, 'firebase.json'),
103-
logger
104-
)
99+
const emulatorsConfig = await autodetectEmulators(options, logger)
105100

106101
// to handle TimeStamp and GeoPoints objects
107102
addPlugin(resolve(runtimeDir, 'payload-plugin'))
@@ -210,7 +205,7 @@ export default defineNuxtModule<VueFireNuxtModuleOptions>({
210205
// Emulators must be enabled after the app is initialized but before some APIs like auth.signinWithCustomToken() are called
211206

212207
if (emulatorsConfig) {
213-
const emulators = detectEmulators(options, emulatorsConfig, logger)
208+
const emulators = emulatorsConfig
214209
// add the option to disable the warning. It only exists in Auth
215210
if (emulators?.auth) {
216211
emulators.auth.options = options.emulators.auth?.options

packages/nuxt/src/module/emulators.ts

Lines changed: 65 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
import { readFile, access, constants } from 'node:fs/promises'
2-
import stripJsonComments from 'strip-json-comments'
31
import type { ConsolaInstance } from 'consola'
42
import type { VueFireNuxtModuleOptionsResolved } from './options'
53

6-
export async function willUseEmulators(
7-
{ emulators }: VueFireNuxtModuleOptionsResolved,
8-
firebaseJsonPath: string,
4+
/**
5+
* Detects the emulators to enable based on their API. Returns an object of all the emulators that should be enabled.
6+
*
7+
* @param options - The module options
8+
* @param logger - The logger instance
9+
*/
10+
export async function autodetectEmulators(
11+
{ emulators: options, auth }: VueFireNuxtModuleOptionsResolved,
912
logger: ConsolaInstance
10-
): Promise<NonNullable<FirebaseEmulatorsJSON['emulators']> | null> {
13+
) {
14+
const defaultHost: string = options.host || '127.0.0.1'
15+
1116
const isEmulatorEnabled =
1217
// emulators is always defined
13-
emulators.enabled &&
18+
options.enabled &&
1419
// Disable emulators on production unless the user explicitly enables them
1520
(process.env.NODE_ENV !== 'production' ||
1621
(process.env.VUEFIRE_EMULATORS &&
@@ -21,66 +26,33 @@ export async function willUseEmulators(
2126
return null
2227
}
2328

24-
// return true if the file doesn't exist instead of throwing
25-
if (await access(firebaseJsonPath, constants.F_OK).catch(() => true)) {
26-
logger.warn(
27-
`The "firebase.json" file doesn't exist at "${firebaseJsonPath}".`
28-
)
29-
return null
30-
}
31-
32-
let firebaseJson: FirebaseEmulatorsJSON | null = null
33-
try {
34-
firebaseJson = JSON.parse(
35-
stripJsonComments(await readFile(firebaseJsonPath, 'utf8'), {
36-
trailingCommas: true,
37-
})
38-
)
39-
} catch (err) {
40-
logger.error('Error parsing the `firebase.json` file', err)
41-
logger.error('Cannot enable Emulators')
42-
}
43-
44-
return firebaseJson?.emulators ?? null
45-
}
46-
47-
/**
48-
* Detects the emulators to enable based on the `firebase.json` file. Returns an object of all the emulators that should
49-
* be enabled based on the `firebase.json` file and other options and environment variables.
50-
*
51-
* @param options - The module options
52-
* @param firebaseJsonPath - resolved path to the `firebase.json` file
53-
* @param logger - The logger instance
54-
*/
55-
export function detectEmulators(
56-
{
57-
emulators: _vuefireEmulatorsOptions,
58-
auth,
59-
}: VueFireNuxtModuleOptionsResolved,
60-
firebaseEmulatorsConfig: NonNullable<FirebaseEmulatorsJSON['emulators']>,
61-
logger: ConsolaInstance
62-
) {
63-
// normalize the emulators option
64-
const vuefireEmulatorsOptions =
65-
typeof _vuefireEmulatorsOptions === 'object'
66-
? _vuefireEmulatorsOptions
67-
: {
68-
enabled: _vuefireEmulatorsOptions,
69-
}
29+
const emulatorsResponse: EmulatorsAPIResponse | null = await fetch(
30+
`http://${defaultHost}:4400/emulators`
31+
)
32+
.then((res) => {
33+
return res.status === 200 ? res.json() : null
34+
})
35+
.catch((err: Error) => {
36+
// skip errors of emulators not running
37+
if (
38+
err instanceof Error &&
39+
typeof err.cause === 'object' &&
40+
// @ts-expect-error: not in the types
41+
err.cause?.code !== 'ECONNREFUSED'
42+
) {
43+
logger.error('Error fetching emulators', err)
44+
}
45+
return null
46+
})
7047

71-
if (!firebaseEmulatorsConfig) {
72-
if (vuefireEmulatorsOptions.enabled !== false) {
73-
logger.warn(
74-
'You enabled emulators but there is no `emulators` key in your `firebase.json` file. Emulators will not be enabled.'
75-
)
76-
}
77-
return
48+
if (!emulatorsResponse) {
49+
return null
7850
}
7951

80-
const defaultHost: string = vuefireEmulatorsOptions.host || '127.0.0.1'
81-
8252
const emulatorsToEnable = services.reduce((acc, service) => {
83-
if (firebaseEmulatorsConfig[service]) {
53+
if (emulatorsResponse[service]) {
54+
let { host, port } = emulatorsResponse[service]!
55+
8456
// these env variables are automatically picked up by the admin SDK too
8557
// https://firebase.google.com/docs/emulator-suite/connect_rtdb?hl=en&authuser=0#admin_sdks
8658
// Also, Firestore is the only one that has a different env variable
@@ -89,8 +61,6 @@ export function detectEmulators(
8961
? 'FIRESTORE_EMULATOR_HOST'
9062
: `FIREBASE_${service.toUpperCase()}_EMULATOR_HOST`
9163

92-
let host: string | undefined
93-
let port: number | undefined
9464
// Pick up the values from the env variables if set by the user
9565
if (process.env[envKey]) {
9666
logger.debug(
@@ -110,52 +80,7 @@ export function detectEmulators(
11080
}
11181
}
11282

113-
// take the values from the firebase.json file
114-
const emulatorsServiceConfig = firebaseEmulatorsConfig[service]
115-
// they might be picked up from the environment variables
116-
host ??= emulatorsServiceConfig?.host || defaultHost
117-
port ??= emulatorsServiceConfig?.port
118-
119-
const missingHostServices: FirebaseEmulatorService[] = []
120-
if (emulatorsServiceConfig?.host == null) {
121-
// we push to warn later in one single warning
122-
missingHostServices.push(service)
123-
} else if (emulatorsServiceConfig.host !== host) {
124-
logger.error(
125-
`The "${service}" emulator is enabled but the "host" property in the "emulators.${service}" section of your "firebase.json" file is different from the "vuefire.emulators.host" value. You might encounter errors in your app if this is not fixed.`
126-
)
127-
}
128-
129-
// The default value is 127.0.0.1, so it's fine if the user doesn't set it at all
130-
if (missingHostServices.length > 0 && host !== '127.0.0.1') {
131-
logger.warn(
132-
`The "${service.at(
133-
0
134-
)!}" 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. You should probably set it to "${defaultHost}" ("vuefire.emulators.host" value).` +
135-
(missingHostServices.length > 1
136-
? ` The following emulators are also missing the "host" key: ${missingHostServices
137-
.slice(1)
138-
.join(', ')}.`
139-
: '')
140-
)
141-
}
142-
143-
if (!port) {
144-
logger.error(
145-
`The "${service}" emulator is enabled but there is no "port" property in the "emulators" section of your "firebase.json" file. It must be specified to enable emulators. The "${service}" emulator won't be enabled.`
146-
)
147-
return acc
148-
// if the port is set in the config, it must match the env variable
149-
} else if (
150-
emulatorsServiceConfig &&
151-
emulatorsServiceConfig.port !== port
152-
) {
153-
logger.error(
154-
`The "${service}" emulator is enabled but the "port" property in the "emulators.${service}" section of your "firebase.json" file is different from the "${envKey}" env variable. You might encounter errors in your app if this is not fixed.`
155-
)
156-
}
157-
158-
// add the emulator to the list
83+
// add them
15984
acc[service] = { host, port }
16085
}
16186
return acc
@@ -177,67 +102,6 @@ export function detectEmulators(
177102
return emulatorsToEnable
178103
}
179104

180-
/**
181-
* Extracted from as we cannot install firebase-tools just for the types
182-
* - https://github.com/firebase/firebase-tools/blob/master/src/firebaseConfig.ts#L183
183-
* - https://github.com/firebase/firebase-tools/blob/master/schema/firebase-config.json
184-
* @internal
185-
*/
186-
export interface FirebaseEmulatorsJSON {
187-
emulators?: {
188-
auth?: {
189-
host?: string
190-
port?: number
191-
}
192-
database?: {
193-
host?: string
194-
port?: number
195-
}
196-
eventarc?: {
197-
host?: string
198-
port?: number
199-
}
200-
extensions?: {
201-
[k: string]: unknown
202-
}
203-
firestore?: {
204-
host?: string
205-
port?: number
206-
websocketPort?: number
207-
}
208-
functions?: {
209-
host?: string
210-
port?: number
211-
}
212-
hosting?: {
213-
host?: string
214-
port?: number
215-
}
216-
hub?: {
217-
host?: string
218-
port?: number
219-
}
220-
logging?: {
221-
host?: string
222-
port?: number
223-
}
224-
pubsub?: {
225-
host?: string
226-
port?: number
227-
}
228-
singleProjectMode?: boolean
229-
storage?: {
230-
host?: string
231-
port?: number
232-
}
233-
ui?: {
234-
enabled?: boolean
235-
host?: string
236-
port?: string | number
237-
}
238-
}
239-
}
240-
241105
export type FirebaseEmulatorService =
242106
| 'auth'
243107
| 'database'
@@ -266,3 +130,32 @@ export interface FirebaseEmulatorsToEnable
266130
options?: Parameters<typeof import('firebase/auth').connectAuthEmulator>[2]
267131
}
268132
}
133+
134+
interface EmulatorServiceAddressInfo {
135+
address: string
136+
family: string // Assuming this will contain valid IPv4 or IPv6 strings
137+
port: number
138+
}
139+
140+
interface EmulatorService {
141+
listen: EmulatorServiceAddressInfo[]
142+
name: string
143+
host: string
144+
port: number
145+
pid?: number // Assuming this field is optional
146+
reservedPorts?: number[] // Assuming this field is optional and can be an array of numbers
147+
webSocketHost?: string // Assuming this field is optional
148+
webSocketPort?: number // Assuming this field is optional
149+
}
150+
151+
interface EmulatorsAPIResponse {
152+
hub?: EmulatorService
153+
ui?: EmulatorService
154+
logging?: EmulatorService
155+
auth?: EmulatorService
156+
functions?: EmulatorService
157+
firestore?: EmulatorService
158+
database?: EmulatorService
159+
hosting?: EmulatorService
160+
storage?: EmulatorService
161+
}

0 commit comments

Comments
 (0)