@hot-updater/firebase 0.35.7 → 0.35.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- import { ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, link, makeEnv, p, resolveHotUpdaterServerVersion, resolvePackageVersion, transformEnv, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
4
+ import { ConfigBuilder, HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV, MissingInitInputsError, assertInitProviderInputs, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, readHotUpdaterInitEnv, resolveHotUpdaterServerVersion, resolveInitProviderInput, resolvePackageVersion, transformEnv, transformTemplate, writeHotUpdaterConfig } from "@hot-updater/cli-tools";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
7
7
  import { StringDecoder } from "node:string_decoder";
@@ -10,7 +10,7 @@ import process$1, { execArgv, execPath, hrtime, platform } from "node:process";
10
10
  import tty from "node:tty";
11
11
  import path$1 from "node:path";
12
12
  import { scheduler, setImmediate, setTimeout } from "node:timers/promises";
13
- import { constants } from "node:os";
13
+ import os, { constants } from "node:os";
14
14
  import { EventEmitter, addAbortListener, on, once, setMaxListeners } from "node:events";
15
15
  import { serialize } from "node:v8";
16
16
  import { appendFileSync, createReadStream, createWriteStream, readFileSync, statSync, writeFileSync } from "node:fs";
@@ -6706,6 +6706,212 @@ createExeca(mapNode);
6706
6706
  createExeca(mapScriptAsync, {}, deepScriptOptions, setScriptSync);
6707
6707
  const { sendMessage, getOneMessage, getEachMessage, getCancelSignal } = getIpcExport();
6708
6708
  //#endregion
6709
+ //#region iac/init/index.ts
6710
+ const isFirebaseRegion = (value) => value !== void 0 && /^[a-z]+(?:-[a-z]+)+[0-9]+$/.test(value);
6711
+ const isFirebaseProjectId = (value) => value !== void 0 && /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(value);
6712
+ const initProvider = {
6713
+ label: "Firebase",
6714
+ inputs: {
6715
+ projectId: {
6716
+ envKey: "HOT_UPDATER_FIREBASE_PROJECT_ID",
6717
+ help: "Firebase project ID",
6718
+ prompt: {
6719
+ message: "Enter the Firebase project ID:",
6720
+ placeholder: "hot-updater-app",
6721
+ type: "text"
6722
+ },
6723
+ validate: isFirebaseProjectId
6724
+ },
6725
+ region: {
6726
+ envKey: "HOT_UPDATER_FIREBASE_REGION",
6727
+ help: "Firebase Functions region",
6728
+ prompt: {
6729
+ message: "Select Region",
6730
+ type: "select"
6731
+ },
6732
+ validate: isFirebaseRegion
6733
+ },
6734
+ applicationCredentials: {
6735
+ envKey: "GOOGLE_APPLICATION_CREDENTIALS",
6736
+ help: "Service account JSON path",
6737
+ optional: true,
6738
+ persistence: "with-consent",
6739
+ prompt: {
6740
+ message: "Enter the service account JSON path (press Enter to configure later)",
6741
+ placeholder: "~/Downloads/firebase-service-account.json",
6742
+ type: "text"
6743
+ }
6744
+ }
6745
+ }
6746
+ };
6747
+ //#endregion
6748
+ //#region iac/firebaseApplicationCredentials.ts
6749
+ const inputFirebaseApplicationCredentials = async ({ applicationCredentials, nonInteractive, projectId }) => {
6750
+ if (nonInteractive) return applicationCredentials;
6751
+ const prompt = initProvider.inputs.applicationCredentials.prompt;
6752
+ p.log.step(`Service account JSON: ${link(`https://console.firebase.google.com/project/${projectId}/settings/serviceaccounts/adminsdk`)}`);
6753
+ p.log.step("Project settings > Service accounts > Generate new private key");
6754
+ const credentialsPath = await p.text({
6755
+ ...getInitProviderTextPromptValues(prompt, applicationCredentials),
6756
+ message: prompt.message
6757
+ });
6758
+ if (p.isCancel(credentialsPath)) process.exit(1);
6759
+ return credentialsPath || void 0;
6760
+ };
6761
+ //#endregion
6762
+ //#region iac/firebaseInitInputs.ts
6763
+ const resolveFirebaseCredentialsPath = (credentialsPath, { cwd = process.cwd(), homeDir = os.homedir(), pathApi = path$1 }) => {
6764
+ const homeRelativePath = credentialsPath === "~" ? "" : credentialsPath.startsWith("~/") || credentialsPath.startsWith("~\\") ? credentialsPath.slice(2) : void 0;
6765
+ return homeRelativePath === void 0 ? pathApi.resolve(cwd, credentialsPath) : pathApi.resolve(homeDir, homeRelativePath);
6766
+ };
6767
+ const getFirebaseCliEnv = (applicationCredentials, pathContext = {}) => {
6768
+ if (!applicationCredentials) return;
6769
+ const resolvedCredentialsPath = resolveFirebaseCredentialsPath(applicationCredentials, pathContext);
6770
+ return { [initProvider.inputs.applicationCredentials.envKey]: resolvedCredentialsPath };
6771
+ };
6772
+ const resolveFirebaseInitInputs = (existingEnv) => {
6773
+ const { inputs } = initProvider;
6774
+ const applicationCredentials = resolveInitProviderInput(existingEnv, inputs.applicationCredentials);
6775
+ return {
6776
+ applicationCredentials: applicationCredentials === "your-credentials.json" ? void 0 : applicationCredentials,
6777
+ projectId: resolveInitProviderInput(existingEnv, inputs.projectId),
6778
+ region: resolveInitProviderInput(existingEnv, inputs.region)
6779
+ };
6780
+ };
6781
+ const assertFirebaseNonInteractiveInputs = (inputs, nonInteractive = false) => {
6782
+ assertInitProviderInputs({
6783
+ inputs,
6784
+ provider: initProvider,
6785
+ strict: nonInteractive
6786
+ });
6787
+ };
6788
+ //#endregion
6789
+ //#region iac/firebaseRegion.ts
6790
+ const REGIONS = [
6791
+ {
6792
+ value: "us-central1",
6793
+ label: "US Central (Iowa)"
6794
+ },
6795
+ {
6796
+ value: "us-east1",
6797
+ label: "US East (South Carolina)"
6798
+ },
6799
+ {
6800
+ value: "us-east4",
6801
+ label: "US East (Northern Virginia)"
6802
+ },
6803
+ {
6804
+ value: "us-west1",
6805
+ label: "US West (Oregon)"
6806
+ },
6807
+ {
6808
+ value: "us-west2",
6809
+ label: "US West (Los Angeles)"
6810
+ },
6811
+ {
6812
+ value: "us-west3",
6813
+ label: "US West (Salt Lake City)"
6814
+ },
6815
+ {
6816
+ value: "us-west4",
6817
+ label: "US West (Las Vegas)"
6818
+ },
6819
+ {
6820
+ value: "europe-west1",
6821
+ label: "Europe West (Belgium)"
6822
+ },
6823
+ {
6824
+ value: "europe-west2",
6825
+ label: "Europe West (London)"
6826
+ },
6827
+ {
6828
+ value: "europe-west3",
6829
+ label: "Europe West (Frankfurt)"
6830
+ },
6831
+ {
6832
+ value: "europe-west6",
6833
+ label: "Europe West (Zurich)"
6834
+ },
6835
+ {
6836
+ value: "asia-east1",
6837
+ label: "Asia East (Taiwan)"
6838
+ },
6839
+ {
6840
+ value: "asia-east2",
6841
+ label: "Asia East (Hong Kong)"
6842
+ },
6843
+ {
6844
+ value: "asia-northeast1",
6845
+ label: "Asia Northeast (Tokyo)"
6846
+ },
6847
+ {
6848
+ value: "asia-northeast2",
6849
+ label: "Asia Northeast (Osaka)"
6850
+ },
6851
+ {
6852
+ value: "asia-northeast3",
6853
+ label: "Asia Northeast (Seoul)"
6854
+ },
6855
+ {
6856
+ value: "asia-south1",
6857
+ label: "Asia South (Mumbai)"
6858
+ },
6859
+ {
6860
+ value: "asia-southeast1",
6861
+ label: "Asia Southeast (Singapore)"
6862
+ },
6863
+ {
6864
+ value: "asia-southeast2",
6865
+ label: "Asia Southeast (Jakarta)"
6866
+ },
6867
+ {
6868
+ value: "australia-southeast1",
6869
+ label: "Australia Southeast (Sydney)"
6870
+ }
6871
+ ];
6872
+ const resolveFirebaseRegion = async ({ cwd, discoverExistingProject = true, nonInteractive, savedRegion, cliEnv }) => {
6873
+ if (nonInteractive) {
6874
+ if (isFirebaseRegion(savedRegion)) return savedRegion;
6875
+ throw new MissingInitInputsError(["HOT_UPDATER_FIREBASE_REGION"]);
6876
+ }
6877
+ let discoveredRegion;
6878
+ if (discoverExistingProject) {
6879
+ const functionsList = await execa("npx", [
6880
+ "firebase",
6881
+ "functions:list",
6882
+ "--json"
6883
+ ], {
6884
+ cwd,
6885
+ env: cliEnv,
6886
+ reject: false
6887
+ });
6888
+ if (functionsList.exitCode === 0) {
6889
+ const parsed = JSON.parse(functionsList.stdout);
6890
+ if (typeof parsed === "object" && parsed !== null && "result" in parsed && Array.isArray(parsed.result)) {
6891
+ for (const entry of parsed.result) if (typeof entry === "object" && entry !== null && "id" in entry && entry.id === "hot-updater" && "region" in entry && typeof entry.region === "string") {
6892
+ discoveredRegion = entry.region;
6893
+ break;
6894
+ }
6895
+ }
6896
+ }
6897
+ }
6898
+ const initialRegion = isFirebaseRegion(savedRegion) ? savedRegion : isFirebaseRegion(discoveredRegion) ? discoveredRegion : REGIONS[0].value;
6899
+ const options = REGIONS.some((region) => region.value === initialRegion) ? REGIONS : [{
6900
+ value: initialRegion,
6901
+ label: `Existing (${initialRegion})`
6902
+ }, ...REGIONS];
6903
+ const selectedRegion = await p.select({
6904
+ initialValue: initialRegion,
6905
+ message: initProvider.inputs.region.prompt.message,
6906
+ options
6907
+ });
6908
+ if (p.isCancel(selectedRegion)) {
6909
+ p.cancel("Operation cancelled.");
6910
+ process.exit(1);
6911
+ }
6912
+ return selectedRegion;
6913
+ };
6914
+ //#endregion
6709
6915
  //#region iac/prepareTemplate.ts
6710
6916
  const ensureExists = async (targetPath, description) => {
6711
6917
  try {
@@ -6778,15 +6984,16 @@ const credential = admin.credential.applicationDefault();`.trim()
6778
6984
  defaultOrNamespace: "admin"
6779
6985
  }).setIntermediateCode(helperStatements.map((statement) => statement.code.trim()).join("\n\n")), { helperStatements });
6780
6986
  };
6781
- const setEnv = async ({ projectId, storageBucket, build }) => {
6987
+ const setEnv = async ({ applicationCredentials, projectId, storageBucket, build, region }) => {
6782
6988
  await makeEnv({
6783
- GOOGLE_APPLICATION_CREDENTIALS: {
6989
+ [initProvider.inputs.applicationCredentials.envKey]: {
6784
6990
  comment: "Project Settings > Service Accounts > New Private Key > Download JSON",
6785
- value: "your-credentials.json"
6991
+ value: applicationCredentials ?? "your-credentials.json"
6786
6992
  },
6787
- HOT_UPDATER_FIREBASE_PROJECT_ID: projectId,
6993
+ [initProvider.inputs.projectId.envKey]: projectId,
6994
+ [initProvider.inputs.region.envKey]: region,
6788
6995
  HOT_UPDATER_FIREBASE_STORAGE_BUCKET: storageBucket
6789
- }, ".env.hotupdater", { preserveKeys: ["GOOGLE_APPLICATION_CREDENTIALS"] });
6996
+ }, ".env.hotupdater", { preserveKeys: applicationCredentials ? [] : [initProvider.inputs.applicationCredentials.envKey] });
6790
6997
  p.log.success("Firebase credentials have been successfully configured.");
6791
6998
  try {
6792
6999
  const configWriteResult = await writeHotUpdaterConfig(getConfigScaffold(build));
@@ -6794,7 +7001,8 @@ const setEnv = async ({ projectId, storageBucket, build }) => {
6794
7001
  else if (configWriteResult.status === "merged") p.log.success("Configuration file 'hot-updater.config.ts' has been updated.");
6795
7002
  else p.log.warn(`Existing 'hot-updater.config.ts' was left unchanged: ${configWriteResult.reason}`);
6796
7003
  } catch (error) {
6797
- console.error("Error writing configuration file:", error.message);
7004
+ const message = error instanceof Error ? error.message : String(error);
7005
+ console.error("Error writing configuration file:", message);
6798
7006
  }
6799
7007
  };
6800
7008
  const handleError = (err) => {
@@ -6802,43 +7010,81 @@ const handleError = (err) => {
6802
7010
  else if (err instanceof Error) p.log.error(`Error occurred: ${err.message}`);
6803
7011
  process.exit(1);
6804
7012
  };
6805
- const listProjects = async () => {
7013
+ const createFirebaseProject = async ({ cliEnv, projectId }) => {
7014
+ if (!isFirebaseProjectId(projectId)) throw new Error(`Invalid Firebase project ID: ${projectId}`);
7015
+ try {
7016
+ await execa("npx", [
7017
+ "firebase",
7018
+ "projects:create",
7019
+ `--display-name=${projectId}`,
7020
+ "--non-interactive",
7021
+ "--",
7022
+ projectId
7023
+ ], {
7024
+ env: cliEnv,
7025
+ stdio: "inherit"
7026
+ });
7027
+ } catch (error) {
7028
+ handleError(error instanceof Error ? error : new Error(String(error)));
7029
+ }
7030
+ p.log.success("Firebase project created successfully");
7031
+ p.log.step("Enable Firestore, Storage, and Billing before running init again:");
7032
+ p.log.step(link(`https://console.firebase.google.com/project/${projectId}/firestore`));
7033
+ p.log.step(link(`https://console.firebase.google.com/project/${projectId}/storage`));
7034
+ };
7035
+ const listProjects = async (nonInteractive = false, cliEnv) => {
6806
7036
  try {
6807
7037
  const projects = await execa("npx", [
6808
7038
  "firebase",
6809
7039
  "projects:list",
6810
- "--json"
6811
- ], { shell: true });
7040
+ "--json",
7041
+ ...nonInteractive ? ["--non-interactive"] : []
7042
+ ], { env: cliEnv });
6812
7043
  return JSON.parse(projects.stdout)?.result ?? [];
6813
- } catch {
6814
- return [];
7044
+ } catch (error) {
7045
+ if (nonInteractive) throw new MissingInitInputsError(["Firebase CLI authentication (`firebase login`)"]);
7046
+ throw error;
6815
7047
  }
6816
7048
  };
6817
- const initFirebaseUser = async (cwd) => {
6818
- try {
6819
- await execa("npx", ["firebase", "login"], {
6820
- stdio: "inherit",
6821
- shell: true
6822
- });
6823
- } catch (err) {
6824
- handleError(err);
6825
- }
6826
- try {
7049
+ const initFirebaseUser = async (cwd, preferredProjectId, nonInteractive = false, cliEnv, resolveCliEnv) => {
7050
+ if (!cliEnv) try {
6827
7051
  const authList = await execa("gcloud", [
6828
7052
  "auth",
6829
7053
  "list",
6830
7054
  "--format=json"
6831
- ], { shell: true });
6832
- if (JSON.parse(authList.stdout).length === 0) await execa("gcloud", ["auth", "login"], {
6833
- stdio: "inherit",
6834
- shell: true
6835
- });
7055
+ ], { env: cliEnv });
7056
+ if (JSON.parse(authList.stdout).length === 0) {
7057
+ if (nonInteractive) throw new MissingInitInputsError(["active gcloud authentication (`gcloud auth login`)"]);
7058
+ await execa("gcloud", ["auth", "login"], {
7059
+ env: cliEnv,
7060
+ stdio: "inherit"
7061
+ });
7062
+ }
6836
7063
  } catch (err) {
7064
+ if (err instanceof MissingInitInputsError) throw err;
6837
7065
  handleError(err);
6838
7066
  }
6839
- const projects = await listProjects();
7067
+ let projects;
7068
+ try {
7069
+ projects = await listProjects(nonInteractive, cliEnv);
7070
+ } catch (error) {
7071
+ if (nonInteractive || cliEnv) throw error;
7072
+ try {
7073
+ await execa("npx", ["firebase", "login"], {
7074
+ env: cliEnv,
7075
+ stdio: "inherit"
7076
+ });
7077
+ projects = await listProjects(false, cliEnv);
7078
+ } catch (loginError) {
7079
+ handleError(loginError instanceof Error ? loginError : new Error(String(loginError)));
7080
+ }
7081
+ }
6840
7082
  const createKey = `create/${Math.random().toString(36).substring(2, 15)}`;
6841
- const projectId = await p.select({
7083
+ const preferredProject = projects.find((project) => project.projectId === preferredProjectId);
7084
+ if (preferredProjectId && !preferredProject) p.log.warn("Saved Firebase project was not found. Select a project again.");
7085
+ if (nonInteractive && !preferredProject) throw new MissingInitInputsError(["HOT_UPDATER_FIREBASE_PROJECT_ID"]);
7086
+ const projectId = nonInteractive && preferredProject ? preferredProject.projectId : await p.select({
7087
+ initialValue: preferredProject?.projectId ?? projects[0]?.projectId,
6842
7088
  message: "Select a Firebase project",
6843
7089
  options: [...projects.map((project) => ({
6844
7090
  label: project.displayName,
@@ -6853,29 +7099,22 @@ const initFirebaseUser = async (cwd) => {
6853
7099
  process.exit(1);
6854
7100
  }
6855
7101
  if (projectId === createKey) {
6856
- const newProjectId = await p.text({ message: "Enter the Firebase project ID:" });
7102
+ const prompt = initProvider.inputs.projectId.prompt;
7103
+ const newProjectId = await p.text({
7104
+ ...getInitProviderTextPromptValues(prompt, preferredProjectId),
7105
+ message: prompt.message,
7106
+ validate: (value) => isFirebaseProjectId(value) ? void 0 : "Use 6-30 lowercase letters, numbers, or hyphens; start with a letter and end with a letter or number."
7107
+ });
6857
7108
  if (p.isCancel(newProjectId)) {
6858
7109
  p.log.error("Project ID is required");
6859
7110
  process.exit(1);
6860
7111
  }
6861
- try {
6862
- await execa("npx", [
6863
- "firebase",
6864
- "projects:create",
6865
- newProjectId
6866
- ], {
6867
- stdio: "inherit",
6868
- shell: true
6869
- });
6870
- p.log.success("Firebase project created successfully");
6871
- p.log.step("Please Go to the following links to enable Firestore and Storage and Billing");
6872
- p.log.step(link(`https://console.firebase.google.com/project/${newProjectId}/firestore`));
6873
- p.log.step(link(`https://console.firebase.google.com/project/${newProjectId}/storage`));
6874
- } catch (err) {
6875
- handleError(err);
6876
- }
6877
- process.exit(0);
7112
+ return {
7113
+ status: "create",
7114
+ projectId: newProjectId
7115
+ };
6878
7116
  }
7117
+ const selectedProjectCliEnv = resolveCliEnv ? await resolveCliEnv(projectId) : cliEnv;
6879
7118
  await p.tasks([{
6880
7119
  title: `Select Firebase project (${projectId})...`,
6881
7120
  task: async () => {
@@ -6883,11 +7122,12 @@ const initFirebaseUser = async (cwd) => {
6883
7122
  await execa("npx", [
6884
7123
  "firebase",
6885
7124
  "use",
6886
- "--add",
6887
- projectId
7125
+ ...nonInteractive ? [] : ["--add"],
7126
+ projectId,
7127
+ ...nonInteractive ? ["--non-interactive"] : []
6888
7128
  ], {
6889
7129
  cwd,
6890
- shell: true
7130
+ env: selectedProjectCliEnv
6891
7131
  });
6892
7132
  } catch (error) {
6893
7133
  if (error instanceof ExecaError) p.log.error(error.stderr || error.stdout || error.message);
@@ -6904,7 +7144,7 @@ const initFirebaseUser = async (cwd) => {
6904
7144
  `--project=${projectId}`,
6905
7145
  "--format=json"
6906
7146
  ], {
6907
- shell: true,
7147
+ env: selectedProjectCliEnv,
6908
7148
  input: "N\n"
6909
7149
  });
6910
7150
  if (JSON.parse(databases.stdout).length === 0) {
@@ -6915,7 +7155,7 @@ const initFirebaseUser = async (cwd) => {
6915
7155
  process.exit(1);
6916
7156
  }
6917
7157
  } catch (err) {
6918
- handleError(err);
7158
+ handleError(err instanceof Error ? err : new Error(String(err)));
6919
7159
  }
6920
7160
  let storageBucket = null;
6921
7161
  await p.tasks([{
@@ -6927,7 +7167,7 @@ const initFirebaseUser = async (cwd) => {
6927
7167
  "list",
6928
7168
  `--project=${projectId}`,
6929
7169
  "--format=json"
6930
- ], { shell: true });
7170
+ ], { env: selectedProjectCliEnv });
6931
7171
  storageBucket = JSON.parse(buckets.stdout).find((bucket) => bucket.name === `${projectId}.firebasestorage.app` || bucket.name === `${projectId}.appspot.com`)?.name;
6932
7172
  if (!storageBucket) {
6933
7173
  p.log.error("Storage Bucket not found");
@@ -6947,7 +7187,7 @@ const initFirebaseUser = async (cwd) => {
6947
7187
  "describe",
6948
7188
  projectId,
6949
7189
  "--format=json"
6950
- ], { shell: true });
7190
+ ], { env: selectedProjectCliEnv });
6951
7191
  const projectJson = JSON.parse(project.stdout);
6952
7192
  const projectNumber = Number(projectJson.projectNumber);
6953
7193
  if (Number.isNaN(projectNumber)) {
@@ -6955,6 +7195,7 @@ const initFirebaseUser = async (cwd) => {
6955
7195
  process.exit(1);
6956
7196
  }
6957
7197
  return {
7198
+ status: "ready",
6958
7199
  storageBucket,
6959
7200
  projectNumber,
6960
7201
  projectId
@@ -6973,88 +7214,6 @@ export default HotUpdater.wrap({
6973
7214
  baseURL: "%%source%%",
6974
7215
  updateStrategy: "appVersion", // or "fingerprint"
6975
7216
  })(App);`;
6976
- const REGIONS = [
6977
- {
6978
- value: "us-central1",
6979
- label: "US Central (Iowa)"
6980
- },
6981
- {
6982
- value: "us-east1",
6983
- label: "US East (South Carolina)"
6984
- },
6985
- {
6986
- value: "us-east4",
6987
- label: "US East (Northern Virginia)"
6988
- },
6989
- {
6990
- value: "us-west1",
6991
- label: "US West (Oregon)"
6992
- },
6993
- {
6994
- value: "us-west2",
6995
- label: "US West (Los Angeles)"
6996
- },
6997
- {
6998
- value: "us-west3",
6999
- label: "US West (Salt Lake City)"
7000
- },
7001
- {
7002
- value: "us-west4",
7003
- label: "US West (Las Vegas)"
7004
- },
7005
- {
7006
- value: "europe-west1",
7007
- label: "Europe West (Belgium)"
7008
- },
7009
- {
7010
- value: "europe-west2",
7011
- label: "Europe West (London)"
7012
- },
7013
- {
7014
- value: "europe-west3",
7015
- label: "Europe West (Frankfurt)"
7016
- },
7017
- {
7018
- value: "europe-west6",
7019
- label: "Europe West (Zurich)"
7020
- },
7021
- {
7022
- value: "asia-east1",
7023
- label: "Asia East (Taiwan)"
7024
- },
7025
- {
7026
- value: "asia-east2",
7027
- label: "Asia East (Hong Kong)"
7028
- },
7029
- {
7030
- value: "asia-northeast1",
7031
- label: "Asia Northeast (Tokyo)"
7032
- },
7033
- {
7034
- value: "asia-northeast2",
7035
- label: "Asia Northeast (Osaka)"
7036
- },
7037
- {
7038
- value: "asia-northeast3",
7039
- label: "Asia Northeast (Seoul)"
7040
- },
7041
- {
7042
- value: "asia-south1",
7043
- label: "Asia South (Mumbai)"
7044
- },
7045
- {
7046
- value: "asia-southeast1",
7047
- label: "Asia Southeast (Singapore)"
7048
- },
7049
- {
7050
- value: "asia-southeast2",
7051
- label: "Asia Southeast (Jakarta)"
7052
- },
7053
- {
7054
- value: "australia-southeast1",
7055
- label: "Australia Southeast (Sydney)"
7056
- }
7057
- ];
7058
7217
  const getFirebaseRuntimePackageInfo = () => {
7059
7218
  const firebasePackageRoot = path.dirname(__require.resolve("@hot-updater/firebase/package.json"));
7060
7219
  return {
@@ -7088,10 +7247,14 @@ const mergeIndexes = (originalIndexes, newIndexes) => {
7088
7247
  fieldOverrides: merge(originalIndexes.fieldOverrides, newIndexes.fieldOverrides)
7089
7248
  };
7090
7249
  };
7091
- const deployFirestore = async (cwd) => {
7092
- const original = await execa("npx", ["firebase", "firestore:indexes"], {
7250
+ const deployFirestore = async (cwd, nonInteractive = false, cliEnv) => {
7251
+ const original = await execa("npx", [
7252
+ "firebase",
7253
+ "firestore:indexes",
7254
+ ...nonInteractive ? ["--non-interactive"] : []
7255
+ ], {
7093
7256
  cwd,
7094
- shell: true
7257
+ env: cliEnv
7095
7258
  });
7096
7259
  let originalIndexes = {
7097
7260
  indexes: [],
@@ -7102,7 +7265,12 @@ const deployFirestore = async (cwd) => {
7102
7265
  indexes: [],
7103
7266
  fieldOverrides: []
7104
7267
  };
7105
- } catch {}
7268
+ } catch {
7269
+ originalIndexes = {
7270
+ indexes: [],
7271
+ fieldOverrides: []
7272
+ };
7273
+ }
7106
7274
  const newIndexes = JSON.parse(await fs.promises.readFile(path.join(cwd, "firestore.indexes.json"), "utf-8"));
7107
7275
  const mergedIndexes = mergeIndexes(originalIndexes, newIndexes);
7108
7276
  await fs.promises.writeFile(path.join(cwd, "firestore.indexes.json"), JSON.stringify(mergedIndexes, null, 2));
@@ -7111,11 +7279,12 @@ const deployFirestore = async (cwd) => {
7111
7279
  "firebase",
7112
7280
  "deploy",
7113
7281
  "--only",
7114
- "firestore"
7282
+ "firestore",
7283
+ ...nonInteractive ? ["--non-interactive"] : []
7115
7284
  ], {
7116
7285
  cwd,
7117
- stdio: "inherit",
7118
- shell: true
7286
+ env: cliEnv,
7287
+ stdio: "inherit"
7119
7288
  });
7120
7289
  } catch (e) {
7121
7290
  if (e instanceof ExecaError) p.log.error(e.stderr || e.stdout || e.message);
@@ -7123,17 +7292,18 @@ const deployFirestore = async (cwd) => {
7123
7292
  process.exit(1);
7124
7293
  }
7125
7294
  };
7126
- const deployFunctions = async (cwd) => {
7295
+ const deployFunctions = async (cwd, nonInteractive = false, cliEnv) => {
7127
7296
  try {
7128
7297
  await execa("npx", [
7129
7298
  "firebase",
7130
7299
  "deploy",
7131
7300
  "--only",
7132
- "functions"
7301
+ "functions",
7302
+ ...nonInteractive ? ["--non-interactive"] : []
7133
7303
  ], {
7134
7304
  cwd,
7135
- stdio: "inherit",
7136
- shell: true
7305
+ env: cliEnv,
7306
+ stdio: "inherit"
7137
7307
  });
7138
7308
  } catch (e) {
7139
7309
  if (e instanceof ExecaError) p.log.error(e.stderr || e.stdout || e.message);
@@ -7141,7 +7311,7 @@ const deployFunctions = async (cwd) => {
7141
7311
  process.exit(1);
7142
7312
  }
7143
7313
  };
7144
- const printTemplate = async (projectId, region) => {
7314
+ const printTemplate = async (projectId, region, cliEnv) => {
7145
7315
  try {
7146
7316
  const { stdout } = await execa("gcloud", [
7147
7317
  "functions",
@@ -7152,7 +7322,7 @@ const printTemplate = async (projectId, region) => {
7152
7322
  "--region",
7153
7323
  region,
7154
7324
  "--format=json"
7155
- ], { shell: true });
7325
+ ], { env: cliEnv });
7156
7326
  const parsedData = JSON.parse(stdout);
7157
7327
  const functionUrl = `${parsedData?.serviceConfig?.uri ?? parsedData.url}/api/check-update`;
7158
7328
  p.note(transformTemplate(SOURCE_TEMPLATE, { source: functionUrl }));
@@ -7164,13 +7334,20 @@ const printTemplate = async (projectId, region) => {
7164
7334
  };
7165
7335
  const checkIfGcloudCliInstalled = async () => {
7166
7336
  try {
7167
- await execa("gcloud", ["--version"], { shell: true });
7337
+ await execa("gcloud", ["--version"]);
7168
7338
  return true;
7169
7339
  } catch {
7170
7340
  return false;
7171
7341
  }
7172
7342
  };
7173
- const runInit = async ({ build }) => {
7343
+ const runInit = async ({ build, envFile }) => {
7344
+ const nonInteractive = envFile !== void 0;
7345
+ const initEnvSources = await readHotUpdaterInitEnv(process.cwd(), envFile);
7346
+ const { managedEnv } = initEnvSources;
7347
+ const savedInputs = resolveFirebaseInitInputs(getHotUpdaterInitInputEnv(initEnvSources, nonInteractive));
7348
+ assertFirebaseNonInteractiveInputs(savedInputs, nonInteractive);
7349
+ let applicationCredentials = savedInputs.applicationCredentials;
7350
+ const cliEnv = nonInteractive ? getFirebaseCliEnv(applicationCredentials) : void 0;
7174
7351
  if (!await checkIfGcloudCliInstalled()) {
7175
7352
  p.log.error("gcloud CLI is not installed");
7176
7353
  p.log.step("Please go to the following link to install the gcloud CLI");
@@ -7180,22 +7357,61 @@ const runInit = async ({ build }) => {
7180
7357
  const { tmpDir, removeTmpDir, functionsDir } = await prepareFirebaseTemplate(path.dirname(path.dirname(__require.resolve("@hot-updater/firebase/functions"))));
7181
7358
  const functionsIndexPath = path.join(functionsDir, "index.cjs");
7182
7359
  const runtimePackageInfo = await syncFunctionsPackageJson(functionsDir);
7183
- const initializeVariable = await initFirebaseUser(tmpDir);
7184
- let currentRegion;
7360
+ const initializeVariable = await initFirebaseUser(tmpDir, savedInputs.projectId, nonInteractive, cliEnv, async (projectId) => {
7361
+ applicationCredentials = await inputFirebaseApplicationCredentials({
7362
+ applicationCredentials,
7363
+ nonInteractive,
7364
+ projectId
7365
+ });
7366
+ return cliEnv;
7367
+ });
7368
+ const currentRegion = await resolveFirebaseRegion({
7369
+ cwd: tmpDir,
7370
+ discoverExistingProject: initializeVariable.status === "ready",
7371
+ nonInteractive,
7372
+ savedRegion: savedInputs.region,
7373
+ cliEnv
7374
+ });
7375
+ const resolvedInputs = {
7376
+ ...savedInputs,
7377
+ applicationCredentials: applicationCredentials || void 0,
7378
+ projectId: initializeVariable.projectId,
7379
+ region: currentRegion
7380
+ };
7381
+ const persistedInputs = getInitProviderEnvVars({
7382
+ includeConsentInputs: await confirmInitInputPersistence({
7383
+ existingEnv: managedEnv,
7384
+ inputs: resolvedInputs,
7385
+ nonInteractive,
7386
+ provider: initProvider
7387
+ }),
7388
+ inputs: resolvedInputs,
7389
+ provider: initProvider
7390
+ });
7391
+ if (initializeVariable.status === "create") {
7392
+ await createFirebaseProject({
7393
+ cliEnv,
7394
+ projectId: initializeVariable.projectId
7395
+ });
7396
+ await makeEnv(persistedInputs);
7397
+ await removeTmpDir();
7398
+ return;
7399
+ }
7400
+ const functionsCode = transformEnv(functionsIndexPath, { REGION: currentRegion });
7401
+ await fs.promises.writeFile(functionsIndexPath, functionsCode);
7185
7402
  await setEnv({
7186
7403
  projectId: initializeVariable.projectId,
7187
7404
  storageBucket: initializeVariable.storageBucket,
7188
- build
7405
+ build,
7406
+ region: currentRegion,
7407
+ applicationCredentials: persistedInputs[initProvider.inputs.applicationCredentials.envKey]
7189
7408
  });
7190
7409
  if (runtimePackageInfo.serverPackageVersion !== runtimePackageInfo.currentPackageVersion) p.note(`Using ${HOT_UPDATER_SERVER_PACKAGE_VERSION_ENV}=${runtimePackageInfo.serverPackageVersion} for Firebase functions deploy.`);
7191
7410
  await p.tasks([{
7192
7411
  title: "Installing dependencies...",
7193
7412
  task: async () => {
7194
7413
  try {
7195
- await execa("npm", ["install"], {
7196
- cwd: functionsDir,
7197
- shell: true
7198
- });
7414
+ await execa("npm", ["install"], { cwd: functionsDir });
7199
7415
  return "Installed dependencies";
7200
7416
  } catch (error) {
7201
7417
  if (error instanceof ExecaError) p.log.error(error.stderr || error.stdout || error.message);
@@ -7203,59 +7419,20 @@ const runInit = async ({ build }) => {
7203
7419
  process.exit(1);
7204
7420
  }
7205
7421
  }
7206
- }, {
7207
- title: "Checking existing functions and setting region",
7208
- task: async () => {
7209
- let isFunctionsExist = false;
7210
- try {
7211
- const { stdout } = await execa("npx", [
7212
- "firebase",
7213
- "functions:list",
7214
- "--json"
7215
- ], {
7216
- cwd: tmpDir,
7217
- shell: true
7218
- });
7219
- const hotUpdater = (JSON.parse(stdout).result || []).find((fn) => fn.id === "hot-updater");
7220
- if (hotUpdater?.region) {
7221
- currentRegion = hotUpdater.region;
7222
- isFunctionsExist = true;
7223
- }
7224
- } catch {}
7225
- if (!isFunctionsExist) {
7226
- const selectedRegion = await p.select({
7227
- message: "Select Region",
7228
- options: REGIONS,
7229
- initialValue: REGIONS[0].value
7230
- });
7231
- if (p.isCancel(selectedRegion)) {
7232
- p.cancel("Operation cancelled.");
7233
- process.exit(1);
7234
- }
7235
- currentRegion = selectedRegion;
7236
- }
7237
- if (!currentRegion) {
7238
- p.log.error("Region is not set");
7239
- await removeTmpDir();
7240
- process.exit(1);
7241
- }
7242
- const code = transformEnv(functionsIndexPath, { REGION: currentRegion });
7243
- await fs.promises.writeFile(functionsIndexPath, code);
7244
- return `Using ${isFunctionsExist ? "existing" : "new"} functions in region: ${currentRegion}`;
7245
- }
7246
7422
  }]);
7247
- await deployFirestore(tmpDir);
7248
- await deployFunctions(tmpDir);
7423
+ await deployFirestore(tmpDir, nonInteractive, cliEnv);
7424
+ await deployFunctions(tmpDir, nonInteractive, cliEnv);
7249
7425
  await p.tasks([{
7250
7426
  title: "Check IAM policy",
7251
7427
  async task(message) {
7252
7428
  const functionsList = await execa("npx", [
7253
7429
  "firebase",
7254
7430
  "functions:list",
7255
- "--json"
7431
+ "--json",
7432
+ ...nonInteractive ? ["--non-interactive"] : []
7256
7433
  ], {
7257
7434
  cwd: tmpDir,
7258
- shell: true
7435
+ env: cliEnv
7259
7436
  });
7260
7437
  const account = (JSON.parse(functionsList.stdout).result || []).find((fn) => fn.id === "hot-updater")?.serviceAccount;
7261
7438
  if (!account) {
@@ -7268,7 +7445,7 @@ const runInit = async ({ build }) => {
7268
7445
  "get-iam-policy",
7269
7446
  initializeVariable.projectId,
7270
7447
  "--format=json"
7271
- ], { shell: true });
7448
+ ], { env: cliEnv });
7272
7449
  if (!JSON.parse(checkIam.stdout).bindings.some((binding) => binding.role === "roles/iam.serviceAccountTokenCreator" && binding.members.includes(`serviceAccount:${account}`))) try {
7273
7450
  message("Adding IAM Service Account Token Creator role to the service account");
7274
7451
  await execa("gcloud", [
@@ -7278,8 +7455,8 @@ const runInit = async ({ build }) => {
7278
7455
  `--member=serviceAccount:${account}`,
7279
7456
  "--role=roles/iam.serviceAccountTokenCreator"
7280
7457
  ], {
7281
- stdio: "inherit",
7282
- shell: true
7458
+ env: cliEnv,
7459
+ stdio: "inherit"
7283
7460
  });
7284
7461
  p.log.success("IAM Service Account Token Creator role has been added to the service account");
7285
7462
  } catch {
@@ -7296,10 +7473,10 @@ const runInit = async ({ build }) => {
7296
7473
  await removeTmpDir();
7297
7474
  process.exit(1);
7298
7475
  }
7299
- await printTemplate(initializeVariable.projectId, currentRegion);
7476
+ await printTemplate(initializeVariable.projectId, currentRegion, cliEnv);
7300
7477
  await removeTmpDir();
7301
7478
  p.log.message(`Next step: ${link("https://hot-updater.dev/docs/managed/firebase#step-3-generated-configurations")}`);
7302
- p.log.message("Next step: Change GOOGLE_APPLICATION_CREDENTIALS=your-credentials.json in .env file");
7479
+ if (!applicationCredentials) p.log.message("Next step: Change GOOGLE_APPLICATION_CREDENTIALS=your-credentials.json in .env file");
7303
7480
  p.log.success("Done! 🎉");
7304
7481
  };
7305
7482
  //#endregion