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.

コマンドを実行します。

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

Implementation

@override
Future<void> exec(ExecContext context) async {
  if (isLocalApply) {
    validateLocal(context);
  }
  final bin = context.yaml.getAsMap("bin");
  final npm = bin.get("npm", "npm");
  final wrangler = bin.get("wrangler", "wrangler");
  final cloudflare = context.yaml.getAsMap("cloudflare");
  final workers = cloudflare.getAsMap("workers");
  final enabledWorkers = workers.get("enable", false);
  final enableFirebaseAuth = workers.get("enable_firebase_auth", false);
  final smartPlacement = workers.get("smart_placement", false);
  final enabledRegion = isCloudflareRegionWorkerEnabled(context.yaml);
  final regionPlacement = _placementRegion(
    workers.getAsMap("region")["placement"],
  );
  final edgeCustomDomain = workers.get("custom_domain", "").trim();
  final regionCustomDomain =
      workers.getAsMap("region").get("custom_domain", "").trim();
  final rootRegionPlacement = _placementRegion(
    context.flavorContext?.yamlValue(
          const ["cloudflare", "workers", "region", "placement"],
          flavor: KatanaFlavor.prod,
        ) ??
        workers.getAsMap("region")["placement"],
  );
  final firebase = context.yaml.getAsMap("firebase");
  final firebaseProjectId = firebase.get("project_id", "");
  final pages = cloudflare.getAsMap("pages");
  final enabledPages = pages.get("enable", false);
  final pubspec = File("pubspec.yaml");
  final pubspecs = loadYaml(await pubspec.readAsString()) as Map;
  final projectIdFromPubspec = pubspecs.get("name", "");
  final projectIdFromYaml = cloudflare.get("project_id", "");
  final projectId =
      projectIdFromYaml.isEmpty ? projectIdFromPubspec : projectIdFromYaml;
  final rootProjectIdFromYaml = context.flavorContext?.yamlValue(
        const ["cloudflare", "project_id"],
        flavor: KatanaFlavor.prod,
      )?.toString() ??
      projectIdFromYaml;
  final rootProjectId = rootProjectIdFromYaml.isEmpty
      ? projectIdFromPubspec
      : rootProjectIdFromYaml;
  final flavor = context.flavorContext?.flavor.name ?? "prod";
  if ((enabledWorkers || enabledPages) && projectId.isEmpty) {
    error(
        "Project ID is not specified. Please enter [project_id] in `katana.yaml`.");
    return;
  }
  if (enableFirebaseAuth && firebaseProjectId.isEmpty) {
    error(
      "Firebase project ID is not specified. Please enter [project_id] in `katana.yaml`.",
    );
    return;
  }
  // Legacy entrypoint (also the file generated by `wrangler init`).
  final workerIndexFile = File(cloudflareLegacyEntryPath);
  final edgeEntryFile = File(cloudflareEdgeEntryPath);
  final regionEntryFile = File(cloudflareRegionEntryPath);
  final pagesIndexFile = File("cloudflare/public/index.html");
  final wranglerJsonc = File("cloudflare/wrangler.jsonc");
  final regionWranglerJsonc =
      File("cloudflare/$cloudflareRegionWranglerConfig");
  if (enabledRegion) {
    for (final name in ["$projectId-region", "$rootProjectId-region"]) {
      if (name.length > 63) {
        throw StateError(
          "The region Worker name `$name` exceeds 63 characters. Shorten [cloudflare]->[project_id]. "
          "region Worker名`$name`が63文字を超えています。[cloudflare]->[project_id]を短くしてください。",
        );
      }
    }
  }
  if (enabledWorkers) {
    // Existing projects used `src/index.ts`; move it to `src/edge.ts` first.
    await migrateCloudflareLegacyEntry();
  }
  final previousWranglerSource =
      wranglerJsonc.existsSync() ? await wranglerJsonc.readAsString() : null;
  final cloudflareDir = Directory("cloudflare");
  final workerRulesFile = File("cloudflare/src/rules.json");
  if (!cloudflareDir.existsSync()) {
    await cloudflareDir.create();
  }
  if (enabledWorkers || enabledPages) {
    if (!wranglerJsonc.existsSync()) {
      final generatedDir = Directory("cloudflare/$projectId");
      if (!generatedDir.existsSync()) {
        await command(
          "Initialize Cloudflare Workers",
          [wrangler, "init", projectId, "--yes", "--cwd=cloudflare"],
          runInShell: true,
        );
      }
      // Keep existing application files when merging the Wrangler scaffold.
      label("Rename files");
      await mergeCloudflareScaffold(generatedDir, cloudflareDir);
      await generatedDir.delete(recursive: true);
      if (workerIndexFile.existsSync()) {
        await workerIndexFile.delete();
      }
      if (pagesIndexFile.existsSync()) {
        await pagesIndexFile.delete();
      }
    }
    label("Rewrite `.gitignore`.");
    final gitignore = File("cloudflare/.gitignore");
    if (!gitignore.existsSync()) {
      error("Cannot find `cloudflare/.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("*-*-*.json"))) {
        gitignores.add("*-*-*.json");
      }
    } else {
      gitignores.removeWhere((e) => e.startsWith("*-*-*.json"));
    }
    await gitignore.writeAsString(gitignores.join("\n"));
  }

  if (enabledWorkers) {
    final functionsDir = Directory("cloudflare/src/functions");
    if (!functionsDir.existsSync()) {
      await functionsDir.create();
    }
    if (!edgeEntryFile.existsSync()) {
      await CloudflareWorkersEntryCliCode(
        entry: "edge",
        firebaseProjectId: enableFirebaseAuth ? firebaseProjectId : null,
      ).generateFile("edge.ts");
    }
    if (!workerRulesFile.existsSync()) {
      await const CloudflareWorkersRulesCliCode().generateFile("rules.json");
    }
    await CloudflareWranglerCliCode(
      projectId: rootProjectId,
      smartPlacement: smartPlacement,
    ).generateFile("wrangler.jsonc");
    final generatedWranglerSource = await wranglerJsonc.readAsString();
    // Existing Wrangler files are the source of truth. Rebuilding from the
    // template would discard routes, triggers, bindings, and other custom
    // root settings that Katana does not own.
    final restoredWranglerSource =
        previousWranglerSource ?? generatedWranglerSource;
    var synchronizedWranglerSource =
        WranglerEnvironmentSynchronizer.synchronize(
      restoredWranglerSource,
      flavor: flavor,
      workerName: projectId,
      rootWorkerName: rootProjectId,
    );
    if (enableFirebaseAuth) {
      synchronizedWranglerSource =
          WranglerEnvironmentSynchronizer.upsertVariables(
        synchronizedWranglerSource,
        flavor: flavor,
        values: {"FIREBASE_PROJECT_ID": firebaseProjectId},
      );
    }
    synchronizedWranglerSource = _syncCustomDomainRoutes(
      synchronizedWranglerSource,
      flavor: flavor,
      customDomain: edgeCustomDomain,
    );
    await wranglerJsonc.writeAsString(synchronizedWranglerSource);
    await installMissingCloudflarePackages(
      npm: npm,
      packages: const [
        "hono",
        "@mathrunet/masamune",
        "@mathrunet/masamune_cloudflare",
      ],
    );
    await addFlutterImport(
      [
        "masamune_functions_cloudflare",
      ],
    );
    if (enabledRegion) {
      await _applyRegionWorker(
        wrangler: wrangler,
        flavor: flavor,
        projectId: projectId,
        rootProjectId: rootProjectId,
        regionEntryFile: regionEntryFile,
        regionWranglerJsonc: regionWranglerJsonc,
        firebaseProjectId: enableFirebaseAuth ? firebaseProjectId : null,
        placement: regionPlacement,
        rootPlacement: rootRegionPlacement,
        customDomain: regionCustomDomain,
      );
    }
  }
  if (enabledPages) {
    if (!pagesIndexFile.existsSync()) {
      await const CloudflarePagesIndexCliCode().generateFile("index.html");
    }
  }
}