blob: 0033886a7573e07224d0f61dd0a972e35524b3b3 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'extract.dart';
import 'inject.dart';
import 'transform.dart';
/// An excerpt updater that can run over the files with [validTargetExtensions]
/// in the specified paths with [update], and updates their code excerpts
/// according to their injection instructions.
final class Updater {
/// A pattern to check for paths to ignore or not descend in to.
static final RegExp _ignoreHiddenPathsPattern = RegExp(r'(^|/)\..*($|/)');
/// The transforms to run on each retrieved region.
final Iterable<Transform> defaultTransforms;
/// Patterns of paths to avoid updating.
final Iterable<RegExp> excludePaths;
/// Extensions of files to consider for injection updating.
final Set<String> validTargetExtensions;
/// The path to the directory of the source files to pull regions from.
final String baseSourcePath;
/// The content to include in plaster replacements by default.
final String defaultPlasterContent;
/// Whether to continue updating or checking for updates if
/// an error is experienced.
final bool continueOnError;
/// Create an [Updater] that can be used to [update] a collection of files
/// with the specified [validTargetExtensions] but
/// not matching [excludePaths].
///
/// Regions in injection instructions are pulled from files
/// within the [baseSourcePath].
///
/// If [withDefaultExclusions] is `true`, files and directories
/// hidden by starting with a `.` are excluded as well.
///
/// The [defaultTransforms] specified are run on each region
/// before transforms provided by instructions on the page.
Updater({
required this.baseSourcePath,
required this.validTargetExtensions,
this.continueOnError = false,
this.defaultPlasterContent = '...',
this.defaultTransforms = const [],
Iterable<RegExp> excludePaths = const [],
bool withDefaultExclusions = true,
}) : excludePaths = [
if (withDefaultExclusions) _ignoreHiddenPathsPattern,
...excludePaths,
];
/// Use the configuration of this [Updater] to process the
/// file at the [pathToUpdate], optionally making updates
/// if [makeUpdates] is `true`.
@useResult
Future<ExcerptUpdateResult> update(
String pathToUpdate, {
bool makeUpdates = true,
}) async {
final pathsToUpdate = (await _findTargetFiles(pathToUpdate)).toList()
..sort();
final warnings = <String>[];
var excerptsVisited = 0;
var excerptsNeedingUpdates = 0;
var filesVisited = 0;
var madeUpdates = false;
final errors = <String>[];
final extractor = ExcerptExtractor();
for (final currentPath in pathsToUpdate) {
final fileUpdater = FileUpdater(
currentPath,
baseSourcePath: baseSourcePath,
defaultPlasterContent: defaultPlasterContent,
defaultTransforms: defaultTransforms,
extractor: extractor,
);
try {
final results = await fileUpdater.process();
warnings.addAll(results.warnings);
excerptsVisited += results.excerptsVisited;
excerptsNeedingUpdates += results.excerptsUpdated.length;
if (makeUpdates && results.needsUpdates) {
madeUpdates = true;
await results.writeUpdates();
}
filesVisited += 1;
} on InjectionException catch (e) {
errors.add(e.toString());
if (!continueOnError) {
break;
}
}
}
return ExcerptUpdateResult._(
excerptsVisited: excerptsVisited,
excerptsNeedingUpdates: excerptsNeedingUpdates,
madeUpdates: madeUpdates,
filesVisited: filesVisited,
totalFilesToVisit: pathsToUpdate.length,
warnings: warnings,
errors: errors,
);
}
@useResult
Future<Iterable<String>> _findTargetFiles(String pathToEntity) async {
if (_shouldExclude(pathToEntity)) return const [];
final entityType = await FileSystemEntity.type(pathToEntity);
if (entityType == FileSystemEntityType.directory) {
return [
await for (final entity in Directory(
pathToEntity,
).list(followLinks: false))
...await _findTargetFiles(entity.path),
];
} else if (entityType == FileSystemEntityType.file &&
validTargetExtensions.contains(path.extension(pathToEntity))) {
return [pathToEntity];
}
return const [];
}
bool _shouldExclude(String path) =>
excludePaths.any((excludePattern) => path.contains(excludePattern));
}
/// The result of running a [Updater] on a collection of files.
@immutable
final class ExcerptUpdateResult {
/// The non-fatal warnings experienced when
/// processing the specified collection of files.
final Iterable<String> warnings;
/// The fatal errors experienced when
/// processing the specified collection of files.
final Iterable<String> errors;
/// The amount of code excerpt injections visited when
/// processing the specified collection of files.
///
/// Note this may not be all excerpts in the collection
/// as a fatal error may have been experienced.
final int excerptsVisited;
/// The amount of code excerpt injections that needed updates when
/// processing the specified collection of files.
///
/// Note this may not be all excerpts needing updates in the collection
/// as a fatal error may have been experienced.
final int excerptsNeedingUpdates;
/// The amount of files visited successfully without errors when
/// processing the specified collection of files.
///
/// If this differs from [totalFilesToVisit], that means
/// there was a fatal error during processing.
final int filesVisited;
/// The total amount of files in the specified collection of files
/// that were meant to be visited.
final int totalFilesToVisit;
/// If the [Updater] wrote updates to the files needing them.
final bool madeUpdates;
ExcerptUpdateResult._({
required this.excerptsVisited,
required this.excerptsNeedingUpdates,
required this.filesVisited,
required this.totalFilesToVisit,
required this.madeUpdates,
this.warnings = const [],
this.errors = const [],
});
}