diff --git a/CHANGELOG.md b/CHANGELOG.md index 75fc8270001..ddf954d471e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ -- Fixes bug where `esbuild` execution was throwing an error saying "Command line too long" on Windows (https://github.com/firebase/firebase-tools/issues/7250 and https://github.com/firebase/firebase-tools/issues/6193). +- Fixed bug where `esbuild` execution was throwing an error saying "Command line too long" on Windows (#7250, #6193). +- Automatically detect app platform during `init dataconnect:sdk`. diff --git a/src/dataconnect/fileUtils.ts b/src/dataconnect/fileUtils.ts index ef5a9a9f119..1eb977d12c1 100644 --- a/src/dataconnect/fileUtils.ts +++ b/src/dataconnect/fileUtils.ts @@ -110,7 +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"]; -const ANDROID_INDICATORS = ["androidmanifest.xml", "build.gradle"]; +const ANDROID_INDICATORS = ["androidmanifest.xml", "build.gradle", "build.gradle.kts"]; // endswith match const IOS_INDICATORS_2 = [".xcworkspace", ".xcodeproj"]; @@ -132,3 +132,8 @@ export async function getPlatformFromFolder(dirPath: string) { return Platform.UNDETERMINED; } + +export async function directoryHasPackageJson(dirPath: string) { + const fileNames = await fs.readdir(dirPath); + return fileNames.some((f) => f.toLowerCase() === "package.json"); +} diff --git a/src/dataconnect/types.ts b/src/dataconnect/types.ts index d7076dd7947..fd9fa846058 100644 --- a/src/dataconnect/types.ts +++ b/src/dataconnect/types.ts @@ -135,7 +135,7 @@ export interface Generate { export interface JavascriptSDK { outputDir: string; package: string; - packageJSONDir?: string; + packageJsonDir?: string; } export interface SwiftSDK { diff --git a/src/init/features/dataconnect/sdk.spec.ts b/src/init/features/dataconnect/sdk.spec.ts index 9a2082d628e..0fdd9a2ffc9 100644 --- a/src/init/features/dataconnect/sdk.spec.ts +++ b/src/init/features/dataconnect/sdk.spec.ts @@ -75,5 +75,6 @@ function mockSDKInfo(shouldGenerate: boolean): sdk.SDKInfo { }, }, shouldGenerate, + displayIOSWarning: false, }; } diff --git a/src/init/features/dataconnect/sdk.ts b/src/init/features/dataconnect/sdk.ts index dc56b8e88d4..615d27d4e5c 100644 --- a/src/init/features/dataconnect/sdk.ts +++ b/src/init/features/dataconnect/sdk.ts @@ -1,25 +1,34 @@ import * as yaml from "yaml"; import * as fs from "fs"; import * as clc from "colorette"; +import * as path from "path"; -import { confirm, promptOnce } from "../../../prompt"; -import { readFirebaseJson } from "../../../dataconnect/fileUtils"; +import { confirm, promptForDirectory, promptOnce } from "../../../prompt"; +import { + readFirebaseJson, + getPlatformFromFolder, + directoryHasPackageJson, +} from "../../../dataconnect/fileUtils"; import { Config } from "../../../config"; import { Setup } from "../.."; import { load } from "../../../dataconnect/load"; -import { logger } from "../../../logger"; -import { ConnectorInfo, ConnectorYaml, JavascriptSDK, KotlinSDK } from "../../../dataconnect/types"; +import { + ConnectorInfo, + ConnectorYaml, + JavascriptSDK, + KotlinSDK, + Platform, +} from "../../../dataconnect/types"; import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator"; import { FirebaseError } from "../../../error"; import { camelCase, snakeCase } from "lodash"; +import { logSuccess, logBullet } from "../../../utils"; -const IOS = "ios"; -const WEB = "web"; -const ANDROID = "android"; export type SDKInfo = { connectorYamlContents: string; connectorInfo: ConnectorInfo; shouldGenerate: boolean; + displayIOSWarning: boolean; }; export async function doSetup(setup: Setup, config: Config): Promise { const sdkInfo = await askQuestions(setup, config); @@ -28,6 +37,7 @@ export async function doSetup(setup: Setup, config: Config): Promise { async function askQuestions(setup: Setup, config: Config): Promise { const serviceCfgs = readFirebaseJson(config); + // TODO: This current approach removes comments from YAML files. Consider a different approach that won't. const serviceInfos = await Promise.all( serviceCfgs.map((c) => load(setup.projectId || "", config, c.source)), ); @@ -54,19 +64,38 @@ async function askQuestions(setup: Setup, config: Config): Promise { choices: connectorChoices, }); - let platforms: string[] = []; - while (!platforms.length) { - platforms = await promptOnce({ - message: "Which platforms do you want to set up a generated SDK for?", - type: "checkbox", - choices: [ - { name: "iOS (Swift)", value: IOS }, - { name: "Web (JavaScript)", value: WEB }, - { name: "Androd (Kotlin)", value: ANDROID }, - ], + // First, lets check if we are in a app directory + let targetPlatform: Platform = Platform.UNDETERMINED; + let appDir: string; + const cwdPlatformGuess = await getPlatformFromFolder(process.cwd()); + if (cwdPlatformGuess !== Platform.UNDETERMINED) { + // If we are, we'll use that directory + logSuccess(`Detected ${cwdPlatformGuess} app in current directory ${process.cwd()}`); + targetPlatform = cwdPlatformGuess; + appDir = process.cwd(); + } else { + // If we aren't, ask the user where their app is, and try to autodetect from there + logBullet(`Couldn't automatically detect your app directory.`); + appDir = await promptForDirectory({ + config, + message: "Where is your app directory?", }); - if (!platforms.length) { - logger.info("You must pick at least one platform."); + const platformGuess = await getPlatformFromFolder(appDir); + if (platformGuess !== Platform.UNDETERMINED) { + logSuccess(`Detected ${platformGuess} app in directory ${appDir}`); + targetPlatform = platformGuess; + } else { + // If we still can't autodetect, just ask the user + logBullet("Couldn't automatically detect your app's platform."); + targetPlatform = await promptOnce({ + message: "Which platform do you want to set up a generated SDK for?", + type: "list", + choices: [ + { name: "iOS (Swift)", value: Platform.IOS }, + { name: "Web (JavaScript)", value: Platform.WEB }, + { name: "Android (Kotlin)", value: Platform.ANDROID }, + ], + }); } } @@ -75,53 +104,58 @@ async function askQuestions(setup: Setup, config: Config): Promise { newConnectorYaml.generate = {}; } - if (platforms.includes(IOS)) { - const outputDir = await promptOnce({ - message: `What directory do you want to write your Swift SDK code to? (If not absolute, path will be relative to '${connectorInfo.directory}')`, - type: "input", - default: - newConnectorYaml.generate.swiftSdk?.outputDir || - `./../gensdk/${newConnectorYaml.connectorId}/swift-sdk`, - }); - const pkg = camelCase(newConnectorYaml.connectorId); + let displayIOSWarning = false; + if (targetPlatform === Platform.IOS) { + const outputDir = + newConnectorYaml.generate.swiftSdk?.outputDir || + path.relative( + connectorInfo.directory, + path.join(appDir, `generated/${newConnectorYaml.connectorId}`), + ); + const pkg = + newConnectorYaml.generate.swiftSdk?.package ?? camelCase(newConnectorYaml.connectorId); const swiftSdk = { outputDir, package: pkg }; newConnectorYaml.generate.swiftSdk = swiftSdk; + displayIOSWarning = true; } - if (platforms.includes(WEB)) { - const outputDir = await promptOnce({ - message: `What directory do you want to write your JavaScript SDK code to? (If not absolute, path will be relative to '${connectorInfo.directory}')`, - type: "input", - default: - newConnectorYaml.generate.javascriptSdk?.outputDir || - `./../gensdk/${newConnectorYaml.connectorId}/javascript-sdk`, - }); + + if (targetPlatform === Platform.WEB) { + const outputDir = + newConnectorYaml.generate.javascriptSdk?.outputDir || + path.relative( + connectorInfo.directory, + path.join(appDir, `generated/${newConnectorYaml.connectorId}`), + ); const pkg = newConnectorYaml.generate.javascriptSdk?.package ?? `@firebasegen/${connectorInfo.connectorYaml.connectorId}`; - const packageJSONDir = await promptOnce({ - message: - "Which directory contains the package.json that you would like to add the JavaScript SDK dependency to? (Leave blank to skip)", - type: "input", - default: newConnectorYaml.generate.javascriptSdk?.packageJSONDir, - }); - // ../.. since we ask relative to connector.yaml + const javascriptSdk: JavascriptSDK = { outputDir, package: pkg, }; - if (packageJSONDir) { - javascriptSdk.packageJSONDir = packageJSONDir; + + if ( + (await directoryHasPackageJson(appDir)) && + (await confirm({ + message: "Would you like to add a dependency on the generated SDK to your package.json?", + })) + ) { + javascriptSdk.packageJsonDir = path.relative(connectorInfo.directory, appDir); } newConnectorYaml.generate.javascriptSdk = javascriptSdk; } - if (platforms.includes(ANDROID)) { - const outputDir = await promptOnce({ - message: `What directory do you want to write your Kotlin SDK code to? (If not absolute, path will be relative to '${connectorInfo.directory}')`, - type: "input", - default: - newConnectorYaml.generate.kotlinSdk?.outputDir || - `./../gensdk/${newConnectorYaml.connectorId}/kotlin-sdk`, - }); + + if (targetPlatform === Platform.ANDROID) { + // app/src/main is a common practice for Andorid, but not explicitly required. + // If it is present, we'll use it. Otherwise, we fall back to the app directory. + const baseDir = fs.existsSync(path.join(appDir, "app/src/main")) + ? path.join(appDir, "app/src/main") + : appDir; + + const outputDir = + newConnectorYaml.generate.kotlinSdk?.outputDir || + path.relative(connectorInfo.directory, path.join(baseDir, `generated`)); const pkg = newConnectorYaml.generate.kotlinSdk?.package ?? `connectors.${snakeCase(connectorInfo.connectorYaml.connectorId)}`; @@ -141,18 +175,26 @@ async function askQuestions(setup: Setup, config: Config): Promise { ); // TODO: Prompt user about adding generated paths to .gitignore const connectorYamlContents = yaml.stringify(newConnectorYaml); - return { connectorYamlContents, connectorInfo, shouldGenerate }; + connectorInfo.connectorYaml = newConnectorYaml; + return { connectorYamlContents, connectorInfo, shouldGenerate, displayIOSWarning }; } export async function actuate(sdkInfo: SDKInfo, projectId?: string) { const connectorYamlPath = `${sdkInfo.connectorInfo.directory}/connector.yaml`; fs.writeFileSync(connectorYamlPath, sdkInfo.connectorYamlContents, "utf8"); - logger.info(`Wrote new config to ${connectorYamlPath}`); + logBullet(`Wrote new config to ${connectorYamlPath}`); if (projectId && sdkInfo.shouldGenerate) { await DataConnectEmulator.generate({ configDir: sdkInfo.connectorInfo.directory, connectorId: sdkInfo.connectorInfo.connectorYaml.connectorId, }); - logger.info(`Generated SDK code for ${sdkInfo.connectorInfo.connectorYaml.connectorId}`); + logBullet(`Generated SDK code for ${sdkInfo.connectorInfo.connectorYaml.connectorId}`); + } + if (sdkInfo.connectorInfo.connectorYaml.generate?.swiftSdk && sdkInfo.displayIOSWarning) { + logBullet( + clc.bold( + "Please follow the instructions here to add your generated sdk to your XCode project:\n\thttps://firebase.google.com/docs/data-connect/gp/ios-sdk#set-client", + ), + ); } } diff --git a/src/prompt.ts b/src/prompt.ts index a6b18e59d17..73a91fc9c33 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -1,7 +1,10 @@ import * as inquirer from "inquirer"; import AutocompletePrompt from "inquirer-autocomplete-prompt"; +import { fileExistsSync, dirExistsSync } from "./fsutils"; import { FirebaseError } from "./error"; +import { Config } from "./config"; +import { logger } from "./logger"; declare module "inquirer" { interface QuestionMap { @@ -131,3 +134,32 @@ export async function confirm(args: { return true; } } + +/** + * Prompts for a directory name, and reprompts if that path does not exist + */ +export async function promptForDirectory(args: { + message: string; + config: Config; + default?: boolean; + relativeTo?: string; +}): Promise { + let dir: string = ""; + while (!dir) { + const target = args.config.path( + await promptOnce({ + message: "Where is your app directory?", + }), + ); + if (fileExistsSync(target)) { + logger.error( + `Expected a directory, but ${target} is a file. Please provide a path to a directory.`, + ); + } else if (!dirExistsSync(target)) { + logger.error(`Directory ${target} not found. Please provide a path to a directory`); + } else { + dir = target; + } + } + return dir; +}