Skip to content

Commit

Permalink
First pass at auto generating sdk configs (#7833)
Browse files Browse the repository at this point in the history
* First pass at auto generating sdk configs

* Fixed formatting issues

* Removed extra command

* Deleted unnecessary files

* Fixed more linting'

* Removed test assertion

* Fixed formatting

* Updated erros

* Misc

* Updated platforms list

* Undid last changes

* Addressed comments

* Fixed client test

* Driveby type fixing

* missed a spot

* Fixed test

* Fix issue where if a user passes in an empty 'out' parameter, the CLI crashes

* Added intelligent sensing where app should be

* Fixed formatting

* Fixed lint

* Fixed app dir

* Misc

* Wrote tests

* Reverted apps sdkconfig changes

* Fixed formatting

* Small changes

* Revert shrinkwrap changes

* Updated test:management script

* Fixed apps-sdkconfig boolean check

* Fixed more boolean

* Fixed formatting

* Added changelog

* Added new options

* Removed unused var

* Added experimental flag

* Moved apps:init behind a flag

* Added apps:init command

* Removed unnecessary experiments

* Addressed comments

---------

Co-authored-by: Joe Hanley <joehanley@google.com>
  • Loading branch information
maneesht and joehan authored Feb 12, 2025
1 parent c4603eb commit 8cc734b
Show file tree
Hide file tree
Showing 12 changed files with 732 additions and 226 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
- Switched Data Connect from `v1beta` API to `v1` API.
- Added code generation of React hooks for Data Connect
- Genkit init improvements around gcloud login and flow input values.
- Added new command `apps:init` under experimental flag (`appsinit`) that automatically detects what SDK to download and places the file in the corresponding place.
- Removed dependencies on some packages and methods that caused deprecation warnings on Node 22.
- Fixes symbol generation when uploading Unity 6 symbols to Crashlytics. (#7867)
- Fixed SSR issues in Angular 19 by adding support for default and reqHandler exports. (#8145)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"test": "npm run lint:quiet && npm run test:compile && npm run mocha",
"test:client-integration": "bash ./scripts/client-integration-tests/run.sh",
"test:compile": "tsc --project tsconfig.compile.json",
"test:management": "npm run lint:quiet && npm run test:compile && nyc mocha 'src/management/*.spec.{ts,js}'",
"test:dataconnect-deploy": "bash ./scripts/dataconnect-test/run.sh",
"test:dataconnect-emulator": "bash ./scripts/dataconnect-emulator-tests/run.sh",
"test:all-emulators": "npm run test:emulator && npm run test:extensions-emulator && npm run test:import-export && npm run test:storage-emulator-integration",
Expand Down
144 changes: 11 additions & 133 deletions src/commands/apps-create.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as clc from "colorette";
import * as ora from "ora";

import { Command } from "../command";
import { needProjectId } from "../projectUtils";
Expand All @@ -8,42 +7,16 @@ import {
AndroidAppMetadata,
AppMetadata,
AppPlatform,
createAndroidApp,
createIosApp,
createWebApp,
getAppPlatform,
IosAppMetadata,
sdkInit,
SdkInitOptions,
WebAppMetadata,
} from "../management/apps";
import { prompt, promptOnce, Question } from "../prompt";
import { promptOnce } from "../prompt";
import { requireAuth } from "../requireAuth";
import { logger } from "../logger";

const DISPLAY_NAME_QUESTION: Question = {
type: "input",
name: "displayName",
default: "",
message: "What would you like to call your app?",
};

interface CreateFirebaseAppOptions {
project: string;
nonInteractive: boolean;
displayName?: string;
}

interface CreateIosAppOptions extends CreateFirebaseAppOptions {
bundleId?: string;
appStoreId?: string;
}

interface CreateAndroidAppOptions extends CreateFirebaseAppOptions {
packageName: string;
}

interface CreateWebAppOptions extends CreateFirebaseAppOptions {
displayName: string;
}
import { Options } from "../options";

function logPostAppCreationInformation(
appMetadata: IosAppMetadata | AndroidAppMetadata | WebAppMetadata,
Expand Down Expand Up @@ -72,91 +45,10 @@ function logPostAppCreationInformation(
logger.info(` firebase apps:sdkconfig ${appPlatform} ${appMetadata.appId}`);
}

async function initiateIosAppCreation(options: CreateIosAppOptions): Promise<IosAppMetadata> {
if (!options.nonInteractive) {
await prompt(options, [
DISPLAY_NAME_QUESTION,
{
type: "input",
default: "",
name: "bundleId",
message: "Please specify your iOS app bundle ID:",
},
{
type: "input",
default: "",
name: "appStoreId",
message: "Please specify your iOS app App Store ID:",
},
]);
}
if (!options.bundleId) {
throw new FirebaseError("Bundle ID for iOS app cannot be empty");
}

const spinner = ora("Creating your iOS app").start();
try {
const appData = await createIosApp(options.project, {
displayName: options.displayName,
bundleId: options.bundleId,
appStoreId: options.appStoreId,
});
spinner.succeed();
return appData;
} catch (err: unknown) {
spinner.fail();
throw err;
}
}

async function initiateAndroidAppCreation(
options: CreateAndroidAppOptions,
): Promise<AndroidAppMetadata> {
if (!options.nonInteractive) {
await prompt(options, [
DISPLAY_NAME_QUESTION,
{
type: "input",
default: "",
name: "packageName",
message: "Please specify your Android app package name:",
},
]);
}
if (!options.packageName) {
throw new FirebaseError("Package name for Android app cannot be empty");
}

const spinner = ora("Creating your Android app").start();
try {
const appData = await createAndroidApp(options.project, {
displayName: options.displayName,
packageName: options.packageName,
});
spinner.succeed();
return appData;
} catch (err: unknown) {
spinner.fail();
throw err;
}
}

async function initiateWebAppCreation(options: CreateWebAppOptions): Promise<WebAppMetadata> {
if (!options.nonInteractive) {
await prompt(options, [DISPLAY_NAME_QUESTION]);
}
if (!options.displayName) {
throw new FirebaseError("Display name for Web app cannot be empty");
}
const spinner = ora("Creating your Web app").start();
try {
const appData = await createWebApp(options.project, { displayName: options.displayName });
spinner.succeed();
return appData;
} catch (err: unknown) {
spinner.fail();
throw err;
}
interface AppsCreateOptions extends Options {
packageName: string;
bundleId: string;
appStoreId: string;
}

export const command = new Command("apps:create [platform] [displayName]")
Expand All @@ -169,9 +61,9 @@ export const command = new Command("apps:create [platform] [displayName]")
.before(requireAuth)
.action(
async (
platform: string = "",
platform = "",
displayName: string | undefined,
options: any,
options: AppsCreateOptions,
): Promise<AppMetadata> => {
const projectId = needProjectId(options);

Expand All @@ -194,21 +86,7 @@ export const command = new Command("apps:create [platform] [displayName]")

logger.info(`Create your ${appPlatform} app in project ${clc.bold(projectId)}:`);
options.displayName = displayName; // add displayName into options to pass into prompt function
let appData;
switch (appPlatform) {
case AppPlatform.IOS:
appData = await initiateIosAppCreation(options);
break;
case AppPlatform.ANDROID:
appData = await initiateAndroidAppCreation(options);
break;
case AppPlatform.WEB:
appData = await initiateWebAppCreation(options);
break;
default:
throw new FirebaseError("Unexpected error. This should not happen");
}

const appData = await sdkInit(appPlatform, options as SdkInitOptions);
logPostAppCreationInformation(appData, appPlatform);
return appData;
},
Expand Down
122 changes: 122 additions & 0 deletions src/commands/apps-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as fs from "fs-extra";
import * as path from "path";

import { Command } from "../command";
import {
AppConfig,
AppPlatform,
getAppConfigFile,
getAppPlatform,
getPlatform,
getSdkConfig,
getSdkOutputPath,
sdkInit,
writeConfigToFile,
} from "../management/apps";
import { requireAuth } from "../requireAuth";
import { logger } from "../logger";
import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import { Platform } from "../dataconnect/types";
import { assertEnabled } from "../experiments";

export interface AppsInitOptions extends Options {
out?: string | boolean;
}

function logUse(platform: AppPlatform, filePath: string) {
switch (platform) {
case AppPlatform.WEB:
logger.info(`
How to use your JS SDK Config:
ES Module:
import { initializeApp } from 'firebase/app';
import json from './${filePath || "firebase-sdk-config.json"}';
initializeApp(json); // or copy and paste the config directly from the json file here
// CommonJS Module:
const { initializeApp } = require('firebase/app');
const json = require('./firebase-js-config.json');
initializeApp(json); // or copy and paste the config directly from the json file here`);
break;
case AppPlatform.ANDROID:
logger.info(
`Visit https://firebase.google.com/docs/android/setup#add-config-file
for information on editing your gradle file and adding Firebase SDKs to your app.
If you're using Unity or C++, visit https://firebase.google.com/docs/cpp/setup?platform=android#add-config-file
for information about adding your config file to your project.`,
);
break;
case AppPlatform.IOS:
logger.info(
`Visit https://firebase.google.com/docs/ios/setup#add-config-file
for information on adding the config file to your targets and adding Firebase SDKs to your app.
If you're using Unity or C++, visit https://firebase.google.com/docs/cpp/setup?platform=ios#add-config-file
for information about adding your config file to your project.`,
);
break;
}
}

function toAppPlatform(str: string) {
switch (str.toUpperCase()) {
case Platform.ANDROID:
return Platform.ANDROID as unknown as AppPlatform.ANDROID;
case Platform.IOS:
return Platform.IOS as unknown as AppPlatform.IOS;
case Platform.WEB:
return Platform.WEB as unknown as AppPlatform.WEB;
}
throw new Error(`Platform ${str} is not compatible with apps:configure`);
}

export const command = new Command("apps:init [platform] [appId]")
.description("Automatically download and create config of a Firebase app. ")
.before(requireAuth)
.option("-o, --out [file]", "(optional) write config output to a file")
.action(async (platform = "", appId = "", options: AppsInitOptions): Promise<AppConfig> => {
assertEnabled("appsinit", "autoconfigure an app");
if (typeof options.out === "boolean") {
throw new Error("Please specify a file path to output to.");
}
const config = options.config;
const appDir = process.cwd();
// auto-detect the platform
const detectedPlatform = platform ? toAppPlatform(platform) : await getPlatform(appDir, config);

let sdkConfig: AppConfig | undefined;
while (sdkConfig === undefined) {
try {
sdkConfig = await getSdkConfig(options, getAppPlatform(detectedPlatform), appId);
} catch (e) {
if ((e as Error).message.includes("associated with this Firebase project")) {
const projectId = needProjectId(options);
await sdkInit(platform, { ...options, project: projectId });
} else {
throw e;
}
}
}

let outputPath = options.out;

const fileInfo = getAppConfigFile(sdkConfig, detectedPlatform);
let relativePath = "";
outputPath = outputPath || (await getSdkOutputPath(appDir, detectedPlatform, options));
const outputDir = path.dirname(outputPath);
fs.mkdirpSync(outputDir);
relativePath = path.relative(appDir, outputPath);
const written = await writeConfigToFile(
outputPath,
options.nonInteractive,
fileInfo.fileContents,
);

if (written) {
logger.info(`App configuration is written in ${relativePath}`);
}
logUse(detectedPlatform, relativePath);

return sdkConfig;
});
Loading

0 comments on commit 8cc734b

Please sign in to comment.