-
-
Notifications
You must be signed in to change notification settings - Fork 344
/
Copy pathmodule.ts
356 lines (312 loc) · 10.9 KB
/
module.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
import { fileURLToPath } from 'node:url'
import { normalize } from 'node:path'
import {
addImports,
addPlugin,
addPluginTemplate,
addServerHandler,
createResolver,
defineNuxtModule,
} from '@nuxt/kit'
// cannot import from firebase/app because the build fails, maybe a nuxt bug?
import type { FirebaseApp, FirebaseOptions } from '@firebase/app-types'
import type { App as FirebaseAdminApp } from 'firebase-admin/app'
import { markRaw } from 'vue'
import { consola } from 'consola'
import {
VueFireNuxtModuleOptions,
VueFireNuxtModuleOptionsResolved,
} from './module/options'
import {
FirebaseEmulatorsToEnable,
detectEmulators,
willUseEmulators,
} from './module/emulators'
const logger = consola.withTag('nuxt-vuefire module')
export default defineNuxtModule<VueFireNuxtModuleOptions>({
meta: {
name: 'vuefire',
configKey: 'vuefire',
compatibility: {
nuxt: '^3.1.0',
},
},
defaults: {
optionsApiPlugin: false,
emulators: true,
},
async setup(options, nuxt) {
// ensure provided options are valid
if (!options.config) {
throw new Error(
'[nuxt-vuefire]: Missing firebase config. Provide a "config" option to the VueFire module options.'
)
}
const { resolve } = createResolver(import.meta.url)
const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))
const templatesDir = fileURLToPath(new URL('../templates', import.meta.url))
// we need this to avoid some warnings about missing credentials and ssr
const hasEmulatorsEnabled = await willUseEmulators(
options,
resolve(nuxt.options.rootDir, 'firebase.json'),
logger
)
// to handle TimeStamp and GeoPoints objects
addPlugin(resolve(runtimeDir, 'payload-plugin'))
// TODO: I don't think the appConfig is the right place to store these as it makes things reactive
// Let plugins and the user access the firebase config within the app
nuxt.options.appConfig.firebaseConfig = markRaw(options.config)
nuxt.options.appConfig.vuefireOptions = markRaw(options)
nuxt.options.runtimeConfig.vuefire = { options }
nuxt.options.build.transpile.push(runtimeDir)
nuxt.options.build.transpile.push(templatesDir)
// This one is set by servers, we set the GOOGLE_APPLICATION_CREDENTIALS env variable instead that has a lower priority and can be both a path or a JSON string
// process.env.FIREBASE_CONFIG ||= JSON.stringify(options.config)
if (typeof options.admin?.serviceAccount === 'string') {
process.env.GOOGLE_APPLICATION_CREDENTIALS ||=
options.admin.serviceAccount
}
const hasServiceAccount =
typeof process.env.GOOGLE_APPLICATION_CREDENTIALS === 'string' &&
process.env.GOOGLE_APPLICATION_CREDENTIALS.length > 0
// resolve the credentials in case of monorepos and other projects started from a different folder
if (
typeof process.env.GOOGLE_APPLICATION_CREDENTIALS === 'string' &&
process.env.GOOGLE_APPLICATION_CREDENTIALS?.[0] !== '{'
) {
const resolvedCredentials = resolve(
nuxt.options.rootDir,
process.env.GOOGLE_APPLICATION_CREDENTIALS
)
process.env.GOOGLE_APPLICATION_CREDENTIALS = resolvedCredentials
}
// NOTE: the order of the plugins is reversed, so we end by adding the app plugin which is used by all other
// plugins
if (options.appCheck) {
addPlugin(resolve(runtimeDir, 'app-check/plugin.client'))
// TODO: ensure this is the only necessary check. Maybe we need to check if server
if (!hasEmulatorsEnabled) {
if (hasServiceAccount) {
// this is needed by the api endpoint to properly work if no service account is provided, otherwise, the projectId is within the service account
addPlugin(resolve(runtimeDir, 'app-check/plugin.server'))
} else if (nuxt.options.ssr) {
logger.warn(
'You activated both SSR and app-check but you are not providing a service account for the admin SDK. See https://vuefire.vuejs.org/nuxt/getting-started.html#configuring-the-admin-sdk.'
)
}
}
}
if (options.auth) {
// TODO: should be fine if emulators are activated
if (nuxt.options.ssr && !hasServiceAccount && !hasEmulatorsEnabled) {
logger.warn(
'You activated both SSR and auth but you are not providing a service account for the admin SDK. See https://vuefire.vuejs.org/nuxt/getting-started.html#configuring-the-admin-sdk.'
)
}
// this adds the VueFire plugin and handle SSR state serialization and hydration
addPluginTemplate({
src: normalize(resolve(templatesDir, 'plugin.ejs')),
options: {
...options,
ssr: nuxt.options.ssr,
},
})
if (
options.auth &&
nuxt.options.ssr &&
(hasServiceAccount || hasEmulatorsEnabled)
) {
// Add the session handler than mints a cookie for the user
addServerHandler({
route: '/api/__session',
handler: resolve(runtimeDir, './auth/api.session-verification'),
})
// must be added after (which means before in code) the plugin module
addPlugin(resolve(runtimeDir, 'auth/plugin-mint-cookie.client'))
}
// hydrates the user if any
addPlugin(resolve(runtimeDir, 'auth/plugin.client'))
// loads the user on the current app
addPlugin(resolve(runtimeDir, 'auth/plugin.server'))
addVueFireImports([
// auth
{ from: 'vuefire', name: 'useFirebaseAuth' },
{ from: 'vuefire', name: 'useCurrentUser' },
])
// these are improved for nuxt to avoid the need to pass the app name
addImports([
{
from: resolve(runtimeDir, 'auth/composables'),
name: 'getCurrentUser',
},
])
}
// Emulators must be enabled after the app is initialized but before some APIs like auth.signinWithCustomToken() are called
if (hasEmulatorsEnabled) {
const emulators = detectEmulators(options, hasEmulatorsEnabled, logger)
// expose the detected emulators to the plugins
nuxt.options.runtimeConfig.public.vuefire ??= {}
nuxt.options.runtimeConfig.public.vuefire.emulators = emulators
for (const serviceName in emulators) {
const { host, port } = emulators[serviceName as keyof typeof emulators]
// set the env variables so they are picked up automatically by the admin SDK
process.env[
serviceName === 'firestore'
? 'FIRESTORE_EMULATOR_HOST'
: `FIREBASE_${serviceName.toUpperCase()}_EMULATOR_HOST`
] = `${host}:${port}`
logger.info(`Enabling ${serviceName} emulator at ${host}:${port}`)
addPlugin(resolve(runtimeDir, `emulators/${serviceName}.plugin`))
}
}
// adds the firebase app to each application
addPlugin(resolve(runtimeDir, 'app/plugin.client'))
addPlugin(resolve(runtimeDir, 'app/plugin.server'))
// we start the admin app before the regular app so we can have access to the user uid everywhere
if (options.admin || nuxt.options.ssr) {
if (!nuxt.options.ssr) {
logger.warn(
'The "admin" option is only used during SSR. You should reenable SSR to use it or remove it if you are not doing SSR or SSG.'
)
}
if (hasServiceAccount || hasEmulatorsEnabled) {
if (options.auth) {
// decodes user token from cookie if any
addPlugin(resolve(runtimeDir, 'auth/plugin-user-token.server'))
}
// injects firebaseAdminApp
addPlugin(resolve(runtimeDir, 'admin/plugin.server'))
}
}
// Add auto imports that are useful to be auto imported
// these imports are overridden by nuxt-vuefire to allow being used in more places like plugins and middlewares
addImports([
// app
{
from: resolve(runtimeDir, 'app/composables'),
name: 'useFirebaseApp',
},
])
addVueFireImports([
// firestore
{ from: 'vuefire', name: 'useFirestore' },
{ from: 'vuefire', name: 'useDocument' },
{ from: 'vuefire', name: 'useCollection' },
// database
{ from: 'vuefire', name: 'useDatabase' },
{ from: 'vuefire', name: 'useDatabaseList' },
{ from: 'vuefire', name: 'useDatabaseObject' },
])
},
// workaround for vite
hooks: {
'vite:extendConfig': (viteInlineConfig, env) => {
viteInlineConfig.resolve ??= {}
viteInlineConfig.resolve.dedupe ??= []
const deps = [
// 'vuefire',
// 'nuxt-vuefire',
'firebase',
'firebase/app',
'@firebase/app',
'firebase/firestore',
'@firebase/firestore',
'firebase/auth',
'@firebase/auth',
'@firebase/component',
'firebase-admin/auth',
'firebase-admin/app',
'firebase-admin/app-check',
]
viteInlineConfig.resolve.dedupe.push(...deps)
viteInlineConfig.optimizeDeps ??= {}
viteInlineConfig.optimizeDeps.exclude ??= []
viteInlineConfig.optimizeDeps.exclude.push(...deps)
},
},
})
// just to have autocomplete and errors
type VueFireModuleExportKeys = keyof Awaited<typeof import('vuefire')>
function addVueFireImports(
imports: Array<{
from: 'vuefire'
name: VueFireModuleExportKeys
}>
) {
return addImports(imports)
}
export type {
NuxtVueFireAppCheckOptions,
NuxtVueFireAppCheckOptionsReCaptchaV3,
NuxtVueFireAppCheckOptionsReCaptchaEnterprise,
} from './runtime/app-check'
/**
* Type Extensions
*/
/**
* Augments the Nuxt Runtime Config with the VueFire module options.
*/
interface VueFireRuntimeConfig {
/**
* Runtime config for the VueFire module.
*/
vuefire?: {
/**
* Options passed to the Nuxt VueFire module
* @internal
*/
options?: VueFireNuxtModuleOptions
}
}
interface VueFirePublicRuntimeConfig {
vuefire?: {
/**
* Emulators to enable.
*
* @internal
*/
emulators?: FirebaseEmulatorsToEnable
}
}
interface VueFireAppConfig {
/**
* Firebase config to initialize the app.
* @internal
*/
firebaseConfig: FirebaseOptions
/**
* VueFireNuxt options used within plugins.
* @internal
*/
vuefireOptions: Pick<VueFireNuxtModuleOptions, 'appCheck' | 'auth'>
}
declare module '@nuxt/schema' {
export interface AppConfig extends VueFireAppConfig {}
export interface RuntimeConfig extends VueFireRuntimeConfig {}
export interface PublicRuntimeConfig extends VueFirePublicRuntimeConfig {}
}
// @ts-ignore: #app not found error when building
declare module '#app' {
interface NuxtApp {
/**
* Firebase App instance.
*/
$firebaseApp: FirebaseApp
/**
* Firebase Admin app. Only available on the server.
*/
$firebaseAdminApp: FirebaseAdminApp
}
}
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
/**
* Firebase App instance.
*/
$firebaseApp: FirebaseApp
/**
* Firebase Admin app. Only available on the server.
*/
$firebaseAdminApp: FirebaseAdminApp
}
}