-
Notifications
You must be signed in to change notification settings - Fork 951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
First pass at auto generating sdk configs #7833
base: master
Are you sure you want to change the base?
Changes from 10 commits
c72612c
f740de4
0432624
2eeff9c
5e6bb73
c04efda
3b6022e
4ab3e9f
204f126
2bcb58c
0e67680
022a189
23bf76e
4f9bdfd
d4200f5
e6fa76d
fc20178
1bee4da
45506fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,10 +16,40 @@ import { getOrPromptProject } from "../management/projects"; | |
import { FirebaseError } from "../error"; | ||
import { requireAuth } from "../requireAuth"; | ||
import { logger } from "../logger"; | ||
import { promptOnce } from "../prompt"; | ||
import { promptForDirectory, promptOnce } from "../prompt"; | ||
import { Options } from "../options"; | ||
|
||
function checkForApps(apps: AppMetadata[], appPlatform: AppPlatform): void { | ||
import { getPlatformFromFolder } from "../dataconnect/fileUtils"; | ||
import * as path from "path"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: prefer grouping this with the other external imports at the top of the file. |
||
import { Platform } from "../dataconnect/types"; | ||
import { logBullet, logSuccess } from "../utils"; | ||
import { sdkInit } from "./apps-create"; | ||
import { logError } from "../logError"; | ||
import { PLATFORMS } from "../init/features/dataconnect/sdk"; | ||
export function getSdkOutputPath(appDir: string, platform: Platform): string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above - prefer to put this in a separate file than the command |
||
switch (platform) { | ||
case Platform.ANDROID: | ||
// build.gradle can be in either / or /android/app. We always want to place the google-services.json in /android/app. | ||
// So we check the current directory if it's app, and if so, we'll place it in the current directory, if not, we'll put it in the android/app dir. | ||
// Fallback is just to the current app dir. | ||
if (path.dirname(appDir) !== "app") { | ||
try { | ||
const fileNames = fs.readdirSync(path.join(appDir, "app")); | ||
if (fileNames.includes("build.gradle")) { | ||
appDir = path.join(appDir, "app"); | ||
} | ||
} catch { | ||
// Wasn't able to find app dir. Default to outputting to current directory. | ||
} | ||
} | ||
return path.join(appDir, "google-services.json"); | ||
case Platform.WEB: | ||
return path.join(appDir, "firebase-js-config.json"); | ||
case Platform.IOS: | ||
return path.join(appDir, "GoogleService-Info.plist"); | ||
} | ||
throw new Error("Platform " + platform.toString() + " is not supported yet."); | ||
} | ||
export function checkForApps(apps: AppMetadata[], appPlatform: AppPlatform): void { | ||
if (!apps.length) { | ||
throw new FirebaseError( | ||
`There are no ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}apps ` + | ||
|
@@ -53,6 +83,75 @@ async function selectAppInteractively( | |
}); | ||
} | ||
|
||
export async function getSdkConfig( | ||
options: Options, | ||
appPlatform: AppPlatform, | ||
appId?: string, | ||
): Promise<any> { | ||
if (!appId) { | ||
let projectId = needProjectId(options); | ||
if (options.nonInteractive && !projectId) { | ||
throw new FirebaseError("Must supply app and project ids in non-interactive mode."); | ||
} else if (!projectId) { | ||
const result = await getOrPromptProject(options); | ||
projectId = result.projectId; | ||
} | ||
|
||
const apps = await listFirebaseApps(projectId, appPlatform); | ||
// Fail out early if there's no apps. | ||
checkForApps(apps, appPlatform); | ||
// if there's only one app, we don't need to prompt interactively | ||
if (apps.length === 1) { | ||
// If there's only one, use it. | ||
appId = apps[0].appId; | ||
appPlatform = apps[0].platform; | ||
} else if (options.nonInteractive) { | ||
// If there's > 1 and we're non-interactive, fail. | ||
throw new FirebaseError(`Project ${projectId} has multiple apps, must specify an app id.`); | ||
} else { | ||
// > 1, ask what the user wants. | ||
const appMetadata: AppMetadata = await selectAppInteractively(apps, appPlatform); | ||
appId = appMetadata.appId; | ||
appPlatform = appMetadata.platform; | ||
} | ||
} | ||
|
||
let configData; | ||
const spinner = ora(`Downloading configuration data of your Firebase ${appPlatform} app`).start(); | ||
try { | ||
configData = await getAppConfig(appId, appPlatform); | ||
} catch (err: any) { | ||
spinner.fail(); | ||
throw err; | ||
} | ||
spinner.succeed(); | ||
|
||
return configData; | ||
} | ||
|
||
export async function writeConfigToFile( | ||
filename: string, | ||
nonInteractive: boolean, | ||
fileContents: string, | ||
) { | ||
if (fs.existsSync(filename)) { | ||
if (nonInteractive) { | ||
throw new FirebaseError(`${filename} already exists`); | ||
} | ||
const overwrite = await promptOnce({ | ||
type: "confirm", | ||
default: false, | ||
message: `${filename} already exists. Do you want to overwrite?`, | ||
}); | ||
|
||
if (!overwrite) { | ||
return; | ||
} | ||
} | ||
// TODO(mtewani): Make the call to get the fileContents a part of one of these util fns. | ||
fs.writeFileSync(filename, fileContents); | ||
} | ||
|
||
export const command = new Command("apps:sdkconfig [platform] [appId]") | ||
.description( | ||
"print the Google Services config of a Firebase app. " + | ||
|
@@ -61,51 +160,81 @@ export const command = new Command("apps:sdkconfig [platform] [appId]") | |
.option("-o, --out [file]", "(optional) write config output to a file") | ||
.before(requireAuth) | ||
.action(async (platform = "", appId = "", options: Options): Promise<AppConfigurationData> => { | ||
let appPlatform = getAppPlatform(platform); | ||
|
||
if (!appId) { | ||
let projectId = needProjectId(options); | ||
if (options.nonInteractive && !projectId) { | ||
throw new FirebaseError("Must supply app and project ids in non-interactive mode."); | ||
} else if (!projectId) { | ||
const result = await getOrPromptProject(options); | ||
projectId = result.projectId; | ||
let outputPath: string | undefined = undefined; | ||
if (options.out === undefined) { | ||
// do auto-download | ||
let appDir = process.cwd(); | ||
const config = options.config; | ||
if (!platform) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this ever hit? I don't think AppPlatform.PLATFORM_UNSPECIFIED is falsey (since its a string enum). Feels like this should maybe be |
||
// Detect what platform based on current user | ||
let targetPlatform = await getPlatformFromFolder(appDir); | ||
if (targetPlatform === Platform.NONE) { | ||
// If we aren't in an app directory, ask the user where their app is, and try to autodetect from there. | ||
appDir = await promptForDirectory({ | ||
config, | ||
message: "Where is your app directory?", | ||
}); | ||
targetPlatform = await getPlatformFromFolder(appDir); | ||
} | ||
if (targetPlatform === Platform.NONE || targetPlatform === Platform.MULTIPLE) { | ||
if (targetPlatform === Platform.NONE) { | ||
logBullet(`Couldn't automatically detect app your in directory ${appDir}.`); | ||
} else { | ||
logSuccess(`Detected multiple app platforms in directory ${appDir}`); | ||
// Can only setup one platform at a time, just ask the user | ||
} | ||
targetPlatform = await promptOnce({ | ||
message: "Which platform do you want to set up an SDK for?", | ||
type: "list", | ||
choices: PLATFORMS, | ||
}); | ||
} else if (targetPlatform === Platform.FLUTTER) { | ||
logError( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Double checking this behavior - what happens after this logging? Should we error out/return instead? |
||
`Flutter is not supported by apps:sdkconfig. Please install the flutterfire CLI and run "flutterfire configure" to set up firebase for your flutter app.`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Include a link to the flutterfire getting started docs? |
||
); | ||
} else { | ||
logSuccess(`Detected ${targetPlatform} app in directory ${appDir}`); | ||
} | ||
platform = | ||
targetPlatform === Platform.MULTIPLE | ||
? AppPlatform.PLATFORM_UNSPECIFIED | ||
: (targetPlatform as Platform); | ||
outputPath = getSdkOutputPath(appDir, platform); | ||
} | ||
|
||
const apps = await listFirebaseApps(projectId, appPlatform); | ||
// Fail out early if there's no apps. | ||
checkForApps(apps, appPlatform); | ||
// if there's only one app, we don't need to prompt interactively | ||
if (apps.length === 1) { | ||
// If there's only one, use it. | ||
appId = apps[0].appId; | ||
appPlatform = apps[0].platform; | ||
} else if (options.nonInteractive) { | ||
// If there's > 1 and we're non-interactive, fail. | ||
throw new FirebaseError(`Project ${projectId} has multiple apps, must specify an app id.`); | ||
} else { | ||
// > 1, ask what the user wants. | ||
const appMetadata: AppMetadata = await selectAppInteractively(apps, appPlatform); | ||
appId = appMetadata.appId; | ||
appPlatform = appMetadata.platform; | ||
} | ||
const outputDir = path.dirname(outputPath!); | ||
fs.mkdirpSync(outputDir); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL about mkdirp - we probably ought to use it in quite a few other places |
||
let sdkConfig: any; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If possible, use a stronger type than any here. |
||
while (sdkConfig === undefined) { | ||
try { | ||
sdkConfig = await getSdkConfig(options, getAppPlatform(platform), appId); | ||
} catch (e) { | ||
if ((e as Error).message.includes("associated with this Firebase project")) { | ||
await sdkInit(platform as unknown as AppPlatform, options); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
} | ||
|
||
let configData; | ||
const spinner = ora( | ||
`Downloading configuration data of your Firebase ${appPlatform} app`, | ||
).start(); | ||
try { | ||
configData = await getAppConfig(appId, appPlatform); | ||
} catch (err: any) { | ||
spinner.fail(); | ||
throw err; | ||
} | ||
spinner.succeed(); | ||
const fileInfo = getAppConfigFile(sdkConfig, platform as unknown as AppPlatform); | ||
await writeConfigToFile(outputPath!, options.nonInteractive, fileInfo.fileContents); | ||
|
||
const fileInfo = getAppConfigFile(configData, appPlatform); | ||
if (appPlatform === AppPlatform.WEB) { | ||
fileInfo.sdkConfig = configData; | ||
if (platform === AppPlatform.WEB) { | ||
console.log(` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use logger.log() or logger.info() instead - console.log circumvents some verbosity/VSCode specific log handling we have in place. |
||
How to use your JS SDK Config: | ||
ES Module: | ||
import { initializeApp } from 'firebase/app'; | ||
import json from './firebase-js-config.json'; | ||
initializeApp(json); | ||
// CommonJS Module: | ||
const { initializeApp } = require('firebase/app'); | ||
const json = require('./firebase-js-config.json'); | ||
initializeApp(json);// instead of initializeApp(config); | ||
`); | ||
if (platform === AppPlatform.WEB) { | ||
fileInfo.sdkConfig = sdkConfig; | ||
} | ||
} | ||
|
||
if (options.out === undefined) { | ||
|
@@ -114,24 +243,9 @@ export const command = new Command("apps:sdkconfig [platform] [appId]") | |
} | ||
|
||
const shouldUseDefaultFilename = options.out === true || options.out === ""; | ||
const filename = shouldUseDefaultFilename ? configData.fileName : options.out; | ||
if (fs.existsSync(filename)) { | ||
if (options.nonInteractive) { | ||
throw new FirebaseError(`${filename} already exists`); | ||
} | ||
const overwrite = await promptOnce({ | ||
type: "confirm", | ||
default: false, | ||
message: `${filename} already exists. Do you want to overwrite?`, | ||
}); | ||
|
||
if (!overwrite) { | ||
return configData; | ||
} | ||
} | ||
|
||
fs.writeFileSync(filename, fileInfo.fileContents); | ||
const filename = shouldUseDefaultFilename ? sdkConfig.fileName : options.out; | ||
await writeConfigToFile(filename, options.nonInteractive, fileInfo.fileContents); | ||
logger.info(`App configuration is written in ${filename}`); | ||
|
||
return configData; | ||
return sdkConfig; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would prefer to move this to a separate file - I like to keep these command files as lean as possible and only export the command itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, please use the Options type instead of any here too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we move everything into a new file? Also, we should put that info somewhere, I think other commands have other functions besides just the Command