<commit_before>import * as clc from "cli-color";
import * as path from "path";
import * as refs from "./refs";
import { Config } from "../config";
import { getExtensionSpec, ManifestInstanceSpec } from "../deploy/extensions/planner";
import { logger } from "../logger";
import { promptOnce } from "../prompt";
import { readEnvFile } from "./paramHelper";
import { FirebaseError } from "../error";
import * as utils from "../utils";
import { isLocalPath, logPrefix } from "./extensionsHelper";
import { ParamType } from "./extensionsApi";

export const ENV_DIRECTORY = "extensions";

/**
 * Write a list of instanceSpecs to extensions manifest.
 *
 * The manifest is composed of both the extension instance list in firebase.json, and
 * env-var for each extension instance under ./extensions/*.env
 *
 * @param specs a list of InstanceSpec to write to the manifest
 * @param config existing config in firebase.json
 * @param options.nonInteractive will try to do the job without asking for user input.
 * @param options.force only when this flag is true this will overwrite existing .env files
 * @param allowOverwrite allows overwriting the entire manifest with the new specs
 */
export async function writeToManifest(
  specs: ManifestInstanceSpec[],
  config: Config,
  options: { nonInteractive: boolean; force: boolean },
  allowOverwrite: boolean = false
): Promise<void> {
  if (
    config.has("extensions") &&
    Object.keys(config.get("extensions")).length &&
    !options.nonInteractive &&
    !options.force
  ) {
    const currentExtensions = Object.entries(config.get("extensions"))
      .map((i) => `${i[0]}: ${i[1]}`)
      .join("\n\t");
    if (allowOverwrite) {
      const overwrite = await promptOnce({
        type: "list",
        message: `firebase.json already contains extensions:\n${currentExtensions}\nWould you like to overwrite or merge?`,
        choices: [
          { name: "Overwrite", value: true },
          { name: "Merge", value: false },
        ],
      });
      if (overwrite) {
        config.set("extensions", {});
      }
    }
  }

  writeExtensionsToFirebaseJson(specs, config);
  await writeEnvFiles(specs, config, options.force);
  await writeLocalSecrets(specs, config, options.force);
}

/**
 * Write the secrets in a list of ManifestInstanceSpec into extensions/{instance-id}.secret.local.
 *
 * Exported for testing.
 */
export async function writeLocalSecrets(
  specs: ManifestInstanceSpec[],
  config: Config,
  force?: boolean
): Promise<void> {
  for (const spec of specs) {
    const extensionSpec = await getExtensionSpec(spec);
    if (!extensionSpec.params) {
      continue;
    }

    const writeBuffer: Record<string, string> = {};
    const locallyOverridenSecretParams = extensionSpec.params.filter(
      (p) => p.type === ParamType.SECRET && spec.params[p.param].local
    );
    for (const paramSpec of locallyOverridenSecretParams) {
      const key = paramSpec.param;
      const localValue = spec.params[key].local!;
      writeBuffer[key] = localValue;
    }

    const content = Object.entries(writeBuffer)
      .sort((a, b) => {
        return a[0].localeCompare(b[0]);
      })
      .map((r) => `${r[0]}=${r[1]}`)
      .join("\n");
    if (content) {
      await config.askWriteProjectFile(
        `extensions/${spec.instanceId}.secret.local`,
        content,
        force
      );
    }
  }<commit_msg>Fixing an issue where not providing a value for optional secrets would cause NPEs (#4507)<commit_after>import * as clc from "cli-color";
import * as path from "path";
import * as refs from "./refs";
import { Config } from "../config";
import { getExtensionSpec, ManifestInstanceSpec } from "../deploy/extensions/planner";
import { logger } from "../logger";
import { promptOnce } from "../prompt";
import { readEnvFile } from "./paramHelper";
import { FirebaseError } from "../error";
import * as utils from "../utils";
import { isLocalPath, logPrefix } from "./extensionsHelper";
import { ParamType } from "./extensionsApi";

export const ENV_DIRECTORY = "extensions";

/**
 * Write a list of instanceSpecs to extensions manifest.
 *
 * The manifest is composed of both the extension instance list in firebase.json, and
 * env-var for each extension instance under ./extensions/*.env
 *
 * @param specs a list of InstanceSpec to write to the manifest
 * @param config existing config in firebase.json
 * @param options.nonInteractive will try to do the job without asking for user input.
 * @param options.force only when this flag is true this will overwrite existing .env files
 * @param allowOverwrite allows overwriting the entire manifest with the new specs
 */
export async function writeToManifest(
  specs: ManifestInstanceSpec[],
  config: Config,
  options: { nonInteractive: boolean; force: boolean },
  allowOverwrite: boolean = false
): Promise<void> {
  if (
    config.has("extensions") &&
    Object.keys(config.get("extensions")).length &&
    !options.nonInteractive &&
    !options.force
  ) {
    const currentExtensions = Object.entries(config.get("extensions"))
      .map((i) => `${i[0]}: ${i[1]}`)
      .join("\n\t");
    if (allowOverwrite) {
      const overwrite = await promptOnce({
        type: "list",
        message: `firebase.json already contains extensions:\n${currentExtensions}\nWould you like to overwrite or merge?`,
        choices: [
          { name: "Overwrite", value: true },
          { name: "Merge", value: false },
        ],
      });
      if (overwrite) {
        config.set("extensions", {});
      }
    }
  }

  writeExtensionsToFirebaseJson(specs, config);
  await writeEnvFiles(specs, config, options.force);
  await writeLocalSecrets(specs, config, options.force);
}

/**
 * Write the secrets in a list of ManifestInstanceSpec into extensions/{instance-id}.secret.local.
 *
 * Exported for testing.
 */
export async function writeLocalSecrets(
  specs: ManifestInstanceSpec[],
  config: Config,
  force?: boolean
): Promise<void> {
  for (const spec of specs) {
    const extensionSpec = await getExtensionSpec(spec);
    if (!extensionSpec.params) {
      continue;
    }

    const writeBuffer: Record<string, string> = {};
    const locallyOverridenSecretParams = extensionSpec.params.filter(
      (p) => p.type === ParamType.SECRET && spec.params[p.param]?.local
    );
    for (const paramSpec of locallyOverridenSecretParams) {
      const key = paramSpec.param;
      const localValue = spec.params[key].local!;
      writeBuffer[key] = localValue;
    }

    const content = Object.entries(writeBuffer)
      .sort((a, b) => {
        return a[0].localeCompare(b[0]);
      })
      .map((r) => `${r[0]}=${r[1]}`)
      .join("\n");
    if (content) {
      await config.askWriteProjectFile(
        `extensions/${spec.instanceId}.secret.local`,
        content,
        force
      );
    }
  }
{'fraction_non_alphanumeric': 0.1322048737779075, 'fraction_numerical': 0.0023347439077776155, 'mean_word_length': 3.3079824010056567, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1330891', 'n_tokens_mistral': 1961, 'n_tokens_neox': 1957, 'n_words': 590}