run method

  1. @override
Future<void> run()

Implementation

@override
Future<void> run() => guarded(() async {
  final root = await findProjectRoot();
  if (root == null) {
    throw UsageException('Could not locate a pubspec.yaml.', usage);
  }

  final target = results?['target'] as String? ?? 'cloudflare';
  if (target != 'cloudflare' && target != 'netlify' && target != 'vercel') {
    throw UsageException('Unsupported deployment target: $target', usage);
  }
  final cloudflareFactory =
      results?['cloudflare-factory'] as String? ?? 'engine';
  if (target != 'cloudflare' && cloudflareFactory != 'engine') {
    throw UsageException(
      '--cloudflare-factory is only supported for Cloudflare deployments.',
      usage,
    );
  }
  final ssrEntry = (results?['ssr-entry'] as String?)?.trim();
  if (ssrEntry != null && ssrEntry.isEmpty) {
    throw UsageException('--ssr-entry cannot be empty.', usage);
  }
  if (ssrEntry != null && target != 'cloudflare') {
    throw UsageException(
      '--ssr-entry is only supported for Cloudflare deployments.',
      usage,
    );
  }
  final dartDurableObjects = _parseDurableObjectBindings(target);
  final variables = _parseCloudflareVariables(target);
  final containers = _parseContainerBindings(target);
  final durableObjects = _mergeDurableObjectBindings(
    dartDurableObjects,
    containers,
  );
  final d1Bindings = _parseD1Bindings(target);
  final r2Bindings = _parseSimpleCloudflareBindings(
    target,
    option: 'r2',
    resource: 'R2 bucket',
    valueHelp: 'BINDING=BUCKET_NAME',
  );
  final queueBindings = _parseSimpleCloudflareBindings(
    target,
    option: 'queue',
    resource: 'Queue',
    valueHelp: 'BINDING=QUEUE_NAME',
  );
  final serviceBindings = _parseSimpleCloudflareBindings(
    target,
    option: 'service',
    resource: 'Service',
    valueHelp: 'BINDING=SERVICE_NAME',
  );
  final workflowBindings = _parseWorkflowBindings(target);
  final secretsStoreBindings = _parseSecretsStoreBindings(target);
  _validateCloudflareBindingNames(
    variables: variables.keys,
    bindingNames: [
      ...durableObjects.map((binding) => binding.bindingName),
      ...d1Bindings.map((binding) => binding.bindingName),
      ...r2Bindings.map((binding) => binding.bindingName),
      ...queueBindings.map((binding) => binding.bindingName),
      ...serviceBindings.map((binding) => binding.bindingName),
      ...workflowBindings.map((binding) => binding.bindingName),
      ...secretsStoreBindings.map((binding) => binding.bindingName),
    ],
  );

  final packageName = await readPackageName(root);
  if (packageName == null) {
    throw UsageException(
      'pubspec.yaml does not contain a package name.',
      usage,
    );
  }

  await _ensureRoutedNode(root);
  if (target == 'netlify') {
    await _deployNetlify(root, packageName);
    return;
  }
  if (target == 'vercel') {
    await _deployVercel(root, packageName);
    return;
  }

  final requestedName = (results?['name'] as String?)?.trim();
  final workerName = _sanitizeWorkerName(
    requestedName == null || requestedName.isEmpty
        ? packageName
        : requestedName,
  );
  final date =
      results?['compatibility-date'] as String? ??
      DateTime.now().toIso8601String().substring(0, 10);
  final dryRun = results?['dry-run'] as bool? ?? false;
  final keepVars = results?['keep-vars'] as bool? ?? false;
  final entry = _resolveImport(packageName, results?['entry'] as String?);

  final buildRoot = root.fileSystem.directory(
    p.join(root.path, '.dart_tool', 'routed', 'deploy', target),
  );
  await buildRoot.create(recursive: true);

  final dartEntry = root.fileSystem.file(
    p.join(buildRoot.path, 'worker_entry.dart'),
  );
  final jsOutput = root.fileSystem.file(
    p.join(buildRoot.path, 'worker.dart.js'),
  );
  final workerOutput = root.fileSystem.file(
    p.join(buildRoot.path, 'worker.js'),
  );
  final configOutput = root.fileSystem.file(
    p.join(buildRoot.path, 'wrangler.jsonc'),
  );

  String? bundledSsrEntry;
  if (ssrEntry != null) {
    final source = root.fileSystem.file(
      p.isAbsolute(ssrEntry) ? ssrEntry : p.join(root.path, ssrEntry),
    );
    if (!source.existsSync()) {
      throw UsageException(
        'SSR entry does not exist: ${source.path}',
        usage,
      );
    }
    final buildDirectory = source.parent;
    final destination = root.fileSystem.directory(
      p.join(buildRoot.path, 'frontend'),
    );
    if (_pathsOverlap(buildDirectory.path, destination.path)) {
      throw UsageException(
        'SSR build directory cannot overlap the generated frontend '
        'directory: ${destination.path}',
        usage,
      );
    }
    _copyFrontendBuild(buildDirectory, destination);
    bundledSsrEntry = p.join('frontend', p.basename(source.path));
  }

  await dartEntry.writeAsString(
    generateCloudflareWorkerEntry(
      importPath: entry,
      factory: cloudflareFactory,
      durableObjectClasses: dartDurableObjects.map(
        (binding) => binding.className,
      ),
    ),
  );
  await _runDart(root, [
    'compile',
    'js',
    dartEntry.path,
    '-o',
    jsOutput.path,
    '-O2',
  ], label: 'Compiling Cloudflare Worker');
  await workerOutput.writeAsString(
    generateCloudflareWorkerWrapper(
      jsOutput.path,
      dartDurableObjects.map((binding) => binding.className),
      containerPorts: {
        for (final container in containers)
          container.className: container.port,
      },
      ssrEntry: bundledSsrEntry,
    ),
  );

  final config = <String, Object?>{
    r'$schema':
        'https://developers.cloudflare.com/workers/wrangler/config-schema.json',
    'name': workerName,
    'main': p.basename(workerOutput.path),
    'compatibility_date': date,
    'compatibility_flags': [
      'nodejs_compat',
      if (bundledSsrEntry != null) 'global_fetch_strictly_public',
    ],
  };
  if (durableObjects.isNotEmpty) {
    config['durable_objects'] = {
      'bindings': [
        for (final binding in durableObjects)
          {'name': binding.bindingName, 'class_name': binding.className},
      ],
    };
    config['migrations'] = [
      {
        'tag': 'routed-v1',
        'new_sqlite_classes': [
          for (final binding in durableObjects) binding.className,
        ],
      },
    ];
  }
  if (containers.isNotEmpty) {
    config['containers'] = [
      for (final container in containers)
        {
          'class_name': container.className,
          'image': _containerImageReference(root, buildRoot, container.image),
          if (container.maxInstances != null)
            'max_instances': container.maxInstances,
        },
    ];
  }
  if (d1Bindings.isNotEmpty) {
    config['d1_databases'] = [
      for (final binding in d1Bindings)
        {
          'binding': binding.bindingName,
          'database_name': binding.databaseName,
          'database_id': binding.databaseId,
        },
    ];
  }
  if (r2Bindings.isNotEmpty) {
    config['r2_buckets'] = [
      for (final binding in r2Bindings)
        {'binding': binding.bindingName, 'bucket_name': binding.resourceName},
    ];
  }
  if (queueBindings.isNotEmpty) {
    config['queues'] = {
      'producers': [
        for (final binding in queueBindings)
          {'binding': binding.bindingName, 'queue': binding.resourceName},
      ],
    };
  }
  if (serviceBindings.isNotEmpty) {
    config['services'] = [
      for (final binding in serviceBindings)
        {'binding': binding.bindingName, 'service': binding.resourceName},
    ];
  }
  if (workflowBindings.isNotEmpty) {
    config['workflows'] = [
      for (final binding in workflowBindings)
        {
          'name': binding.workflowName,
          'binding': binding.bindingName,
          'class_name': binding.className,
          if (binding.scriptName != null) 'script_name': binding.scriptName,
        },
    ];
  }
  if (secretsStoreBindings.isNotEmpty) {
    config['secrets_store_secrets'] = [
      for (final binding in secretsStoreBindings)
        {
          'binding': binding.bindingName,
          'store_id': binding.storeId,
          'secret_name': binding.secretName,
        },
    ];
  }
  if (bundledSsrEntry != null) {
    config['assets'] = {
      'directory': 'frontend',
      'binding': 'ASSETS',
      'run_worker_first': true,
    };
  }
  if (variables.isNotEmpty) {
    config['vars'] = variables;
  }
  await configOutput.writeAsString(
    const JsonEncoder.withIndent('  ').convert(config),
  );

  final wranglerArgs = <String>['wrangler@latest', 'deploy'];
  if (dryRun) wranglerArgs.add('--dry-run');
  if (keepVars) wranglerArgs.add('--keep-vars');
  wranglerArgs
    ..add('--config')
    ..add(configOutput.path);
  await _runNpx(root, wranglerArgs, label: 'Deploying Cloudflare Worker');

  logger.info('Routed Cloudflare deployment complete: $workerName');
});