stampBuildDirectory function

StampResult stampBuildDirectory(
  1. String buildDir, {
  2. String? pubspecPath,
  3. String? appVersion,
  4. bool force = false,
  5. DateTime? now,
})

Stamps a flutter build web output directory in place.

  • Computes a SHA-256 content hash of the build (ignoring version.json and this tool's own edits) and a build ID <hash16>-<UTC timestamp>.
  • Writes/merges version.json with buildId, builtAt, appVersion and contentHash, preserving keys Flutter already put there.
  • Cache-busts main.dart.js, flutter_bootstrap.js and flutter.js references in index.html and flutter_bootstrap.js with ?v=<id>.
  • Injects <meta name="web-update-guard-build-id"> into index.html.

Re-running on unchanged output is a byte-for-byte no-op unless force is set, which issues a new timestamp. appVersion overrides the version read from pubspecPath (default: <buildDir>/../../pubspec.yaml, then ./pubspec.yaml, then Flutter's own version.json fields). now exists for tests.

Throws a StampException when the directory or index.html is missing or malformed.

Implementation

StampResult stampBuildDirectory(
  String buildDir, {
  String? pubspecPath,
  String? appVersion,
  bool force = false,
  DateTime? now,
}) {
  final Directory dir = Directory(buildDir);
  if (!dir.existsSync()) {
    throw StampException('Build directory "$buildDir" does not exist.');
  }
  final File index = File(p.join(dir.path, 'index.html'));
  if (!index.existsSync()) {
    throw StampException(
      'No index.html in "$buildDir". Run `flutter build web` first.',
    );
  }
  final File bootstrap = File(p.join(dir.path, 'flutter_bootstrap.js'));
  final File versionFile = File(p.join(dir.path, 'version.json'));

  final String indexText = index.readAsStringSync();
  final String? bootstrapText = bootstrap.existsSync()
      ? bootstrap.readAsStringSync()
      : null;

  final (String contentHash, int hashedCount) = _hashBuild(
    dir,
    indexText,
    bootstrapText,
  );
  final Map<String, Object?> existing = _readJsonObject(versionFile);

  DateTime builtAt;
  bool reused = false;
  final Object? previousAt = existing['builtAt'];
  final DateTime? parsedPrevious = previousAt is String
      ? DateTime.tryParse(previousAt)
      : null;
  if (!force &&
      existing['contentHash'] == contentHash &&
      parsedPrevious != null) {
    builtAt = parsedPrevious.toUtc();
    reused = true;
  } else {
    final DateTime t = (now ?? DateTime.now()).toUtc();
    builtAt = DateTime.utc(t.year, t.month, t.day, t.hour, t.minute, t.second);
  }

  final String buildId = formatBuildId(contentHash, builtAt);
  final BuildInfo info = BuildInfo(
    buildId: buildId,
    builtAt: builtAt,
    appVersion:
        appVersion ??
        _resolveAppVersion(dir, pubspecPath) ??
        _flutterVersion(existing),
    contentHash: contentHash,
  );

  final List<String> written = <String>[];
  final String stampedIndex;
  try {
    stampedIndex = stampIndexHtml(indexText, buildId);
  } on FormatException catch (e) {
    throw StampException('Cannot stamp index.html: ${e.message}');
  }
  if (stampedIndex != indexText) {
    index.writeAsStringSync(stampedIndex);
    written.add('index.html');
  }
  if (bootstrapText != null) {
    final String stamped = stampBootstrapJs(bootstrapText, buildId);
    if (stamped != bootstrapText) {
      bootstrap.writeAsStringSync(stamped);
      written.add('flutter_bootstrap.js');
    }
  }

  final Map<String, Object?> merged = <String, Object?>{
    ...existing,
    ...info.toJson(),
  };
  final String versionJson =
      '${const JsonEncoder.withIndent('  ').convert(merged)}\n';
  if (!versionFile.existsSync() ||
      versionFile.readAsStringSync() != versionJson) {
    versionFile.writeAsStringSync(versionJson);
    written.add('version.json');
  }

  return StampResult(
    info: info,
    writtenFiles: written,
    reusedBuiltAt: reused,
    hashedFileCount: hashedCount,
  );
}