firebase-tools
Version:
Command-Line Interface for Firebase
349 lines (348 loc) • 16.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.addSdkGenerateToConnectorYaml = exports.initAppCounters = exports.actuate = exports.chooseApp = exports.askQuestions = exports.FDC_SDK_PLATFORM_ENV = exports.FDC_SDK_FRAMEWORKS_ENV = exports.FDC_APP_FOLDER = void 0;
const yaml = require("yaml");
const clc = require("colorette");
const path = require("path");
const cwd = process.cwd();
const prompt_1 = require("../../../prompt");
const load_1 = require("../../../dataconnect/load");
const error_1 = require("../../../error");
const lodash_1 = require("lodash");
const utils_1 = require("../../../utils");
const appUtils_1 = require("../../../appUtils");
const dataconnectEmulator_1 = require("../../../emulator/dataconnectEmulator");
const auth_1 = require("../../../auth");
const create_app_1 = require("./create_app");
const track_1 = require("../../../track");
const fsutils_1 = require("../../../fsutils");
const cloudbilling_1 = require("../../../gcp/cloudbilling");
exports.FDC_APP_FOLDER = "FDC_APP_FOLDER";
exports.FDC_SDK_FRAMEWORKS_ENV = "FDC_SDK_FRAMEWORKS";
exports.FDC_SDK_PLATFORM_ENV = "FDC_SDK_PLATFORM";
async function askQuestions(setup) {
const info = {
apps: [],
};
info.apps = await chooseApp();
if (!info.apps.length) {
const npxMissingWarning = (0, utils_1.commandExistsSync)("npx")
? ""
: clc.yellow(" (you need to install Node.js first)");
const flutterMissingWarning = (0, utils_1.commandExistsSync)("flutter")
? ""
: clc.yellow(" (you need to install Flutter first)");
const choice = await (0, prompt_1.select)({
message: `Do you want to create an app template?`,
choices: [
{ name: `React${npxMissingWarning}`, value: "react" },
{ name: `Next.JS${npxMissingWarning}`, value: "next" },
{ name: `Flutter${flutterMissingWarning}`, value: "flutter" },
{ name: "skip", value: "skip" },
],
});
try {
switch (choice) {
case "react":
await (0, create_app_1.createReactApp)((0, utils_1.newUniqueId)("web-app", (0, fsutils_1.listFiles)(cwd)));
break;
case "next":
await (0, create_app_1.createNextApp)((0, utils_1.newUniqueId)("web-app", (0, fsutils_1.listFiles)(cwd)));
break;
case "flutter":
await (0, create_app_1.createFlutterApp)((0, utils_1.newUniqueId)("flutter_app", (0, fsutils_1.listFiles)(cwd)));
break;
case "skip":
break;
}
}
catch (err) {
(0, utils_1.logLabeledError)("dataconnect", `Failed to create a ${choice} app template`);
}
}
setup.featureInfo = setup.featureInfo || {};
setup.featureInfo.dataconnectSdk = info;
}
exports.askQuestions = askQuestions;
async function chooseApp() {
let apps = dedupeAppsByPlatformAndDirectory(await (0, appUtils_1.detectApps)(cwd));
if (apps.length) {
(0, utils_1.logLabeledSuccess)("dataconnect", `Detected existing apps ${apps.map((a) => (0, appUtils_1.appDescription)(a)).join(", ")}`);
}
else {
(0, utils_1.logLabeledWarning)("dataconnect", "Cannot detect an existing app in the current directory.");
}
const envAppFolder = (0, utils_1.envOverride)(exports.FDC_APP_FOLDER, "");
const envPlatform = (0, utils_1.envOverride)(exports.FDC_SDK_PLATFORM_ENV, "");
const envFrameworks = (0, utils_1.envOverride)(exports.FDC_SDK_FRAMEWORKS_ENV, "")
.split(",")
.filter((f) => !!f)
.map((f) => f);
if (envAppFolder && envPlatform) {
const envAppRelDir = path.relative(cwd, path.resolve(cwd, envAppFolder));
const matchedApps = apps.filter((app) => app.directory === envAppRelDir && (!app.platform || app.platform === envPlatform));
if (matchedApps.length) {
for (const a of matchedApps) {
a.frameworks = [...(a.frameworks || []), ...envFrameworks];
}
return matchedApps;
}
return [
{
platform: envPlatform,
directory: envAppRelDir,
frameworks: envFrameworks,
},
];
}
if (apps.length >= 2) {
const choices = apps.map((a) => {
return {
name: (0, appUtils_1.appDescription)(a),
value: a,
checked: a.directory === ".",
};
});
const pickedApps = await (0, prompt_1.checkbox)({
message: "Which apps do you want to set up Data Connect SDKs in?",
choices,
});
if (!pickedApps || !pickedApps.length) {
throw new error_1.FirebaseError("Command Aborted. Please choose at least one app.");
}
apps = pickedApps;
}
return apps;
}
exports.chooseApp = chooseApp;
async function actuate(setup, config) {
var _a, _b, _c;
const sdkInfo = (_a = setup.featureInfo) === null || _a === void 0 ? void 0 : _a.dataconnectSdk;
if (!sdkInfo) {
throw new Error("Data Connect SDK feature RequiredInfo is not provided");
}
const startTime = Date.now();
try {
await actuateWithInfo(setup, config, sdkInfo);
}
finally {
const fdcInfo = (_b = setup.featureInfo) === null || _b === void 0 ? void 0 : _b.dataconnect;
if (!fdcInfo) {
const source = ((_c = setup.featureInfo) === null || _c === void 0 ? void 0 : _c.dataconnectSource) || "init_sdk";
void (0, track_1.trackGA4)("dataconnect_init", Object.assign({ source, project_status: setup.projectId
? (await (0, cloudbilling_1.isBillingEnabled)(setup))
? "blaze"
: "spark"
: "missing" }, initAppCounters(sdkInfo)), Date.now() - startTime);
}
}
}
exports.actuate = actuate;
function initAppCounters(info) {
var _a;
const counts = {
num_web_apps: 0,
num_android_apps: 0,
num_ios_apps: 0,
num_flutter_apps: 0,
num_admin_node_apps: 0,
};
for (const app of (_a = info.apps) !== null && _a !== void 0 ? _a : []) {
switch (app.platform) {
case appUtils_1.Platform.ADMIN_NODE:
counts.num_admin_node_apps++;
break;
case appUtils_1.Platform.WEB:
counts.num_web_apps++;
break;
case appUtils_1.Platform.ANDROID:
counts.num_android_apps++;
break;
case appUtils_1.Platform.IOS:
counts.num_ios_apps++;
break;
case appUtils_1.Platform.FLUTTER:
counts.num_flutter_apps++;
break;
}
}
return counts;
}
exports.initAppCounters = initAppCounters;
async function actuateWithInfo(setup, config, info) {
if (!info.apps.length) {
info.apps = await (0, appUtils_1.detectApps)(cwd);
if (!info.apps.length) {
(0, utils_1.logLabeledBullet)("dataconnect", "No apps to setup Data Connect Generated SDKs");
return;
}
}
const apps = dedupeAppsByPlatformAndDirectory(info.apps);
const connectorInfo = await chooseExistingConnector(setup, config);
const connectorYaml = JSON.parse(JSON.stringify(connectorInfo.connectorYaml));
for (const app of apps) {
if (!(0, fsutils_1.dirExistsSync)(app.directory)) {
(0, utils_1.logLabeledWarning)("dataconnect", `App directory ${app.directory} does not exist`);
}
addSdkGenerateToConnectorYaml(connectorInfo, connectorYaml, app);
}
const connectorYamlContents = yaml.stringify(connectorYaml);
connectorInfo.connectorYaml = connectorYaml;
const connectorYamlPath = `${connectorInfo.directory}/connector.yaml`;
config.writeProjectFile(path.relative(config.projectDir, connectorYamlPath), connectorYamlContents);
(0, utils_1.logLabeledBullet)("dataconnect", `Installing the generated SDKs ...`);
const account = (0, auth_1.getGlobalDefaultAccount)();
try {
await dataconnectEmulator_1.DataConnectEmulator.generate({
configDir: connectorInfo.directory,
account,
});
}
catch (e) {
(0, utils_1.logLabeledError)("dataconnect", `Failed to generate Data Connect SDKs\n${e === null || e === void 0 ? void 0 : e.message}`);
}
(0, utils_1.logLabeledSuccess)("dataconnect", `Installed generated SDKs for ${clc.bold(apps.map((a) => (0, appUtils_1.appDescription)(a)).join(", "))}`);
if (apps.some((a) => a.platform === appUtils_1.Platform.IOS)) {
(0, utils_1.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/ios-sdk#set-client"));
}
if (apps.some((a) => { var _a; return (_a = a.frameworks) === null || _a === void 0 ? void 0 : _a.includes(appUtils_1.Framework.REACT); })) {
(0, utils_1.logBullet)("Visit https://firebase.google.com/docs/data-connect/web-sdk#react for more information on how to set up React Generated SDKs for Firebase Data Connect");
}
if (apps.some((a) => { var _a; return (_a = a.frameworks) === null || _a === void 0 ? void 0 : _a.includes(appUtils_1.Framework.ANGULAR); })) {
(0, utils_1.logBullet)("Run `ng add @angular/fire` to install angular sdk dependencies.\nVisit https://github.com/invertase/tanstack-query-firebase/tree/main/packages/angular for more information on how to set up Angular Generated SDKs for Firebase Data Connect");
}
}
async function chooseExistingConnector(setup, config) {
const serviceInfos = await (0, load_1.loadAll)(setup.projectId || "", config);
const choices = serviceInfos
.map((si) => {
return si.connectorInfo.map((ci) => {
return {
name: `${si.dataConnectYaml.location}/${si.dataConnectYaml.serviceId}/${ci.connectorYaml.connectorId}`,
value: ci,
};
});
})
.flat();
if (!choices.length) {
throw new error_1.FirebaseError(`No Firebase Data Connect workspace found. Run ${clc.bold("firebase init dataconnect")} to set up a service and connector.`);
}
if (choices.length === 1) {
return choices[0].value;
}
const connectorEnvVar = (0, utils_1.envOverride)("FDC_CONNECTOR", "");
if (connectorEnvVar) {
const existingConnector = choices.find((c) => c.name === connectorEnvVar);
if (existingConnector) {
(0, utils_1.logBullet)(`Picking up the existing connector ${clc.bold(connectorEnvVar)}.`);
return existingConnector.value;
}
(0, utils_1.logWarning)(`Unable to pick up an existing connector based on FDC_CONNECTOR=${connectorEnvVar}.`);
}
(0, utils_1.logWarning)(`Pick up the first connector ${clc.bold(connectorEnvVar)}. Use FDC_CONNECTOR to override it`);
return choices[0].value;
}
function addSdkGenerateToConnectorYaml(connectorInfo, connectorYaml, app) {
const connectorDir = connectorInfo.directory;
const appDir = app.directory;
if (!connectorYaml.generate) {
connectorYaml.generate = {};
}
const generate = connectorYaml.generate;
switch (app.platform) {
case appUtils_1.Platform.ADMIN_NODE: {
const adminNodeSdk = {
outputDir: path.relative(connectorDir, path.join(appDir, `src/dataconnect-admin-generated`)),
package: `@dataconnect/admin-generated`,
packageJsonDir: path.relative(connectorDir, appDir),
};
if (!(0, lodash_1.isArray)(generate === null || generate === void 0 ? void 0 : generate.adminNodeSdk)) {
generate.adminNodeSdk = generate.adminNodeSdk ? [generate.adminNodeSdk] : [];
}
if (!generate.adminNodeSdk.some((s) => s.outputDir === adminNodeSdk.outputDir)) {
generate.adminNodeSdk.push(adminNodeSdk);
}
break;
}
case appUtils_1.Platform.WEB: {
const javascriptSdk = {
outputDir: path.relative(connectorDir, path.join(appDir, `src/dataconnect-generated`)),
package: `@dataconnect/generated`,
packageJsonDir: path.relative(connectorDir, appDir),
react: false,
angular: false,
};
for (const f of app.frameworks || []) {
javascriptSdk[f] = true;
}
if (!(0, lodash_1.isArray)(generate === null || generate === void 0 ? void 0 : generate.javascriptSdk)) {
generate.javascriptSdk = generate.javascriptSdk ? [generate.javascriptSdk] : [];
}
if (!generate.javascriptSdk.some((s) => s.outputDir === javascriptSdk.outputDir)) {
generate.javascriptSdk.push(javascriptSdk);
}
break;
}
case appUtils_1.Platform.FLUTTER: {
const dartSdk = {
outputDir: path.relative(connectorDir, path.join(appDir, `lib/dataconnect_generated`)),
package: "dataconnect_generated/generated.dart",
};
if (!(0, lodash_1.isArray)(generate === null || generate === void 0 ? void 0 : generate.dartSdk)) {
generate.dartSdk = generate.dartSdk ? [generate.dartSdk] : [];
}
if (!generate.dartSdk.some((s) => s.outputDir === dartSdk.outputDir)) {
generate.dartSdk.push(dartSdk);
}
break;
}
case appUtils_1.Platform.ANDROID: {
const kotlinSdk = {
outputDir: path.relative(connectorDir, path.join(appDir, `src/main/kotlin`)),
package: `com.google.firebase.dataconnect.generated`,
};
if (!(0, lodash_1.isArray)(generate === null || generate === void 0 ? void 0 : generate.kotlinSdk)) {
generate.kotlinSdk = generate.kotlinSdk ? [generate.kotlinSdk] : [];
}
if (!generate.kotlinSdk.some((s) => s.outputDir === kotlinSdk.outputDir)) {
generate.kotlinSdk.push(kotlinSdk);
}
break;
}
case appUtils_1.Platform.IOS: {
const swiftSdk = {
outputDir: path.relative(connectorDir, path.join(app.directory, `../FirebaseDataConnectGenerated`)),
package: "DataConnectGenerated",
};
if (!(0, lodash_1.isArray)(generate === null || generate === void 0 ? void 0 : generate.swiftSdk)) {
generate.swiftSdk = generate.swiftSdk ? [generate.swiftSdk] : [];
}
if (!generate.swiftSdk.some((s) => s.outputDir === swiftSdk.outputDir)) {
generate.swiftSdk.push(swiftSdk);
}
break;
}
default:
throw new error_1.FirebaseError(`Unsupported platform ${app.platform} for Data Connect SDK generation. Supported platforms are: ${Object.values(appUtils_1.Platform).join(", ")}\n${JSON.stringify(app)}`);
}
}
exports.addSdkGenerateToConnectorYaml = addSdkGenerateToConnectorYaml;
function dedupeAppsByPlatformAndDirectory(apps) {
var _a;
const uniqueApps = new Map();
for (const app of apps) {
const frameworkKey = app.frameworks ? [...app.frameworks].sort().join(",") : "";
const key = `${app.platform}:${app.directory}:${frameworkKey}`;
if (!uniqueApps.has(key)) {
const minifiedApp = {
platform: app.platform,
directory: app.directory,
};
if ((_a = app.frameworks) === null || _a === void 0 ? void 0 : _a.length) {
minifiedApp.frameworks = [...app.frameworks];
}
uniqueApps.set(key, minifiedApp);
}
}
return Array.from(uniqueApps.values());
}