Skip to content

Commit

Permalink
Allow configurable outputs for combining builder
Browse files Browse the repository at this point in the history
  • Loading branch information
simolus3 committed Aug 19, 2021
1 parent 2ddf63c commit 76566af
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 45 deletions.
5 changes: 5 additions & 0 deletions source_gen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.1.0-dev

* Add the `build_extensions` option to `combining_builder`, allowing output
files to be generated into a different directory.

## 1.0.5

* Fix a bug with reviving constant expressions which are fields defined on a
Expand Down
36 changes: 36 additions & 0 deletions source_gen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,41 @@ targets:
If you provide a builder that uses `SharedPartBuilder` and `combining_builder`,
you should document this feature for your users.

### Generating files in different directories

When using shared-part builders which apply the `combining_builder` as part of
the build, the output location for an input file can be changed.
By default, a `.g.dart` file next to the input is generated.

To change this, set the `build_extensions` option on the combining builder. In
the options, `build_extensions` is a map from `String` to `String`, where the
key is matches inputs and the value is a single build output.
For more details on build extensions, see [the docs in the build package][outputs].

For example, you can use these options to generate files under `lib/generated`
with the following build configuration:

```yaml
targets:
$default:
builders:
source_gen|combining_builder:
options:
build_extensions:
'^lib/{{}}.dart': 'lib/generated/{{}}.g.dart'
```

Remember to change the `part` statement in the input to refer to the correct
output file in the other directory.

Note that builder options are part of `source_gen`'s public api! When using
them in a build configuration, always add a dependency on `source_gen` as well:

```yaml
dev_dependencies:
source_gen: ^1.1.0
```

## FAQ

### What is the difference between `source_gen` and [build][]?
Expand Down Expand Up @@ -148,3 +183,4 @@ wraps a single Generator to make a `Builder` which creates Dart library files.
[Trivial example]: https://github.com/dart-lang/source_gen/blob/master/source_gen/test/src/comment_generator.dart
[Full example package]: https://github.com/dart-lang/source_gen/tree/master/example
[example usage]: https://github.com/dart-lang/source_gen/tree/master/example_usage
[outputs]: https://github.com/dart-lang/build/blob/master/docs/writing_a_builder.md#configuring-outputs
62 changes: 56 additions & 6 deletions source_gen/lib/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ 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<String, dynamic>.from(options.config);
Expand All @@ -29,10 +32,12 @@ Builder combiningBuilder([BuilderOptions options = BuilderOptions.empty]) {
final ignoreForFile = Set<String>.from(
optionsMap.remove('ignore_for_file') as List? ?? <String>[],
);
final buildExtensions = _validatedBuildExtensionsFrom(optionsMap);

final builder = CombiningBuilder(
includePartName: includePartName,
ignoreForFile: ignoreForFile,
buildExtensions: buildExtensions,
);

if (optionsMap.isNotEmpty) {
Expand All @@ -41,6 +46,41 @@ Builder combiningBuilder([BuilderOptions options = BuilderOptions.empty]) {
return builder;
}

Map<String, List<String>> _validatedBuildExtensionsFrom(
Map<String, dynamic> optionsMap) {
final extensionsOption = optionsMap.remove('build_extensions');
if (extensionsOption == null) return _defaultExtensions;

if (extensionsOption is! Map) {
throw ArgumentError(
'Configured build_extensions should be a map from inputs to outputs.');
}

final result = <String, List<String>>{};

for (final entry in extensionsOption.entries) {
final input = entry.key;
if (input is! String || !input.endsWith('.dart')) {
throw ArgumentError('Invalid key in build_extensions option: `$input` '
'should be a string ending with `.dart`');
}

final output = entry.value;
if (output is! String || !output.endsWith('.dart')) {
throw ArgumentError('Invalid output extension `$output`. It should be a '
'string ending with `.dart`');
}

result[input] = [output];
}

if (result.isEmpty) {
throw ArgumentError('Configured build_extensions must not be empty.');
}

return result;
}

PostProcessBuilder partCleanup(BuilderOptions options) =>
const FileDeletingBuilder(['.g.part']);

Expand All @@ -53,9 +93,7 @@ class CombiningBuilder implements Builder {
final Set<String> _ignoreForFile;

@override
Map<String, List<String>> get buildExtensions => const {
'.dart': [_outputExtensions]
};
final Map<String, List<String>> buildExtensions;

/// Returns a new [CombiningBuilder].
///
Expand All @@ -65,6 +103,7 @@ class CombiningBuilder implements Builder {
const CombiningBuilder({
bool? includePartName,
Set<String>? ignoreForFile,
this.buildExtensions = _defaultExtensions,
}) : _includePartName = includePartName ?? false,
_ignoreForFile = ignoreForFile ?? const <String>{};

Expand Down Expand Up @@ -104,8 +143,20 @@ class CombiningBuilder implements Builder {
.where((s) => s.isNotEmpty)
.join('\n\n');
if (assets.isEmpty) return;

final inputLibrary = await buildStep.inputLibrary;
final partOf = nameOfPartial(inputLibrary, buildStep.inputId);
final outputId = buildStep.allowedOutputs.single;
final partOf = nameOfPartial(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 ignoreForFile = _ignoreForFile.isEmpty
? ''
Expand All @@ -117,7 +168,6 @@ part of $partOf;
$assets
''';
await buildStep.writeAsString(
buildStep.inputId.changeExtension(_outputExtensions), output);
await buildStep.writeAsString(outputId, output);
}
}
32 changes: 15 additions & 17 deletions source_gen/lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,31 +105,29 @@ class _Builder extends Builder {

if (!_isLibraryBuilder) {
final asset = buildStep.inputId;
final name = nameOfPartial(library, asset);
final name = nameOfPartial(library, asset, outputId);
contentBuffer.writeln();

String part;
if (this is PartBuilder) {
contentBuffer
..write(languageOverrideForLibrary(library))
..writeln('part of $name;');
part = computePartUrl(buildStep.inputId, outputId);
final part = computePartUrl(buildStep.inputId, outputId);

final libraryUnit =
await buildStep.resolver.compilationUnitFor(buildStep.inputId);
final hasLibraryPartDirectiveWithOutputUri =
hasExpectedPartDirective(libraryUnit, part);
if (!hasLibraryPartDirectiveWithOutputUri) {
// TODO: Upgrade to error in a future breaking change?
log.warning('$part must be included as a part directive in '
'the input library with:\n part \'$part\';');
return;
}
} else {
assert(this is SharedPartBuilder);
final finalPartId = buildStep.inputId.changeExtension('.g.dart');
part = computePartUrl(buildStep.inputId, finalPartId);
}

final libraryUnit =
await buildStep.resolver.compilationUnitFor(buildStep.inputId);
final hasLibraryPartDirectiveWithOutputUri = libraryUnit.directives
.whereType<PartDirective>()
.any((e) => e.uri.stringValue == part);
if (!hasLibraryPartDirectiveWithOutputUri) {
// TODO: Upgrade to error in a future breaking change?
log.warning('$part must be included as a part directive in '
'the input library with:\n part \'$part\';');
return;
// For shared-part builders, `part` statements will be checked by the
// combining build step.
}
}

Expand Down
14 changes: 11 additions & 3 deletions source_gen/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:io';

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:build/build.dart';
Expand Down Expand Up @@ -37,14 +38,21 @@ String typeNameOf(DartType type) {
throw UnimplementedError('(${type.runtimeType}) $type');
}

bool hasExpectedPartDirective(CompilationUnit unit, String part) =>
unit.directives
.whereType<PartDirective>()
.any((e) => e.uri.stringValue == part);

/// Returns a name suitable for `part of "..."` when pointing to [element].
String nameOfPartial(LibraryElement element, AssetId source) {
String nameOfPartial(LibraryElement element, AssetId source, AssetId output) {
if (element.name.isNotEmpty) {
return element.name;
}

final sourceUrl = p.basename(source.uri.toString());
return '\'$sourceUrl\'';
assert(source.package == output.package);
final relativeSourceUri =
p.url.relative(source.path, from: p.dirname(output.path));
return '\'$relativeSourceUri\'';
}

/// Returns a suggested library identifier based on [source] path and package.
Expand Down
4 changes: 2 additions & 2 deletions source_gen/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: source_gen
version: 1.0.5
version: 1.1.0-dev
description: >-
Source code generation builders and utilities for the Dart build system
repository: https://github.com/dart-lang/source_gen
Expand All @@ -10,7 +10,7 @@ environment:
dependencies:
analyzer: ^2.0.0
async: ^2.5.0
build: ^2.0.0
build: ^2.1.0
dart_style: ^2.0.0
glob: ^2.0.0
meta: ^1.3.0
Expand Down
Loading

0 comments on commit 76566af

Please sign in to comment.