Skip to content
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

Merged
merged 47 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c72612c
First pass at auto generating sdk configs
maneesht Oct 14, 2024
f740de4
Fixed formatting issues
maneesht Oct 14, 2024
0432624
Removed extra command
maneesht Oct 17, 2024
2eeff9c
Deleted unnecessary files
maneesht Oct 17, 2024
5e6bb73
Fixed more linting'
maneesht Oct 17, 2024
c04efda
Removed test assertion
maneesht Oct 21, 2024
3b6022e
Fixed formatting
maneesht Oct 21, 2024
4ab3e9f
Updated erros
maneesht Oct 21, 2024
204f126
Misc
maneesht Oct 21, 2024
2bcb58c
Updated platforms list
maneesht Oct 21, 2024
0e67680
Undid last changes
maneesht Oct 21, 2024
022a189
Addressed comments
maneesht Oct 21, 2024
23bf76e
Fixed client test
maneesht Oct 22, 2024
4f9bdfd
Driveby type fixing
joehan Oct 22, 2024
d4200f5
Merge branch 'mtewani/auto-apps-first-pass' of github.com:firebase/fi…
joehan Oct 22, 2024
e6fa76d
missed a spot
joehan Oct 22, 2024
fc20178
Fixed test
maneesht Oct 22, 2024
1bee4da
Merge remote-tracking branch 'refs/remotes/public/mtewani/auto-apps-f…
maneesht Oct 22, 2024
45506fe
Merge branch 'master' into mtewani/auto-apps-first-pass
maneesht Oct 22, 2024
bf0a1b3
Merge remote-tracking branch 'public/master' into mtewani/auto-apps-f…
maneesht Feb 4, 2025
c3963be
Fix issue where if a user passes in an empty 'out' parameter, the CLI…
maneesht Feb 4, 2025
40d8397
Added intelligent sensing where app should be
maneesht Feb 5, 2025
db0ac18
Fixed formatting
maneesht Feb 5, 2025
1327522
Fixed lint
maneesht Feb 5, 2025
e95bdad
Fixed app dir
maneesht Feb 5, 2025
b6af1e6
Misc
maneesht Feb 6, 2025
cb3a09e
Merge remote-tracking branch 'origin/master' into mtewani/auto-apps-f…
maneesht Feb 6, 2025
f676ff8
Wrote tests
maneesht Feb 6, 2025
76e7691
Reverted apps sdkconfig changes
maneesht Feb 6, 2025
67fe143
Merge remote-tracking branch 'public/master' into mtewani/auto-apps-f…
maneesht Feb 6, 2025
e9c7280
Fixed formatting
maneesht Feb 6, 2025
4f744df
Small changes
maneesht Feb 6, 2025
5c4af31
Revert shrinkwrap changes
maneesht Feb 6, 2025
f8d1848
Updated test:management script
maneesht Feb 6, 2025
f3cd670
Fixed apps-sdkconfig boolean check
maneesht Feb 6, 2025
45b3f97
Fixed more boolean
maneesht Feb 6, 2025
a37000c
Fixed formatting
maneesht Feb 6, 2025
453ec56
Added changelog
maneesht Feb 6, 2025
09f2ba3
Added new options
maneesht Feb 6, 2025
f5248f1
Removed unused var
maneesht Feb 6, 2025
5ab216f
Added experimental flag
maneesht Feb 12, 2025
9cb95e6
Moved apps:init behind a flag
maneesht Feb 12, 2025
a236f0f
Added apps:init command
maneesht Feb 12, 2025
92bc6d6
Removed unnecessary experiments
maneesht Feb 12, 2025
804e0a7
Fixed changelog
maneesht Feb 12, 2025
7f05bb3
Addressed comments
maneesht Feb 12, 2025
3409f41
Merge branch 'master' into mtewani/auto-apps-first-pass
joehan Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 4 additions & 134 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,44 +7,16 @@
AndroidAppMetadata,
AppMetadata,
AppPlatform,
createAndroidApp,
createIosApp,
createWebApp,
getAppPlatform,
IosAppMetadata,
sdkInit,
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;
}

function logPostAppCreationInformation(
export function logPostAppCreationInformation(

Check warning on line 19 in src/commands/apps-create.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
appMetadata: IosAppMetadata | AndroidAppMetadata | WebAppMetadata,
appPlatform: AppPlatform,
): void {
Expand All @@ -72,93 +43,6 @@
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: any) {
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: any) {
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: any) {
spinner.fail();
throw err;
}
}

export const command = new Command("apps:create [platform] [displayName]")
.description(
"create a new Firebase app. [platform] can be IOS, ANDROID or WEB (case insensitive).",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you're here, could you add a type for options on this command as well?

Expand All @@ -169,14 +53,14 @@
.before(requireAuth)
.action(
async (
platform: string = "",

Check warning on line 56 in src/commands/apps-create.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Type string trivially inferred from a string literal, remove type annotation
displayName: string | undefined,
options: any,

Check warning on line 58 in src/commands/apps-create.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
): Promise<AppMetadata> => {
const projectId = needProjectId(options);

Check warning on line 60 in src/commands/apps-create.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `{ projectId?: string | undefined; project?: string | undefined; rc?: RC | undefined; }`

if (!options.nonInteractive && !platform) {

Check warning on line 62 in src/commands/apps-create.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .nonInteractive on an `any` value
platform = await promptOnce({

Check warning on line 63 in src/commands/apps-create.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
type: "list",
message: "Please choose the platform of the app:",
choices: [
Expand All @@ -193,22 +77,8 @@
}

logger.info(`Create your ${appPlatform} app in project ${clc.bold(projectId)}:`);
options.displayName = displayName; // add displayName into options to pass into prompt function

Check warning on line 80 in src/commands/apps-create.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .displayName on an `any` value
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);
logPostAppCreationInformation(appData, appPlatform);
return appData;
},
Expand Down
171 changes: 63 additions & 108 deletions src/commands/apps-sdkconfig.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,24 @@
import * as ora from "ora";
import * as fs from "fs-extra";

import { Command } from "../command";
import {
AppConfigurationData,
AppMetadata,
AppConfig,
AppPlatform,
getAppConfig,
getAppConfigFile,
getAppPlatform,
listFirebaseApps,
getPlatform,
getSdkConfig,
getSdkOutputPath,
sdkInit,
writeConfigToFile,
} from "../management/apps";
import { needProjectId } from "../projectUtils";
import { getOrPromptProject } from "../management/projects";
import { FirebaseError } from "../error";
import { requireAuth } from "../requireAuth";
import { logger } from "../logger";
import { promptOnce } from "../prompt";
import { Options } from "../options";
import * as path from "path";
Copy link
Contributor

Choose a reason for hiding this comment

The 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.


function checkForApps(apps: AppMetadata[], appPlatform: AppPlatform): void {
if (!apps.length) {
throw new FirebaseError(
`There are no ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}apps ` +
"associated with this Firebase project",
);
}
}

async function selectAppInteractively(
apps: AppMetadata[],
appPlatform: AppPlatform,
): Promise<AppMetadata> {
checkForApps(apps, appPlatform);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const choices = apps.map((app: any) => {
return {
name:
`${app.displayName || app.bundleId || app.packageName}` +
` - ${app.appId} (${app.platform})`,
value: app,
};
});

return await promptOnce({
type: "list",
message:
`Select the ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}` +
"app to get the configuration data:",
choices,
});
interface AppsSdkConfigOptions extends Options {
out?: string | boolean;
}

export const command = new Command("apps:sdkconfig [platform] [appId]")
Expand All @@ -59,79 +27,66 @@
"[platform] can be IOS, ANDROID or WEB (case insensitive)",
)
.option("-o, --out [file]", "(optional) write config output to a file")
// Note: Command behaves weirdly with optional string flags - when `--out`, options.out is a boolean
// but when `--out myFile.json`, options.out is a string
.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;
.action(
async (
platform: AppPlatform = AppPlatform.PLATFORM_UNSPECIFIED,
appId = "",
options: AppsSdkConfigOptions,
): Promise<AppConfig> => {
const config = options.config;
const appDir = process.cwd();
if (!platform) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 if (platform === AppPlatform.PLATFORM_UNSPECIFIED)

// Auto-detect platform based on current directory if not specified
platform = await getPlatform(appDir, config);
}

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 sdkConfig: AppConfig | undefined;
while (sdkConfig === undefined) {
try {
sdkConfig = await getSdkConfig(options, getAppPlatform(platform), appId);

Check warning on line 49 in src/commands/apps-sdkconfig.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `string | undefined`
} catch (e) {
if ((e as Error).message.includes("associated with this Firebase project")) {
await sdkInit(platform as unknown as AppPlatform, options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this type washing is now unnecessary (since you strongly typed platform in all locations afaict)

} 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(configData, appPlatform);
if (appPlatform === AppPlatform.WEB) {
fileInfo.sdkConfig = configData;
}

if (options.out === undefined) {
logger.info(fileInfo.fileContents);
return fileInfo;
}

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`);
let writeToFile = false;
let outputPath: string = "";

Check warning on line 60 in src/commands/apps-sdkconfig.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Type string trivially inferred from a string literal, remove type annotation
if (typeof options.out === "boolean") {
writeToFile = options.out;
outputPath = getSdkOutputPath(appDir, platform);
} else if (typeof options.out === "string") {
writeToFile = true;
outputPath = options.out;
}
const overwrite = await promptOnce({
type: "confirm",
default: false,
message: `${filename} already exists. Do you want to overwrite?`,
});

if (!overwrite) {
return configData;
if (writeToFile) {
const outputDir = path.dirname(outputPath!);

Check warning on line 70 in src/commands/apps-sdkconfig.ts

View workflow job for this annotation

GitHub Actions / lint (20)

This assertion is unnecessary since it does not change the type of the expression
fs.mkdirpSync(outputDir);
const fileInfo = getAppConfigFile(sdkConfig, platform);
await writeConfigToFile(outputPath!, options.nonInteractive, fileInfo.fileContents);
if (platform === AppPlatform.WEB) {
logger.info(`
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);
`);
}
logger.info(`App configuration is written in ${fileInfo}`);
}
}

fs.writeFileSync(filename, fileInfo.fileContents);
logger.info(`App configuration is written in ${filename}`);

return configData;
});
return sdkConfig;
},
);
1 change: 1 addition & 0 deletions src/dataconnect/fileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export async function pickService(
// case insensitive exact match indicators for supported app platforms
const WEB_INDICATORS = ["package.json", "package-lock.json", "node_modules"];
const IOS_INDICATORS = ["info.plist", "podfile", "package.swift"];
// Note: build.gradle can be nested inside android/ and android/app.
const ANDROID_INDICATORS = ["androidmanifest.xml", "build.gradle", "build.gradle.kts"];
const DART_INDICATORS = ["pubspec.yaml", "pubspec.lock"];

Expand Down
Loading
Loading