From 36b6677b0d8500686bbbba20baec3c8821e12be4 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Thu, 6 Feb 2025 10:33:07 -0800 Subject: [PATCH] Added Generated React Bindings for FDC (#8158) --- CHANGELOG.md | 1 + .../src/data-connect/sdk-generation.ts | 2 +- src/dataconnect/fileUtils.spec.ts | 57 ++++++++++++++----- src/dataconnect/fileUtils.ts | 39 ++++++++++++- src/dataconnect/types.ts | 6 +- src/init/features/dataconnect/sdk.ts | 51 +++++++++++++++-- 6 files changed, 134 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe82e30401..77cdec55372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ +- Added code generation of React hooks for Data Connect - Genkit init improvements around gcloud login and flow input values. diff --git a/firebase-vscode/src/data-connect/sdk-generation.ts b/firebase-vscode/src/data-connect/sdk-generation.ts index 980c46f65d2..5144e39d002 100644 --- a/firebase-vscode/src/data-connect/sdk-generation.ts +++ b/firebase-vscode/src/data-connect/sdk-generation.ts @@ -168,7 +168,7 @@ export function registerFdcSdkGeneration( vscode.commands.executeCommand("fdc.init-sdk", { appFolder }); } else { // generate yaml - const newConnectorYaml = generateSdkYaml( + const newConnectorYaml = await generateSdkYaml( platform, connectorYaml, connectorYamlFolderPath, diff --git a/src/dataconnect/fileUtils.spec.ts b/src/dataconnect/fileUtils.spec.ts index bc0cac648ff..579c5623c10 100644 --- a/src/dataconnect/fileUtils.spec.ts +++ b/src/dataconnect/fileUtils.spec.ts @@ -1,7 +1,7 @@ import * as mockfs from "mock-fs"; import { expect } from "chai"; -import { getPlatformFromFolder } from "./fileUtils"; +import { getPlatformFromFolder, SUPPORTED_FRAMEWORKS } from "./fileUtils"; import { generateSdkYaml } from "../init/features/dataconnect/sdk"; import { ConnectorYaml, Platform } from "./types"; import FileSystem from "mock-fs/lib/filesystem"; @@ -192,9 +192,9 @@ describe("generateSdkYaml", () => { }, ]; for (const c of cases) { - it(c.desc, () => { + it(c.desc, async () => { mockfs({ [appFolderDetectable]: { ["package.json"]: "{}" } }); - const modifiedYaml = generateSdkYaml( + const modifiedYaml = await generateSdkYaml( Platform.WEB, sampleConnectorYaml, connectorYamlFolder, @@ -204,6 +204,37 @@ describe("generateSdkYaml", () => { }); } }); + for (const f of SUPPORTED_FRAMEWORKS) { + describe(`Check support for ${f} framework`, () => { + const cases = [ + { + desc: `can detect a ${f}`, + depName: f, + detect: true, + }, + { + desc: `can detect not ${f}`, + depName: `not-${f}`, + }, + ]; + for (const c of cases) { + it(c.desc, async () => { + mockfs({ + [appFolderDetectable]: { + ["package.json"]: `{"dependencies": {"${c.depName}": "1"}}`, + }, + }); + const modifiedYaml = await generateSdkYaml( + Platform.WEB, + sampleConnectorYaml, + connectorYamlFolder, + appFolderDetectable, + ); + expect(modifiedYaml.generate?.javascriptSdk?.[f]).to.equal(c.detect); + }); + } + }); + } describe("IOS platform should add Swift SDK Generation", () => { const cases: { @@ -237,8 +268,8 @@ describe("generateSdkYaml", () => { }, ]; for (const c of cases) { - it(c.desc, () => { - const modifiedYaml = generateSdkYaml( + it(c.desc, async () => { + const modifiedYaml = await generateSdkYaml( Platform.IOS, sampleConnectorYaml, connectorYamlFolder, @@ -308,14 +339,14 @@ describe("generateSdkYaml", () => { }, ]; for (const c of cases) { - it(c.desc, () => { + it(c.desc, async () => { mockfs({ [appFolderHasJava + "/app/src/main/java"]: {}, [appFolderHasKotlin + "/app/src/main/kotlin"]: {}, [appFolderHasBoth + "/app/src/main/java"]: {}, [appFolderHasBoth + "/app/src/main/kotlin"]: {}, }); - const modifiedYaml = generateSdkYaml( + const modifiedYaml = await generateSdkYaml( Platform.ANDROID, sampleConnectorYaml, connectorYamlFolder, @@ -358,8 +389,8 @@ describe("generateSdkYaml", () => { }, ]; for (const c of cases) { - it(c.desc, () => { - const modifiedYaml = generateSdkYaml( + it(c.desc, async () => { + const modifiedYaml = await generateSdkYaml( Platform.FLUTTER, sampleConnectorYaml, connectorYamlFolder, @@ -370,9 +401,9 @@ describe("generateSdkYaml", () => { } }); - it("should create generate object if it doesn't exist", () => { + it("should create generate object if it doesn't exist", async () => { const yamlWithoutGenerate: ConnectorYaml = { connectorId: "default-connector" }; - const modifiedYaml = generateSdkYaml( + const modifiedYaml = await generateSdkYaml( Platform.WEB, yamlWithoutGenerate, connectorYamlFolder, @@ -381,9 +412,9 @@ describe("generateSdkYaml", () => { expect(modifiedYaml.generate).to.exist; }); - it("should not modify yaml for unknown platforms", () => { + it("should not modify yaml for unknown platforms", async () => { const unknownPlatform = "unknown" as Platform; // Type assertion for test - const modifiedYaml = generateSdkYaml( + const modifiedYaml = await generateSdkYaml( unknownPlatform, sampleConnectorYaml, connectorYamlFolder, diff --git a/src/dataconnect/fileUtils.ts b/src/dataconnect/fileUtils.ts index ce854c15ca2..d7000fc7423 100644 --- a/src/dataconnect/fileUtils.ts +++ b/src/dataconnect/fileUtils.ts @@ -2,11 +2,19 @@ import * as fs from "fs-extra"; import * as path from "path"; import { FirebaseError } from "../error"; -import { ConnectorYaml, DataConnectYaml, File, Platform, ServiceInfo } from "./types"; +import { + ConnectorYaml, + DataConnectYaml, + File, + Platform, + ServiceInfo, + SupportedFrameworks, +} from "./types"; import { readFileFromDirectory, wrappedSafeLoad } from "../utils"; import { Config } from "../config"; import { DataConnectMultiple } from "../firebaseConfig"; import { load } from "./load"; +import { PackageJSON } from "../frameworks/compose/discover/runtime/node"; export function readFirebaseJson(config?: Config): DataConnectMultiple { if (!config?.has("dataconnect")) { @@ -152,3 +160,32 @@ export async function getPlatformFromFolder(dirPath: string) { // because we found indicators for multiple platforms. return Platform.MULTIPLE; } + +export async function resolvePackageJson( + packageJsonPath: string, +): Promise { + let validPackageJsonPath = packageJsonPath; + if (!packageJsonPath.endsWith("package.json")) { + validPackageJsonPath = path.join(packageJsonPath, "package.json"); + } + validPackageJsonPath = path.resolve(validPackageJsonPath); + try { + return JSON.parse((await fs.readFile(validPackageJsonPath)).toString()); + } catch { + return undefined; + } +} + +export const SUPPORTED_FRAMEWORKS: (keyof SupportedFrameworks)[] = ["react"]; +export function getFrameworksFromPackageJson( + packageJson: PackageJSON, +): (keyof SupportedFrameworks)[] { + const devDependencies = Object.keys(packageJson.devDependencies ?? {}); + const dependencies = Object.keys(packageJson.dependencies ?? {}); + const matched = new Set( + [...devDependencies, ...dependencies].filter((dep) => + SUPPORTED_FRAMEWORKS.includes(dep as keyof SupportedFrameworks), + ), + ); + return Array.from(matched) as (keyof SupportedFrameworks)[]; +} diff --git a/src/dataconnect/types.ts b/src/dataconnect/types.ts index ddd9c6e9e3b..798dd497dbf 100644 --- a/src/dataconnect/types.ts +++ b/src/dataconnect/types.ts @@ -146,7 +146,11 @@ export interface Generate { dartSdk?: DartSDK; } -export interface JavascriptSDK { +export interface SupportedFrameworks { + react?: boolean; +} + +export interface JavascriptSDK extends SupportedFrameworks { outputDir: string; package: string; packageJsonDir?: string; diff --git a/src/init/features/dataconnect/sdk.ts b/src/init/features/dataconnect/sdk.ts index 54b4ab38925..bce8a0ecd85 100644 --- a/src/init/features/dataconnect/sdk.ts +++ b/src/init/features/dataconnect/sdk.ts @@ -4,8 +4,14 @@ import * as clc from "colorette"; import * as path from "path"; import { dirExistsSync } from "../../../fsutils"; -import { promptForDirectory, promptOnce } from "../../../prompt"; -import { readFirebaseJson, getPlatformFromFolder } from "../../../dataconnect/fileUtils"; +import { promptForDirectory, promptOnce, prompt } from "../../../prompt"; +import { + readFirebaseJson, + getPlatformFromFolder, + getFrameworksFromPackageJson, + resolvePackageJson, + SUPPORTED_FRAMEWORKS, +} from "../../../dataconnect/fileUtils"; import { Config } from "../../../config"; import { Setup } from "../.."; import { load } from "../../../dataconnect/load"; @@ -16,6 +22,7 @@ import { JavascriptSDK, KotlinSDK, Platform, + SupportedFrameworks, } from "../../../dataconnect/types"; import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator"; import { FirebaseError } from "../../../error"; @@ -101,12 +108,35 @@ async function askQuestions(setup: Setup, config: Config): Promise { }); const connectorYaml = JSON.parse(JSON.stringify(connectorInfo.connectorYaml)) as ConnectorYaml; - const newConnectorYaml = generateSdkYaml( + const newConnectorYaml = await generateSdkYaml( targetPlatform, connectorYaml, connectorInfo.directory, appDir, ); + if (targetPlatform === Platform.WEB) { + const unusedFrameworks = SUPPORTED_FRAMEWORKS.filter( + (framework) => newConnectorYaml!.generate?.javascriptSdk![framework], + ); + if (unusedFrameworks.length > 0) { + const additionalFrameworks: { features: (keyof SupportedFrameworks)[] } = await prompt( + setup, + [ + { + type: "checkbox", + name: "features", + message: + "Which framework would you like to generate SDKs for? " + + "Press Space to select features, then Enter to confirm your choices.", + choices: unusedFrameworks, + }, + ], + ); + for (const framework of additionalFrameworks.features) { + newConnectorYaml!.generate!.javascriptSdk![framework] = true; + } + } + } // TODO: Prompt user about adding generated paths to .gitignore const connectorYamlContents = yaml.stringify(newConnectorYaml); @@ -115,12 +145,12 @@ async function askQuestions(setup: Setup, config: Config): Promise { return { connectorYamlContents, connectorInfo, displayIOSWarning }; } -export function generateSdkYaml( +export async function generateSdkYaml( targetPlatform: Platform, connectorYaml: ConnectorYaml, connectorDir: string, appDir: string, -): ConnectorYaml { +): Promise { if (!connectorYaml.generate) { connectorYaml.generate = {}; } @@ -135,14 +165,23 @@ export function generateSdkYaml( if (targetPlatform === Platform.WEB) { const pkg = `${connectorYaml.connectorId}-connector`; + const packageJsonDir = path.relative(connectorDir, appDir); const javascriptSdk: JavascriptSDK = { outputDir: path.relative(connectorDir, path.join(appDir, `dataconnect-generated/js/${pkg}`)), package: `@firebasegen/${pkg}`, // If appDir has package.json, Emulator would add Generated JS SDK to `package.json`. // Otherwise, emulator would ignore it. Always add it here in case `package.json` is added later. // TODO: Explore other platforms that can be automatically installed. Dart? Android? - packageJsonDir: path.relative(connectorDir, appDir), + packageJsonDir, }; + const packageJson = await resolvePackageJson(appDir); + if (packageJson) { + const frameworksUsed = getFrameworksFromPackageJson(packageJson); + for (const framework of frameworksUsed) { + javascriptSdk[framework] = true; + } + } + connectorYaml.generate.javascriptSdk = javascriptSdk; }