exec method

  1. @override
Future<void> exec(
  1. ExecContext context
)
override

Run command.

The contents of katana.yaml and the arguments of the command are passed to context.

コマンドを実行します。

contextkatana.yamlの内容やコマンドの引数が渡されます。

Implementation

@override
Future<void> exec(ExecContext context) async {
  final bin = context.yaml.getAsMap("bin");
  final flutter = bin.get("flutter", "flutter");
  final npm = bin.get("npm", "npm");
  final npx = bin.get("npx", "npx");
  final firebaseCommand = bin.get("firebase", "firebase");
  final gsutil = bin.get("gsutil", "gsutil");
  final flutterfireCommand = bin.get("flutterfire", "flutterfire");
  final firebase = context.yaml.getAsMap("firebase");
  final github = context.yaml.getAsMap("github");
  final action = github.getAsMap("action");
  final enableGithubAction = action.get("enable", false);
  final platformGithubAction = action.get("platform", "");
  final webGithubAction = action.getAsMap("web");
  final repositoryFirebaseWebGithubAction =
      webGithubAction.get("repository", "");
  final projectId = firebase.get("project_id", "");
  final hosting = firebase.getAsMap("hosting");
  final useFlutter = hosting.get("use_flutter", false);
  final firestore = firebase.getAsMap("firestore");
  final dataconnect = firebase.getAsMap("dataconnect");
  // final overwriteFirestoreRule = firestore.get("overwrite_rule", false);
  final enabledFirestore = firestore.get("enable", false);
  final firestorePrimaryRemoteIndex =
      firestore.get("primary_remote_index", false);
  final firestoreGenerateRulesAndIndexes =
      firestore.get("generate_rules_and_indexes", false);
  final enabledDataconnect = dataconnect.get("enable", false);
  final enabledAuthentication =
      firebase.getAsMap("authentication").get("enable", false);
  final enabledFunctions =
      firebase.getAsMap("functions").get("enable", false);
  final storage = firebase.getAsMap("storage");
  // final overwriteStorageRule = storage.get("overwrite_rule", false);
  final enabledStorage = storage.get("enable", false);
  final enabledCors = storage.get("cors", false);
  final enabledHosting = hosting.get("enable", false);
  final enableActions = hosting.get("github_actions", false);
  final enabledLogger = firebase.getAsMap("logger").get("enable", false);
  if (projectId.isEmpty) {
    error(
      "The item [firebase]->[project_id] is missing. Please provide the Firebase project ID for the configuration.",
    );
    return;
  }
  if (enableActions && !enableGithubAction) {
    error(
      "The item [hosting]->[github_actions] is true, but the item [github]->[action]->[enable] is false. Please set the item [github]->[action]->[enable] to true.",
    );
    return;
  }
  if (enableActions && !platformGithubAction.contains("web")) {
    error(
      "The item [hosting]->[github_actions] is true, but the item [github]->[action]->[platform] does not contain `web`. Please set the item [github]->[action]->[platform] to `web`.",
    );
    return;
  }
  if (enableActions && repositoryFirebaseWebGithubAction.isEmpty) {
    error(
      "The item [github]->[action]->[web]->[repository] is missing. Please provide the repository name for the Firebase Web configuration.",
    );
    return;
  }
  label("Create firebase directory");
  final firebaseDir = Directory("firebase");
  if (!firebaseDir.existsSync()) {
    await firebaseDir.create();
  }
  label("Check firebase.json");
  final firebaseJsonFile = File("firebase/firebase.json");
  final firebaseJsonFileExists = firebaseJsonFile.existsSync();
  final firebaseJson = firebaseJsonFileExists
      ? jsonDecodeAsMap(await firebaseJsonFile.readAsString())
      : <String, dynamic>{};
  label("Check status");
  final firebaseFunctionsIndex = File("firebase/functions/src/index.ts");
  final firebaseFunctionsIndexExists = firebaseFunctionsIndex.existsSync();
  label("Load data");
  final gradle = AppGradle();
  await gradle.load();
  final androidApplicationId = gradle.android?.defaultConfig.applicationId
      .trim()
      .replaceAll('"', "")
      .replaceAll("'", "");
  final xcode = XCode();
  await xcode.load();
  final bundleId = xcode.pbxBuildConfiguration
      .map(
        (e) => e.buildSettings
            .firstWhereOrNull((e) => e.key == "PRODUCT_BUNDLE_IDENTIFIER")
            ?.value,
      )
      .firstWhereOrNull((e) => e != null)
      ?.trim()
      .replaceAll('"', "")
      .replaceAll("'", "");
  if (bundleId.isEmpty) {
    error(
      "Bundle ID is not set in your XCode project. Please open `ios/Runner.xcodeproj` and check the settings.",
    );
    return;
  }
  if (!firebaseJsonFileExists) {
    await command(
      "Run flutterfire configure",
      [
        flutterfireCommand,
        "configure",
        "-y",
        "--project=$projectId",
        "--ios-bundle-id=$bundleId",
        "--macos-bundle-id=$bundleId",
        if (androidApplicationId.isNotEmpty)
          "--android-package-name=$androidApplicationId",
      ],
    );
  }
  await addFlutterImport(
    [
      "firebase_core",
      if (enabledAuthentication) ...[
        "firebase_auth",
        "katana_auth_firebase",
      ],
      if (enabledFirestore) ...[
        "cloud_firestore",
        "katana_model_firestore",
      ],
      if (enabledDataconnect) ...[
        "firebase_data_connect",
        "masamune_model_firebase_data_connect",
      ],
      if (enabledStorage) ...[
        "firebase_storage",
        "katana_storage_firebase",
      ],
      if (enabledFunctions) ...[
        "katana_functions_firebase",
      ],
      if (enabledLogger) ...[
        "firebase_analytics",
        "firebase_crashlytics",
        "firebase_performance",
        "masamune_logger_firebase",
      ]
    ],
  );
  await addFlutterImport(
    [
      if (enabledDataconnect) ...[
        "masamune_model_firebase_data_connect_builder",
      ],
    ],
    development: true,
  );
  final commandStack = <String>[];
  if (enabledFirestore) {
    if (!firebaseJson.containsKey("firestore")) {
      await command(
        "Initialize Firestore",
        [
          firebaseCommand,
          "init",
          "firestore",
          "--project",
          projectId,
        ],
        runInShell: true,
        workingDirectory: "firebase",
        action: (process, line) {
          _runCommandStack(
            line,
            "? Are you ready to proceed?",
            commandStack,
            () => process.stdin.write("y\n"),
          );
          _runCommandStack(
            line,
            "? What file should be used for Firestore Rules?",
            commandStack,
            () => process.stdin.write("\n"),
          );
          _runCommandStack(
            line,
            "? File firestore.indexes.json already exists.",
            commandStack,
            () => process.stdin.write("n\n"),
          );
          _runCommandStack(
            line,
            "? File firestore.rules already exists.",
            commandStack,
            () => process.stdin.write("n\n"),
          );
          _runCommandStack(
            line,
            "? Would you like to delete these indexes?",
            commandStack,
            () => process.stdin.write("n\n"),
          );
          _runCommandStack(
            line,
            "? What file should be used for Firestore indexes?",
            commandStack,
            () => process.stdin.write("\n"),
          );
        },
      );
      // ファイルがfirebaseフォルダに作られないので移動
      label("Move files");
      final firebasercFile = File(".firebaserc");
      if (firebasercFile.existsSync()) {
        await firebasercFile.rename("firebase/.firebaserc");
      }
      final firebaseJsonFile = File("firebase.json");
      if (firebaseJsonFile.existsSync()) {
        await firebaseJsonFile.rename("firebase/firebase.json");
      }
      final firestoreIndexFile = File("firestore.indexes.json");
      if (firestoreIndexFile.existsSync()) {
        await firestoreIndexFile.rename("firebase/firestore.indexes.json");
      }
      final firestoreRulesFile = File("firestore.rules");
      if (firestoreRulesFile.existsSync()) {
        await firestoreRulesFile.rename("firebase/firestore.rules");
      }
      label("Rewriting Rules");
      await const FirestoreRulesCliCode().generateFile("firestore.rules");
    }
  }
  if (enabledDataconnect) {
    if (!firebaseJson.containsKey("dataconnect")) {
      await command(
        "Initialize Data Connect",
        [
          firebaseCommand,
          "init",
          "dataconnect",
          "--project",
          projectId,
        ],
        runInShell: true,
        workingDirectory: "firebase",
        action: (process, line) {
          _runCommandStack(
            line,
            "? Your project already has existing services. Which would you like to set up",
            commandStack,
            () => process.stdin.write("\n"),
          );
        },
      );
      // ファイルがfirebaseフォルダに作られないので移動
      label("Move files");
      final firebasercFile = File(".firebaserc");
      if (firebasercFile.existsSync()) {
        await firebasercFile.rename("firebase/.firebaserc");
      }
      final firebaseJsonFile = File("firebase.json");
      if (firebaseJsonFile.existsSync()) {
        await firebaseJsonFile.rename("firebase/firebase.json");
      }
      final firebaseDataConnect = Directory("dataconnect");
      if (firebaseDataConnect.existsSync()) {
        await firebaseDataConnect.rename("firebase/dataconnect");
      }
    }
    final adapter =
        File("lib/adapters/firebase_data_connect_model_adapter.dart");
    if (!adapter.existsSync()) {
      label("Create Adapter");
      await const FirebaseDataConnectModelAdapterCliCode().generateDartCode(
          "lib/adapters/firebase_data_connect_model_adapter",
          "firebase_data_connect_model_adapter");
      await command(
        "Run the project's build_runner to generate code.",
        [
          flutter,
          "packages",
          "pub",
          "run",
          "build_runner",
          "build",
          "--delete-conflicting-outputs",
        ],
      );
    }
    final connector = File("lib/dataconnect/connector.dart");
    if (!connector.existsSync()) {
      label("Create Dummy Connector");
      await const FirebaseDataConnectDummyConnectorCliCode()
          .generateDartCode("lib/dataconnect/connector", "connector");
    }
  }
  if (enabledStorage) {
    if (!firebaseJson.containsKey("storage")) {
      await command(
          "Initialize Storage",
          [
            firebaseCommand,
            "init",
            "storage",
            "--project",
            projectId,
          ],
          runInShell: true,
          workingDirectory: "firebase", action: (process, line) {
        _runCommandStack(
          line,
          "? Are you ready to proceed?",
          commandStack,
          () => process.stdin.write("y\n"),
        );
        _runCommandStack(
          line,
          "? File storage.rules already exists.",
          commandStack,
          () => process.stdin.write("y\n"),
        );
        _runCommandStack(
          line,
          "? What file should be used for Storage Rules?",
          commandStack,
          () => process.stdin.write("\n"),
        );
      });
      // ファイルがfirebaseフォルダに作られないので移動
      label("Move files");
      final firebasercFile = File(".firebaserc");
      if (firebasercFile.existsSync()) {
        await firebasercFile.rename("firebase/.firebaserc");
      }
      final firebaseJsonFile = File("firebase.json");
      if (firebaseJsonFile.existsSync()) {
        await firebaseJsonFile.rename("firebase/firebase.json");
      }
      final firebaseDataConnect = File("storage.rules");
      if (firebaseDataConnect.existsSync()) {
        await firebaseDataConnect.rename("firebase/storage.rules");
      }
      label("Rewriting Rules");
      await const FirebaseStorageRulesCliCode().generateFile("storage.rules");
      if (enabledCors) {
        label("Set the cors.json");
        await const FirebaseStorageCorsCliCode().generateFile("cors.json");
        await command(
          "Run cors.json deploy",
          [
            gsutil,
            "cors",
            "set",
            "cors.json",
            "gs://$projectId.appspot.com",
          ],
          workingDirectory: "firebase",
        );
      }
    }
    if (firestoreGenerateRulesAndIndexes) {
      await addFlutterImport(
        [
          "masamune_model_firestore_builder",
        ],
        development: true,
      );
    }
  }
  if (enabledHosting) {
    if (!firebaseJson.containsKey("hosting")) {
      await command(
        "Initialize Hosting",
        [
          firebaseCommand,
          "init",
          "hosting",
          "--project",
          projectId,
        ],
        runInShell: true,
        workingDirectory: "firebase",
        action: (process, line) {
          _runCommandStack(
            line,
            "? Are you ready to proceed?",
            commandStack,
            () => process.stdin.write("y\n"),
          );
          _runCommandStack(
            line,
            "? What do you want to use as your public directory?",
            commandStack,
            () => process.stdin.write("hosting\n"),
          );
          _runCommandStack(
            line,
            "? Configure as a single-page app",
            commandStack,
            () {
              if (useFlutter) {
                process.stdin.write("y\n");
              } else {
                process.stdin.write("n\n");
              }
            },
          );
          _runCommandStack(
            line,
            "? Set up automatic builds and deploys with GitHub?",
            commandStack,
            () => process.stdin.write(
              enableActions ? "y\n" : "n\n",
            ),
          );
          _runCommandStack(
            line,
            "? File hosting/index.html already exists. Overwrite?",
            commandStack,
            () => process.stdin.write("n\n"),
          );
          _runCommandStack(
            line,
            "? File hosting/404.html already exists. Overwrite?",
            commandStack,
            () => process.stdin.write("n\n"),
          );
          _runCommandStack(
            line,
            "? For which GitHub repository would you like to set up a GitHub workflow?",
            commandStack,
            () => process.stdin.write("$repositoryFirebaseWebGithubAction\n"),
          );
          _runCommandStack(
            line,
            "? Set up the workflow to run a build script before every deploy?",
            commandStack,
            () => process.stdin.write("y\n"),
          );
          _runCommandStack(
            line,
            "? What script should be run before every deploy?",
            commandStack,
            () => process.stdin.write("\n"),
          );
          _runCommandStack(
            line,
            "? Set up automatic deployment to your site's live channel when a PR is merged?",
            commandStack,
            () => process.stdin.write("n\n"),
          );
        },
      );
      // ファイルがfirebaseフォルダに作られないので移動
      label("Move files");
      final firebasercFile = File(".firebaserc");
      if (firebasercFile.existsSync()) {
        await firebasercFile.rename("firebase/.firebaserc");
      }
      final firebaseJsonFile = File("firebase.json");
      if (firebaseJsonFile.existsSync()) {
        await firebaseJsonFile.rename("firebase/firebase.json");
      }
      final firebaseDataConnect = Directory("hosting");
      if (firebaseDataConnect.existsSync()) {
        await firebaseDataConnect.rename("firebase/hosting");
      }
    }
  }
  if (enabledFunctions) {
    if (!firebaseJson.containsKey("functions")) {
      await command(
        "Initialize Functions",
        [
          firebaseCommand,
          "init",
          "functions",
          "--project",
          projectId,
        ],
        runInShell: true,
        workingDirectory: "firebase",
        action: (process, line) {
          _runCommandStack(
            line,
            "? Are you ready to proceed?",
            commandStack,
            () => process.stdin.write("y\n"),
          );
          _runCommandStack(
            line,
            "? What language would you like to use to write Cloud Functions?",
            commandStack,
            () => process.stdin.write("j\n"),
          );
          _runCommandStack(
            line,
            "? Do you want to use ESLint to catch probable bugs and enforce style?",
            commandStack,
            () => process.stdin.write("y\n"),
          );
          _runCommandStack(
            line,
            "? Do you want to install dependencies with npm now?",
            commandStack,
            () => process.stdin.write("y\n"),
          );
        },
      );
      // ファイルがfirebaseフォルダに作られないので移動
      label("Move files");
      final firebasercFile = File(".firebaserc");
      if (firebasercFile.existsSync()) {
        await firebasercFile.rename("firebase/.firebaserc");
      }
      final firebaseJsonFile = File("firebase.json");
      if (firebaseJsonFile.existsSync()) {
        await firebaseJsonFile.rename("firebase/firebase.json");
      }
      final firebaseDataConnect = Directory("functions");
      if (firebaseDataConnect.existsSync()) {
        await firebaseDataConnect.rename("firebase/functions");
      }
    }
    await command(
      "Package installation.",
      [
        npm,
        "install",
        "@mathrunet/masamune",
      ],
      workingDirectory: "firebase/functions",
    );
    await command(
      "Package installation for dev.",
      [
        npm,
        "install",
        "--save-dev",
        "jest",
        "ts-jest",
        "@types/jest",
      ],
      workingDirectory: "firebase/functions",
    );
    await command(
      "Initialize Jest",
      [
        npx,
        "ts-jest",
        "config:init",
      ],
      workingDirectory: "firebase/functions",
    );
    if (!firebaseFunctionsIndexExists) {
      label("Data replacement for Firebase Functions.");
      await const FirebaseFunctionsIndexCliCode().generateFile("index.ts");
    }
    await command(
      "Fix lint errors.",
      [
        "eslint",
        "--ext",
        ".js,.ts",
        "--fix",
        ".",
      ],
      workingDirectory: "firebase/functions",
    );
    await const FunctionsEslintrcCliCode().generateFile(".eslintrc.js");
  }
  label("Edit config.properties");
  final configPropertiesFile = File("android/config.properties");
  if (!configPropertiesFile.existsSync()) {
    await configPropertiesFile.writeAsString("");
  }
  final configProperties = await configPropertiesFile.readAsLines();
  if (!configProperties
      .any((element) => element.startsWith("flutter.minSdkVersion"))) {
    await configPropertiesFile.writeAsString([
      ...configProperties,
      "flutter.minSdkVersion=${Config.firebaseMinSdkVersion}",
    ].join("\n"));
  }
  label("Edit build.gradle");
  if (!gradle.loadProperties.any((e) => e.name == "configProperties")) {
    gradle.loadProperties.add(
      GradleLoadProperties(
        path: "config.properties",
        name: "configProperties",
        file: "configPropertiesFile",
      ),
    );
  }
  if (!gradle.plugins
      .any((e) => e.plugin == "com.google.gms.google-services")) {
    gradle.plugins.add(
      GradlePlugin(
        plugin: "com.google.gms.google-services",
      ),
    );
  }
  gradle.android?.defaultConfig.minSdkVersion =
      "configProperties[\"flutter.minSdkVersion\"].toInteger()";
  await gradle.save();
  label("Edit settings.gradle.");
  final settingsGradle = SettingsGradle();
  await settingsGradle.load();
  if (!settingsGradle.plugins.any((element) =>
      element.package.startsWith("com.google.gms.google-services"))) {
    settingsGradle.plugins.add(
      SettingsGradlePlugins(
        package: "com.google.gms.google-services",
        version: Config.googleServicesVersion,
        apply: false,
      ),
    );
  }
  await settingsGradle.save();
  label("Rewrite `package.json`");
  final packageJson = File("firebase/functions/package.json");
  if (packageJson.existsSync()) {
    final json = jsonDecodeAsMap(await packageJson.readAsString());
    final scripts = json.getAsMap("scripts");
    scripts["test"] = "firebase emulators:exec --only firestore 'npx jest'";
    json["scripts"] = scripts;
    await packageJson.writeAsString(jsonEncode(json));
  }
  label("Rewrite `.gitignore`.");
  final gitignore = File("firebase/.gitignore");
  if (!gitignore.existsSync()) {
    error("Cannot find `firebase/.gitignore`. Project is broken.");
    return;
  }
  final gitignores = await gitignore.readAsLines();
  if (context.yaml.getAsMap("git").get("ignore_secure_file", true)) {
    if (!gitignores.any((e) => e.startsWith(".env"))) {
      gitignores.add(".env");
    }
  } else {
    gitignores.removeWhere((e) => e.startsWith(".env"));
  }
  await gitignore.writeAsString(gitignores.join("\n"));
  if (enabledFunctions) {
    label("Create functions test folder.");
    final functionsTest = Directory("firebase/functions/test");
    if (!functionsTest.existsSync()) {
      await functionsTest.create();
    }
  }
  if (enabledFirestore) {
    if (firestorePrimaryRemoteIndex) {
      label("Import firestore.indexes.json");
      final firestoreIndexes = File("firebase/firestore.indexes.json");
      final indexData = await command(
        "Import indexes.",
        [
          firebaseCommand,
          "firestore:indexes",
        ],
        workingDirectory: "firebase",
      );
      await firestoreIndexes.writeAsString(indexData);
    }
    if (firebaseJsonFileExists) {
      await command(
        "Run firebase deploy",
        [
          firebaseCommand,
          "deploy",
          if (enableActions) ...[
            "--except",
            "hosting",
          ]
        ],
        workingDirectory: "firebase",
      );
    }
  }
}