Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0857121
react template
fredzqm Aug 22, 2025
f6bd4dc
m
fredzqm Aug 22, 2025
06e0041
fix(dataconnect): fix failing tests and improve test coverage
google-labs-jules[bot] Aug 22, 2025
d18e2c7
Merge remote-tracking branch 'origin/fix/dataconnect-tests' into fz/apps
fredzqm Aug 22, 2025
bd14600
commit
fredzqm Aug 22, 2025
8fb692e
tests
fredzqm Aug 22, 2025
b6d40df
tests
fredzqm Aug 22, 2025
8993523
merge
fredzqm Aug 22, 2025
32b2ddf
tests
fredzqm Aug 22, 2025
95b2e48
m
fredzqm Aug 22, 2025
761ec89
changelog
fredzqm Aug 22, 2025
4a5b148
m
fredzqm Aug 22, 2025
f46a001
vite react template commented out
fredzqm Aug 22, 2025
91b8b30
m
fredzqm Aug 22, 2025
73aa6a0
pick between react & next
fredzqm Aug 25, 2025
cf69843
metrics
fredzqm Aug 25, 2025
4d2f2eb
metric
fredzqm Aug 25, 2025
3eb8b30
m
fredzqm Aug 25, 2025
ee46a5d
tests
fredzqm Aug 25, 2025
cd15f7b
m
fredzqm Aug 25, 2025
851438c
m
fredzqm Aug 25, 2025
1e0ebf1
Merge branch 'master' into fz/apps
fredzqm Aug 25, 2025
23fa07d
Merge remote-tracking branch 'origin/master' into fz/apps
fredzqm Aug 25, 2025
0173bde
only trust ENV_VAR when envPlatform is also set
fredzqm Aug 25, 2025
8caa946
Merge branch 'master' into fz/apps
fredzqm Aug 26, 2025
132cc15
Merge remote-tracking branch 'origin/fz/apps' into fz/apps
fredzqm Aug 26, 2025
06d5882
comments
fredzqm Aug 26, 2025
d752ace
warning log
fredzqm Aug 26, 2025
0ff4595
m
fredzqm Aug 26, 2025
cef0edd
Update src/init/features/dataconnect/sdk.ts
fredzqm Aug 26, 2025
fbbc229
m
fredzqm Aug 26, 2025
4d91fa7
Merge remote-tracking branch 'origin/fz/apps' into fz/apps
fredzqm Aug 26, 2025
38aa32c
feedbacks
fredzqm Aug 26, 2025
bded9d6
remove
fredzqm Aug 26, 2025
2acd3b0
Update src/init/features/dataconnect/sdk.ts
fredzqm Aug 27, 2025
5ee905f
Merge remote-tracking branch 'origin/master' into fz/apps
fredzqm Aug 27, 2025
9680728
m
fredzqm Aug 27, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
- Adds additional Crashlytics tools for debugging/analyzing crashes (#9020)
- Improve `firebase init dataconnect` to detect all apps and set up SDKs in platform-specific directories (#9026)
- `firebase init dataconnect` provide an option to create an Next.JS app template (#9026)
4 changes: 4 additions & 0 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@

const setup: Setup = {
config: config.src,
rcfile: config.readProjectFile(".firebaserc", {

Check warning on line 196 in src/commands/init.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
json: true,
fallback: {},
}),
Expand Down Expand Up @@ -252,6 +252,10 @@
if (setup.features.includes("hosting") && setup.features.includes("hosting:github")) {
setup.features = setup.features.filter((f) => f !== "hosting:github");
}
// "dataconnect:sdk" is a part of "dataconnect", so if both are selected, "dataconnect:sdk" is ignored.
if (setup.features.includes("dataconnect") && setup.features.includes("dataconnect:sdk")) {
setup.features = setup.features.filter((f) => f !== "dataconnect:sdk");
}

await init(setup, config, options);

Expand Down
179 changes: 179 additions & 0 deletions src/dataconnect/appFinder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { expect } from "chai";
import * as fs from "fs-extra";
import { getPlatformFromFolder, detectApps } from "./appFinder";
import { Platform } from "./types";

describe("getPlatformFromFolder", () => {
const testDir = "test-dir";

afterEach(() => {
fs.removeSync(testDir);
});

it("should return WEB if package.json exists", async () => {
fs.outputFileSync(`${testDir}/package.json`, "{}");
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 down the line we should add helper functions here, but I'm fine with leaving this as-is until we increase the complexity of these tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😄 All of those tests are generated by Gemini Code Assist or Jules~

const platform = await getPlatformFromFolder(testDir);
expect(platform).to.equal(Platform.WEB);
});

it("should return ANDROID if src/main exists", async () => {
fs.mkdirpSync(`${testDir}/src/main`);
const platform = await getPlatformFromFolder(testDir);
expect(platform).to.equal(Platform.ANDROID);
});

it("should return IOS if .xcodeproj exists", async () => {
fs.mkdirpSync(`${testDir}/a.xcodeproj`);
const platform = await getPlatformFromFolder(testDir);
expect(platform).to.equal(Platform.IOS);
});

it("should return FLUTTER if pubspec.yaml exists", async () => {
fs.outputFileSync(`${testDir}/pubspec.yaml`, "name: test");
const platform = await getPlatformFromFolder(testDir);
expect(platform).to.equal(Platform.FLUTTER);
});

it("should return MULTIPLE if multiple identifiers exist", async () => {
fs.outputFileSync(`${testDir}/package.json`, "{}");
fs.outputFileSync(`${testDir}/pubspec.yaml`, "name: test");
const platform = await getPlatformFromFolder(testDir);
expect(platform).to.equal(Platform.MULTIPLE);
});

it("should return NONE if no identifiers exist", async () => {
fs.mkdirpSync(testDir);
const platform = await getPlatformFromFolder(testDir);
expect(platform).to.equal(Platform.NONE);
});
});

describe("detectApps", () => {
const testDir = "test-dir";

afterEach(() => {
fs.removeSync(testDir);
});

it("should detect a web app", async () => {
fs.outputFileSync(`${testDir}/package.json`, "{}");
const apps = await detectApps(testDir);
expect(apps).to.deep.equal([
{
platform: Platform.WEB,
directory: ".",
frameworks: [],
},
]);
});

it("should detect an android app", async () => {
fs.mkdirpSync(`${testDir}/src/main`);
const apps = await detectApps(testDir);
expect(apps).to.deep.equal([
{
platform: Platform.ANDROID,
directory: ".",
},
]);
});

it("should detect an ios app", async () => {
fs.mkdirpSync(`${testDir}/a.xcodeproj`);
const apps = await detectApps(testDir);
expect(apps).to.deep.equal([
{
platform: Platform.IOS,
directory: ".",
},
]);
});

it("should detect a flutter app", async () => {
fs.outputFileSync(`${testDir}/pubspec.yaml`, "name: test");
const apps = await detectApps(testDir);
expect(apps).to.deep.equal([
{
platform: Platform.FLUTTER,
directory: ".",
},
]);
});

it("should detect multiple apps", async () => {
fs.mkdirpSync(`${testDir}/web`);
fs.outputFileSync(`${testDir}/web/package.json`, "{}");
fs.mkdirpSync(`${testDir}/android/src/main`);
const apps = await detectApps(testDir);
expect(apps).to.deep.equal([
{
platform: Platform.WEB,
directory: `web`,
frameworks: [],
},
{
platform: Platform.ANDROID,
directory: `android`,
},
]);
});

it("should detect web frameworks", async () => {
fs.outputFileSync(
`${testDir}/package.json`,
JSON.stringify({
dependencies: {
react: "1.0.0",
},
}),
);
const apps = await detectApps(testDir);
expect(apps).to.deep.equal([
{
platform: Platform.WEB,
directory: ".",
frameworks: ["react"],
},
]);
});

it("should detect a nested web app", async () => {
fs.mkdirpSync(`${testDir}/frontend`);
fs.outputFileSync(`${testDir}/frontend/package.json`, "{}");
const apps = await detectApps(testDir);
expect(apps).to.deep.equal([
{
platform: Platform.WEB,
directory: "frontend",
frameworks: [],
},
]);
});

it("should detect multiple top-level and nested apps", async () => {
fs.mkdirpSync(`${testDir}/android/src/main`);
fs.mkdirpSync(`${testDir}/ios/a.xcodeproj`);
fs.mkdirpSync(`${testDir}/web/frontend`);
fs.outputFileSync(`${testDir}/web/frontend/package.json`, "{}");

const apps = await detectApps(testDir);
const expected = [
{
platform: Platform.ANDROID,
directory: "android",
},
{
platform: Platform.IOS,
directory: "ios",
},
{
platform: Platform.WEB,
directory: "web/frontend",
frameworks: [],
},
];
expect(apps.sort((a, b) => a.directory.localeCompare(b.directory))).to.deep.equal(
expected.sort((a, b) => a.directory.localeCompare(b.directory)),
);
});
});
99 changes: 99 additions & 0 deletions src/dataconnect/appFinder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as fs from "fs-extra";
import * as path from "path";
import { glob } from "glob";
import { Framework, Platform } from "./types";
import { PackageJSON } from "../frameworks/compose/discover/runtime/node";

export interface App {
platform: Platform;
directory: string;
frameworks?: Framework[];
}

/** Returns a string description of the app */
export function appDescription(a: App): string {
return `${a.directory} (${a.platform.toLowerCase()})`;
}

/** Given a directory, determine the platform type */
export async function getPlatformFromFolder(dirPath: string): Promise<Platform> {
const apps = await detectApps(dirPath);
const hasWeb = apps.some((app) => app.platform === Platform.WEB);
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider, instead, creating a set with all discovered platform values. If the set's size is 0 then return NONE. If its size is 1 then return that platform. Otherwise return multiple. That will be more maintainable as well for when new platforms are added.

Copy link
Contributor Author

@fredzqm fredzqm Aug 26, 2025

Choose a reason for hiding this comment

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

getPlatformFromFolder can go away soon.

Only kept it because management/apps.ts still use it.

Copy link
Contributor

Choose a reason for hiding this comment

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

"can go away soon" -> famous last words for code that will still exist in 10 years

const hasAndroid = apps.some((app) => app.platform === Platform.ANDROID);
const hasIOS = apps.some((app) => app.platform === Platform.IOS);
const hasDart = apps.some((app) => app.platform === Platform.FLUTTER);
if (!hasWeb && !hasAndroid && !hasIOS && !hasDart) {
return Platform.NONE;
} else if (hasWeb && !hasAndroid && !hasIOS && !hasDart) {
return Platform.WEB;
} else if (hasAndroid && !hasWeb && !hasIOS && !hasDart) {
return Platform.ANDROID;
} else if (hasIOS && !hasWeb && !hasAndroid && !hasDart) {
return Platform.IOS;
} else if (hasDart && !hasWeb && !hasIOS && !hasAndroid) {
return Platform.FLUTTER;
}
// At this point, its not clear which platform the app directory is
// because we found indicators for multiple platforms.
return Platform.MULTIPLE;
}

/** Detects the apps in a given directory */
export async function detectApps(dirPath: string): Promise<App[]> {
const packageJsonFiles = await detectFiles(dirPath, "package.json");
const pubSpecYamlFiles = await detectFiles(dirPath, "pubspec.yaml");
const srcMainFolders = await detectFiles(dirPath, "src/main/");
const xCodeProjects = await detectFiles(dirPath, "*.xcodeproj/");
const apps: App[] = [
...(await Promise.all(packageJsonFiles.map((p) => packageJsonToWebApp(dirPath, p)))),
...pubSpecYamlFiles.map((f) => ({ platform: Platform.FLUTTER, directory: path.dirname(f) })),
...srcMainFolders.map((f) => ({
platform: Platform.ANDROID,
directory: path.dirname(path.dirname(f)),
})),
...xCodeProjects.map((f) => ({ platform: Platform.IOS, directory: path.dirname(f) })),
];
return apps;
}

async function packageJsonToWebApp(dirPath: string, packageJsonFile: string): Promise<App> {
const fullPath = path.join(dirPath, packageJsonFile);
const packageJson = JSON.parse((await fs.readFile(fullPath)).toString());

Check warning on line 61 in src/dataconnect/appFinder.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
return {
platform: Platform.WEB,
directory: path.dirname(packageJsonFile),
frameworks: getFrameworksFromPackageJson(packageJson),

Check warning on line 65 in src/dataconnect/appFinder.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `PackageJSON`
};
}

export const WEB_FRAMEWORKS: Framework[] = ["react", "angular"];
export const WEB_FRAMEWORKS_SIGNALS: { [key in Framework]: string[] } = {
react: ["react", "next"],
angular: ["@angular/core"],
};

export function getFrameworksFromPackageJson(packageJson: PackageJSON): Framework[] {

Check warning on line 75 in src/dataconnect/appFinder.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
const devDependencies = Object.keys(packageJson.devDependencies ?? {});
const dependencies = Object.keys(packageJson.dependencies ?? {});
const allDeps = Array.from(new Set([...devDependencies, ...dependencies]));
return WEB_FRAMEWORKS.filter((framework) =>
WEB_FRAMEWORKS_SIGNALS[framework]!.find((dep) => allDeps.includes(dep)),

Check warning on line 80 in src/dataconnect/appFinder.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
);
}

async function detectFiles(dirPath: string, filePattern: string): Promise<string[]> {
const options = {
cwd: dirPath,
ignore: [
"**/dataconnect*/**",
"**/node_modules/**", // Standard dependency directory
"**/dist/**", // Common build output
"**/build/**", // Common build output
"**/out/**", // Another common build output
"**/.next/**", // Next.js build directory
"**/coverage/**", // Test coverage reports
],
absolute: false,
};
return glob(`{${filePattern},*/${filePattern},*/*/${filePattern}}`, options);
}
Loading
Loading