buildEmbeddedFlutter method

Future<BuildStepResult> buildEmbeddedFlutter()

Build the Flutter web guest used by arcane_jaspr_flutter_embed.

Steps:

  1. flutter build web --release --base-href=<mount>/ in the <embeddedFlutterPackageName>/ directory.
  2. Copy build/web/** from the guest into <webPackageName>/web<mount>/ so it ships with the Jaspr build.
  3. Uncomment the Flutter-bootstrap script tag in the host's web/index.html (idempotent — finds the ORACULAR_FLUTTER_BOOTSTRAP_BEGIN / END markers and replaces the commented block in between).

Implementation

Future<BuildStepResult> buildEmbeddedFlutter() async {
  final String label =
      'Flutter web build for embed (mount: ${config.embeddedFlutterMount})';

  if (config.template != TemplateType.arcaneJasprFlutterEmbed &&
      config.jasprRenderMode != JasprRenderMode.embed) {
    return BuildStepResult.skipped(
      BuildStepKind.embedFlutterGuest,
      label,
      reason: 'Project does not use the Jaspr+Flutter embed template.',
    );
  }

  final Directory guestDir = Directory(_embeddedFlutterPath);
  if (!guestDir.existsSync()) {
    return BuildStepResult.failed(
      BuildStepKind.embedFlutterGuest,
      label,
      reason: 'Embedded Flutter project not found: ${guestDir.path}',
    );
  }

  // Flutter's `--base-href` must start AND end with `/`. Normalize the
  // user-configured mount (e.g. `/app`, `app`, `/app/`) to the
  // canonical `/app/` form.
  final String baseHref = _normalizedBaseHref(config.embeddedFlutterMount);
  info('Building Flutter web guest with --base-href=$baseHref...');

  final ProcessResult? buildResult = await _runner.runWithRetry(
    'flutter',
    <String>['build', 'web', '--release', '--base-href=$baseHref'],
    workingDirectory: guestDir.path,
    operationName: 'flutter build web (embed)',
  );

  if (buildResult == null || !buildResult.success) {
    return BuildStepResult.failed(
      BuildStepKind.embedFlutterGuest,
      label,
      reason: buildResult?.stderr.trim() ?? 'flutter build web failed.',
    );
  }

  // Copy the Flutter web bundle into the Jaspr host's `web/<mount>/`.
  final Directory guestBuild =
      Directory(p.join(guestDir.path, 'build', 'web'));
  if (!guestBuild.existsSync()) {
    return BuildStepResult.failed(
      BuildStepKind.embedFlutterGuest,
      label,
      reason: 'Flutter web build did not emit ${guestBuild.path}.',
    );
  }

  final String mount = baseHref.substring(1, baseHref.length - 1);
  final Directory hostMount =
      Directory(p.join(_jasprAppPath, 'web', mount));

  try {
    if (hostMount.existsSync()) {
      await hostMount.delete(recursive: true);
    }
    await hostMount.create(recursive: true);
    await _copyDirectoryRecursive(guestBuild, hostMount);
  } on FileSystemException catch (e) {
    return BuildStepResult.failed(
      BuildStepKind.embedFlutterGuest,
      label,
      reason: 'Copy of Flutter build into Jaspr host failed: ${e.message}',
    );
  }

  // Make the bootstrap script visible in the Jaspr index.html.
  try {
    await _enableFlutterBootstrapScript(mount: mount);
  } on FileSystemException catch (e) {
    // Non-fatal: the bundle is in place, the user can manually edit
    // index.html. Surface as a warning in the report.
    warn('Failed to enable Flutter bootstrap injection: ${e.message}');
    return BuildStepResult(
      kind: BuildStepKind.embedFlutterGuest,
      label: label,
      status: BuildStepStatus.success,
      message:
          'Flutter web bundle copied, but index.html bootstrap edit failed: '
          '${e.message}',
      outputPath: hostMount.path,
    );
  }

  return BuildStepResult.success(
    BuildStepKind.embedFlutterGuest,
    label,
    outputPath: hostMount.path,
  );
}