// Copyright (c) 2018, 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. /// Configuration for using `package:build`-compatible build systems. /// /// See: /// * [build_runner](https://pub.dev/packages/build_runner) /// /// This library is **not** intended to be imported by typical end-users unless /// you are creating a custom compilation pipeline. See documentation for /// details, and `build.yaml` for how these builders are configured by default. library; import 'package:build/build.dart'; import 'package:glob/glob.dart'; import 'package:path/path.dart' as p; import 'src/builder.dart'; import 'src/utils.dart'; const _outputExtensions = '.g.dart'; const _partFiles = '.g.part'; const _defaultExtensions = { '.dart': [_outputExtensions], }; Builder combiningBuilder([BuilderOptions options = BuilderOptions.empty]) { final optionsMap = Map.from(options.config); final includePartName = optionsMap.remove('include_part_name') as bool?; final ignoreForFile = Set.from( optionsMap.remove('ignore_for_file') as List? ?? [], ); final preamble = optionsMap.remove('preamble') as String? ?? ''; final buildExtensions = validatedBuildExtensionsFrom( optionsMap, _defaultExtensions, ); final header = optionsMap.remove('header') as String?; final builder = CombiningBuilder( includePartName: includePartName, ignoreForFile: ignoreForFile, preamble: preamble, buildExtensions: buildExtensions, header: header, ); if (optionsMap.isNotEmpty) { log.warning('These options were ignored: `$optionsMap`.'); } return builder; } PostProcessBuilder partCleanup(BuilderOptions options) => const FileDeletingBuilder(['.g.part']); /// A [Builder] which combines part files generated from [SharedPartBuilder]. /// /// This will glob all files of the form `.*.g.part`. class CombiningBuilder implements Builder { final bool _includePartName; final Set _ignoreForFile; final String _preamble; @override final Map> buildExtensions; final String? header; /// Returns a new [CombiningBuilder]. /// /// If [includePartName] is `true`, the name of each source part file /// is output as a comment before its content. This can be useful when /// debugging build issues. /// /// If [header] is provided, it will be output as a comment before the /// generated code. const CombiningBuilder({ bool? includePartName, Set? ignoreForFile, String? preamble, this.buildExtensions = _defaultExtensions, this.header, }) : _includePartName = includePartName ?? false, _ignoreForFile = ignoreForFile ?? const {}, _preamble = preamble ?? ''; @override Future build(BuildStep buildStep) async { // Pattern used for `findAssets`, which must be glob-compatible final assetPath = buildStep.inputId.path; final pattern = '${p.withoutExtension(Glob.quote(assetPath))}.*$_partFiles'; final inputBaseName = p.basenameWithoutExtension( buildStep.inputId.pathSegments.last, ); // Pattern used to ensure items are only considered if they match // [file name without extension].[valid part id].[part file extension] final restrictedPattern = RegExp( [ '^', // start of string RegExp.escape(inputBaseName), // file name, without extension r'\.', // `.` character partIdRegExpLiteral, // A valid part ID RegExp.escape(_partFiles), // the ending part extension '\$', // end of string ].join(), ); final assetIds = await buildStep .findAssets(Glob(pattern)) .where((id) => restrictedPattern.hasMatch(id.pathSegments.last)) .toList() ..sort(); final assets = await Stream.fromIterable(assetIds) .asyncMap((id) async { var content = (await buildStep.readAsString(id)).trim(); if (_includePartName) { content = '// Part: ${id.pathSegments.last}\n$content'; } return content; }) .where((s) => s.isNotEmpty) .join('\n\n'); if (assets.isEmpty) return; final inputLibrary = await buildStep.inputLibrary; final outputId = buildStep.allowedOutputs.single; final partOfUri = uriOfPartial(inputLibrary, buildStep.inputId, outputId); // Ensure that the input has a correct `part` statement. final libraryUnit = await buildStep.resolver.compilationUnitFor( buildStep.inputId, ); final part = computePartUrl(buildStep.inputId, outputId); if (!hasExpectedPartDirective(libraryUnit, part)) { log.warning( '$part must be included as a part directive in ' 'the input library with:\n part \'$part\';', ); return; } final headerBit = (header ?? defaultFileHeader).trim(); final headerContent = headerBit.isEmpty ? languageOverrideForLibrary(inputLibrary) : '$headerBit\n${languageOverrideForLibrary(inputLibrary)}'; final ignoreForFile = _ignoreForFile.isEmpty ? '' : '// ignore_for_file: ${_ignoreForFile.join(', ')}\n'; final preamble = _preamble.isEmpty ? '' : '$_preamble\n'; final optionalBits = [headerContent, ignoreForFile, preamble] .map((e) => e.trim()) .where((e) => e.isNotEmpty) .map((e) => '$e\n\n') .toList(); final output = ''' ${optionalBits.join()}part of '$partOfUri'; $assets '''; await buildStep.writeAsString(outputId, output); } }