-
Notifications
You must be signed in to change notification settings - Fork 1k
[FDC] Detect all apps during init dataconnect
#9026
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
base: master
Are you sure you want to change the base?
Changes from 22 commits
0857121
f6bd4dc
06e0041
d18e2c7
bd14600
8fb692e
b6d40df
8993523
32b2ddf
95b2e48
761ec89
4a5b148
f46a001
91b8b30
73aa6a0
cf69843
4d2f2eb
3eb8b30
ee46a5d
cd15f7b
851438c
1e0ebf1
23fa07d
0173bde
8caa946
132cc15
06d5882
d752ace
0ff4595
cef0edd
fbbc229
4d91fa7
38aa32c
bded9d6
2acd3b0
5ee905f
9680728
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) |
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`, "{}"); | ||
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)), | ||
); | ||
}); | ||
}); |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Only kept it because There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/"); | ||
maneesht marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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()); | ||
return { | ||
platform: Platform.WEB, | ||
directory: path.dirname(packageJsonFile), | ||
frameworks: getFrameworksFromPackageJson(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[] { | ||
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)), | ||
); | ||
} | ||
|
||
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); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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~