Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dart build runner generate one dart file with content

Tags:

dart

I am working on a dart package with includes over 200 models and at the moment i have to write manually one line of "export" for each model, to make the models available for everyone who uses this package.

I want the build runner to generate one dart file which contains every export definition.

Therefore I would create an annotation "ExportModel". The builder should search for each class annotated with this annotation.

I tried creating some Builders, but they will generate a *.g.dart file for each class that is annotated. I just want to have one file.

Is where a way to create a builder that runs only once and creates a file at the end ?

like image 785
Ephenodrom Avatar asked Jul 10 '19 14:07

Ephenodrom


1 Answers

The short answer to your question of a builder that only runs once and creates a single file in the package is to use r'$lib$' as the input extension. The long answer is that to find the classes that are annotated you probably want an intermediate output to track them.

I'd write this with 2 builders, one to search for the ExportModel annotation, and another to write the exports file. Here is a rough sketch with details omitted - I haven't tested any of the code here but it should get you started on the right path.

Builder 1 - find the classes annotated with @ExportModel().

Could write with some utilities from package:source_gen, but can't use LibraryBuilder since it's not outputting Dart code...

Goal is to write a .exports file next to each .dart file which as the name of all the classes that are annotated with @ExportModel().

class ExportLocatingBuilder implements Builder {
  @override
  final buildExtensions = const {
    '.dart': ['.exports']
  };

  @override
  Future<void> build(BuildStep buildStep) async {
    final resolver = buildStep.resolver;
    if (!await resolver.isLibrary(buildStep.inputId)) return;
    final lib = LibraryReader(await buildStep.inputLibrary);
    final exportAnnotation = TypeChecker.fromRuntime(ExportModel);
    final annotated = [
      for (var member in lib.annotatedWith(exportAnnotation)) element.name,
    ];
    if (annotated.isNotEmpty) {
      buildStep.writeAsString(
          buildStep.inputId.changeExtension('.exports'), annotated.join(','));
    }
  }
}

This builder should be build_to: cache and you may want to have a PostProcessBuilder that cleans up all the outputs it produces which would be specified with applies_builder. You can use the FileDeletingBuilder to cheaply implement the cleanup. See the FAQ on temporary outputs and the angular cleanup for example.

Builder 2 - find the .exports files and generate a Dart file

Use findAssets to track down all those .exports files, and write an export statement for each one. Use a show with the content of the file which should contain the names of the members that were annotated.

class ExportsBuilder implements Builder {
  @override
  final buildExtensions = const {
    r'$lib$': ['exports.dart']
  };

  @override
  Future<void> build(BuildStep buildStep) async {
    final exports = buildStep.findAssets(Glob('**/*.exports'));
    final content = [
      await for (var exportLibrary in exports)
        'export \'${exportLibrary.changeExtension('.dart').uri}\' '
            'show ${await buildStep.readAsString(exportLibrary)};',
    ];
    if (content.isNotEmpty) {
      buildStep.writeAsString(
          AssetId(buildStep.inputId.package, 'lib/exports.dart'),
          content.join('\n'));
    }
  }
}

This builder should likely be build_to: source if you want to publish this file on pub. It should have a required_inputs: [".exports"] to ensure it runs after the previous builder.

Why does it need to be this complex?

You could implement this as a single builder which uses findAssets to find all the Dart files. The downside is that rebuilds would be much slower because it would be invalidated by any content change in any Dart file and you'd end up parsing all Dart code for a change in any Dart code. With the 2 builder approach then only the individual .exports which come from a changed Dart file need to be resolved and rebuilt on a change, and then only if the exports change will the exports.dart file be invalidated.

Older versions of build_runner also didn't support using the Resolver to resolve code that isn't transitively imported from the input library. Recent version of build_runner have relaxed this constraint.

like image 141
Nate Bosch Avatar answered Sep 20 '22 16:09

Nate Bosch