Skip to content

Commit

Permalink
Added Generated React Bindings for FDC (#8158)
Browse files Browse the repository at this point in the history
  • Loading branch information
maneesht authored Feb 6, 2025
1 parent 8383fbb commit 36b6677
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- Added code generation of React hooks for Data Connect
- Genkit init improvements around gcloud login and flow input values.
2 changes: 1 addition & 1 deletion firebase-vscode/src/data-connect/sdk-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
57 changes: 44 additions & 13 deletions src/dataconnect/fileUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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: {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
39 changes: 38 additions & 1 deletion src/dataconnect/fileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down Expand Up @@ -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<PackageJSON | undefined> {
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)[];
}
6 changes: 5 additions & 1 deletion src/dataconnect/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
51 changes: 45 additions & 6 deletions src/init/features/dataconnect/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -16,6 +22,7 @@ import {
JavascriptSDK,
KotlinSDK,
Platform,
SupportedFrameworks,
} from "../../../dataconnect/types";
import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator";
import { FirebaseError } from "../../../error";
Expand Down Expand Up @@ -101,12 +108,35 @@ async function askQuestions(setup: Setup, config: Config): Promise<SDKInfo> {
});

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);
Expand All @@ -115,12 +145,12 @@ async function askQuestions(setup: Setup, config: Config): Promise<SDKInfo> {
return { connectorYamlContents, connectorInfo, displayIOSWarning };
}

export function generateSdkYaml(
export async function generateSdkYaml(
targetPlatform: Platform,
connectorYaml: ConnectorYaml,
connectorDir: string,
appDir: string,
): ConnectorYaml {
): Promise<ConnectorYaml> {
if (!connectorYaml.generate) {
connectorYaml.generate = {};
}
Expand All @@ -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;
}

Expand Down

0 comments on commit 36b6677

Please sign in to comment.