diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json new file mode 100644 index 0000000..b3db758 --- /dev/null +++ b/.fvm/fvm_config.json @@ -0,0 +1,4 @@ +{ + "flutterSdkVersion": "stable", + "flavors": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 65c34dc..ab2dac0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build/ # Omit committing pubspec.lock for library packages; see # https://dart.dev/guides/libraries/private-files#pubspeclock. pubspec.lock +.fvm/flutter_sdk \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4006fd2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "yaml.schemaStore.enable": false, + "dart.sdkPaths": [ + ".fvm/flutter_sdk" + ], +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 02fdbb8..e72d2c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## 0.1.0 +- Upgrade to Dart 3 +- Add prunning option - prune when pubspec.lock was changed +- Add verbose mode +- Add debug mode +- Support part file imports, relative imports, package imports. +- Support generated files that are imported rather than as "part" file. +- Support other generated files, not just .g.dart. +- Add cache commands + - Prune - clears cache directory + - List - list files, their actual hash and dirty state + +**Breaking change** +- Drop support for "redis". + ## 0.0.7 - Fix a major bug where a class's dependencies were not considered while generating digest, so any change in the super classes caused wrong output. diff --git a/README.md b/README.md index a67686b..e9cf448 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,32 @@ Cached Build Runner is a Dart package that optimizes the build_runner by caching cached_build_runner [arguments] ``` -### Usage +### Commands Global options: -h, --help Print this usage information. Available commands: -* **build**: Performs a single build on the specified targets and then exits. -* **watch**: Builds the specified targets, watching the file system for updates and rebuilding as appropriate. +* **build**: Performs a single build on the specified targets and then exits. +* **watch**: Builds the specified targets, watching the file system for updates and rebuilding as appropriate. +* **cache**: Commands for manipulating cache. Available arguments: * -h, --help: Print out usage instructions. -* -q, --quiet: Disables printing out logs during the build. -* -r, --redis: Use Redis database if installed on the system. Using Redis allows multiple instance access and is ideal for usage in pipelines. The default implementation uses a file system storage (Hive), which is ideal for usage in local systems. +* -v, --verbose: Enables verbose logs. +* -d, --debug: Enables even more verbose logs. +* -p, --[no]prune: Enable pruning cache directory when pubspec.lock was changed since last build. Defaults true. +* -c, --cache-directory: Provide the directory where this tool can keep the caches. + +## Cache sub-commands +* **prune**: Clear cache directory. +* **list**: List table of files with hash (digest) and their dirty state. +arguments +* -h, --help: Print out usage instructions. +* -v, --verbose: Enables verbose logs. * -c, --cache-directory: Provide the directory where this tool can keep the caches. -# Cached Build Runner +# Installation Add the package to your pubspec.yaml file under dev_dependencies: ```yaml @@ -35,28 +45,6 @@ dev_dependencies: Replace latest_version with the latest available version of the package. -# License -``` -MIT License - -Copyright (c) 2021 Jyotirmoy Paul - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` \ No newline at end of file +--- +Original work done by @jyotirmoy-paul. \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index dee8927..fc71bfd 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,30 +1,13 @@ -# This file configures the static analysis results for your project (errors, -# warnings, and lints). -# -# This enables the 'recommended' set of lints from `package:lints`. -# This set helps identify many issues that may lead to problems when running -# or consuming Dart code, and enforces writing Dart using a single, idiomatic -# style and format. -# -# If you want a smaller set of lints you can change this to specify -# 'package:lints/core.yaml'. These are just the most critical lints -# (the recommended set includes the core lints). -# The core lints are also what is used by pub.dev for scoring packages. - -include: package:lints/recommended.yaml - -# Uncomment the following section to specify additional rules. - -# linter: -# rules: -# - camel_case_types - -# analyzer: -# exclude: -# - path/to/excluded/files/** - -# For more information about the core and recommended set of lints, see -# https://dart.dev/go/core-lints - -# For additional information about configuring this file, see -# https://dart.dev/guides/language/analysis-options +include: package:netglade_analysis/lints.yaml + +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + - "build/" + +dart_code_metrics: + extends: + - package:netglade_analysis/dcm.yaml + pubspec-rules: + prefer-publish-to-none: false diff --git a/bin/cached_build_runner.dart b/bin/cached_build_runner.dart index 90a257d..b532ec9 100644 --- a/bin/cached_build_runner.dart +++ b/bin/cached_build_runner.dart @@ -1,15 +1,19 @@ import 'package:args/command_runner.dart'; import 'package:cached_build_runner/commands/build_command.dart'; +import 'package:cached_build_runner/commands/cache/cache_command.dart'; import 'package:cached_build_runner/commands/watch_command.dart'; +import 'package:cached_build_runner/di_container.dart'; Future main(List arguments) async { + DiContainer.setup(); + const commandName = 'cached_build_runner'; - const commandDescription = - 'Optimizes the build_runner by caching generated codes for non changed .dart files'; + const commandDescription = 'Optimizes the build_runner by caching generated codes for non changed .dart files'; - final runner = CommandRunner(commandName, commandDescription) + final runner = CommandRunner(commandName, commandDescription) ..addCommand(BuildCommand()) - ..addCommand(WatchCommand()); + ..addCommand(WatchCommand()) + ..addCommand(CacheCommand()); - runner.run(arguments); + await runner.run(arguments); } diff --git a/example/build.yaml b/example/build.yaml new file mode 100644 index 0000000..20cf648 --- /dev/null +++ b/example/build.yaml @@ -0,0 +1,17 @@ +# # targets: +# # $default: +# # builders: +# # drift_dev: +# # enabled: false +# # drift_dev:not_shared: +# # enabled: true +# # generate_for: +# # include: +# # - lib/domain/data/**.dart +# global_options: +# freezed: +# runs_before: +# - auto_mappr +# json_serializable: +# runs_before: +# - auto_mappr diff --git a/example/lib/core_models/user.dart b/example/lib/core_models/user.dart new file mode 100644 index 0000000..cc0d34a --- /dev/null +++ b/example/lib/core_models/user.dart @@ -0,0 +1,21 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'user.g.dart'; + +@JsonSerializable() +class User { + final int id; + final String name; + final int age; + + int get ageX => age; + + factory User.fromJson(Map json) => _$UserFromJson(json); + + User({ + required this.id, + required this.name, + required this.age, + }); + Map toJson() => _$UserToJson(this); +} diff --git a/example/lib/core_models/user_dto.dart b/example/lib/core_models/user_dto.dart new file mode 100644 index 0000000..15d9d98 --- /dev/null +++ b/example/lib/core_models/user_dto.dart @@ -0,0 +1,11 @@ +class UserDto { + final int id; + final String name; + final int age; + + UserDto({ + required this.id, + required this.name, + required this.age, + }); +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 5da88f5..ab73b3a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,75 +1 @@ -import 'package:example/models/counter_model.dart'; -import 'package:flutter/material.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - final _counterModel = CounterModel( - description: 'Model which can keep a count.', - ); - - void _incrementCounter() { - setState(() { - _counterModel.count++; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '${_counterModel.count}', - style: Theme.of(context).textTheme.headlineMedium, - ), - Text( - '${_counterModel.toJson()}', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), - ); - } -} +void main() {} diff --git a/example/lib/models/base_class.dart b/example/lib/models/base_class.dart index 303da6a..5076ff3 100644 --- a/example/lib/models/base_class.dart +++ b/example/lib/models/base_class.dart @@ -1,5 +1,6 @@ import 'package:example/core_models/core_class.dart'; // absolute import import 'package:example/core_models/core_interface.dart'; // absolute import + import '../core_models/core_mixin.dart'; // relative import class BaseClass extends CoreClass with CoreMixin implements CoreInterface { diff --git a/example/lib/models/counter_model.dart b/example/lib/models/counter_model.dart index 319c5fb..3a27823 100644 --- a/example/lib/models/counter_model.dart +++ b/example/lib/models/counter_model.dart @@ -1,4 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; + import 'base_class.dart'; part 'counter_model.g.dart'; @@ -7,14 +8,17 @@ part 'counter_model.g.dart'; class CounterModel extends BaseClass { int count; final String description; + final String x; + final String y; CounterModel({ this.count = 0, - this.description = 'description', + this.description = 'descriptionx', + required this.x, + this.y = 'yza', }); - factory CounterModel.fromJson(Map json) => - _$CounterModelFromJson(json); + factory CounterModel.fromJson(Map json) => _$CounterModelFromJson(json); Map toJson() => _$CounterModelToJson(this); } diff --git a/example/lib/models/counter_model2.dart b/example/lib/models/counter_model2.dart new file mode 100644 index 0000000..8f3088f --- /dev/null +++ b/example/lib/models/counter_model2.dart @@ -0,0 +1,23 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../core_models/user.dart'; +import 'base_class.dart'; + +part 'counter_model2.g.dart'; + +@JsonSerializable() +class CounterModel2 extends BaseClass { + int count; + final String description; + final User user; + + CounterModel2({ + this.count = 0, + this.description = 'descriptionsadsadasaaa', + required this.user, + }); + + factory CounterModel2.fromJson(Map json) => _$CounterModel2FromJson(json); + + Map toJson() => _$CounterModel2ToJson(this); +} diff --git a/example/lib/models/freezed_union.dart b/example/lib/models/freezed_union.dart new file mode 100644 index 0000000..db0bc9f --- /dev/null +++ b/example/lib/models/freezed_union.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../core_models/user.dart'; + +part 'freezed_union.freezed.dart'; + +@freezed +abstract class FreezedUnion with _$FreezedUnion { + const factory FreezedUnion.home() = Home; + const factory FreezedUnion.user(User user) = UserUnion; +} diff --git a/example/lib/models/freezed_union.freezed.dart b/example/lib/models/freezed_union.freezed.dart new file mode 100644 index 0000000..1fba332 --- /dev/null +++ b/example/lib/models/freezed_union.freezed.dart @@ -0,0 +1,309 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'freezed_union.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$FreezedUnion { + @optionalTypeArgs + TResult when({ + required TResult Function() home, + required TResult Function(User user) user, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? home, + TResult? Function(User user)? user, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? home, + TResult Function(User user)? user, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(Home value) home, + required TResult Function(UserUnion value) user, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(Home value)? home, + TResult? Function(UserUnion value)? user, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Home value)? home, + TResult Function(UserUnion value)? user, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $FreezedUnionCopyWith<$Res> { + factory $FreezedUnionCopyWith( + FreezedUnion value, $Res Function(FreezedUnion) then) = + _$FreezedUnionCopyWithImpl<$Res, FreezedUnion>; +} + +/// @nodoc +class _$FreezedUnionCopyWithImpl<$Res, $Val extends FreezedUnion> + implements $FreezedUnionCopyWith<$Res> { + _$FreezedUnionCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$HomeImplCopyWith<$Res> { + factory _$$HomeImplCopyWith( + _$HomeImpl value, $Res Function(_$HomeImpl) then) = + __$$HomeImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$HomeImplCopyWithImpl<$Res> + extends _$FreezedUnionCopyWithImpl<$Res, _$HomeImpl> + implements _$$HomeImplCopyWith<$Res> { + __$$HomeImplCopyWithImpl(_$HomeImpl _value, $Res Function(_$HomeImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$HomeImpl implements Home { + const _$HomeImpl(); + + @override + String toString() { + return 'FreezedUnion.home()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$HomeImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() home, + required TResult Function(User user) user, + }) { + return home(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? home, + TResult? Function(User user)? user, + }) { + return home?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? home, + TResult Function(User user)? user, + required TResult orElse(), + }) { + if (home != null) { + return home(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(Home value) home, + required TResult Function(UserUnion value) user, + }) { + return home(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(Home value)? home, + TResult? Function(UserUnion value)? user, + }) { + return home?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Home value)? home, + TResult Function(UserUnion value)? user, + required TResult orElse(), + }) { + if (home != null) { + return home(this); + } + return orElse(); + } +} + +abstract class Home implements FreezedUnion { + const factory Home() = _$HomeImpl; +} + +/// @nodoc +abstract class _$$UserUnionImplCopyWith<$Res> { + factory _$$UserUnionImplCopyWith( + _$UserUnionImpl value, $Res Function(_$UserUnionImpl) then) = + __$$UserUnionImplCopyWithImpl<$Res>; + @useResult + $Res call({User user}); +} + +/// @nodoc +class __$$UserUnionImplCopyWithImpl<$Res> + extends _$FreezedUnionCopyWithImpl<$Res, _$UserUnionImpl> + implements _$$UserUnionImplCopyWith<$Res> { + __$$UserUnionImplCopyWithImpl( + _$UserUnionImpl _value, $Res Function(_$UserUnionImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? user = null, + }) { + return _then(_$UserUnionImpl( + null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as User, + )); + } +} + +/// @nodoc + +class _$UserUnionImpl implements UserUnion { + const _$UserUnionImpl(this.user); + + @override + final User user; + + @override + String toString() { + return 'FreezedUnion.user(user: $user)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserUnionImpl && + (identical(other.user, user) || other.user == user)); + } + + @override + int get hashCode => Object.hash(runtimeType, user); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$UserUnionImplCopyWith<_$UserUnionImpl> get copyWith => + __$$UserUnionImplCopyWithImpl<_$UserUnionImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() home, + required TResult Function(User user) user, + }) { + return user(this.user); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? home, + TResult? Function(User user)? user, + }) { + return user?.call(this.user); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? home, + TResult Function(User user)? user, + required TResult orElse(), + }) { + if (user != null) { + return user(this.user); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(Home value) home, + required TResult Function(UserUnion value) user, + }) { + return user(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(Home value)? home, + TResult? Function(UserUnion value)? user, + }) { + return user?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Home value)? home, + TResult Function(UserUnion value)? user, + required TResult orElse(), + }) { + if (user != null) { + return user(this); + } + return orElse(); + } +} + +abstract class UserUnion implements FreezedUnion { + const factory UserUnion(final User user) = _$UserUnionImpl; + + User get user; + @JsonKey(ignore: true) + _$$UserUnionImplCopyWith<_$UserUnionImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/example/lib/models/json_part.dart b/example/lib/models/json_part.dart new file mode 100644 index 0000000..c6a5fa7 --- /dev/null +++ b/example/lib/models/json_part.dart @@ -0,0 +1,6 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'json_part.g.dart'; +part 'x/json_partof.dart'; + +class X {} diff --git a/example/lib/models/x/json_partof.dart b/example/lib/models/x/json_partof.dart new file mode 100644 index 0000000..1ae6652 --- /dev/null +++ b/example/lib/models/x/json_partof.dart @@ -0,0 +1,11 @@ +part of '../json_part.dart'; + +@JsonSerializable() +class JsonDartPartOf { + final int id; + + factory JsonDartPartOf.fromJson(Map json) => _$JsonDartPartOfFromJson(json); + + JsonDartPartOf({required this.id}); + Map toJson() => _$JsonDartPartOfToJson(this); +} diff --git a/example/lib/user_mappr.auto_mappr.dart b/example/lib/user_mappr.auto_mappr.dart new file mode 100644 index 0000000..0dc7fd3 --- /dev/null +++ b/example/lib/user_mappr.auto_mappr.dart @@ -0,0 +1,220 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AutoMapprGenerator +// ************************************************************************** + +// ignore_for_file: type=lint, unnecessary_cast, unused_local_variable + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:auto_mappr_annotation/auto_mappr_annotation.dart' as _i1; + +import 'core_models/user.dart' as _i3; +import 'core_models/user_dto.dart' as _i2; + +/// {@template package:example/user_mappr.dart} +/// Available mappings: +/// - `UserDto` → `User`. +/// {@endtemplate} +class $UserMappr implements _i1.AutoMapprInterface { + const $UserMappr(); + + Type _typeOf() => T; + + List<_i1.AutoMapprInterface> get _delegates => const []; + + /// {@macro AutoMapprInterface:canConvert} + /// {@macro package:example/user_mappr.dart} + @override + bool canConvert({bool recursive = true}) { + final sourceTypeOf = _typeOf(); + final targetTypeOf = _typeOf(); + if ((sourceTypeOf == _typeOf<_i2.UserDto>() || + sourceTypeOf == _typeOf<_i2.UserDto?>()) && + (targetTypeOf == _typeOf<_i3.User>() || + targetTypeOf == _typeOf<_i3.User?>())) { + return true; + } + if (recursive) { + for (final mappr in _delegates) { + if (mappr.canConvert()) { + return true; + } + } + } + return false; + } + + /// {@macro AutoMapprInterface:convert} + /// {@macro package:example/user_mappr.dart} + @override + TARGET convert(SOURCE? model) { + if (canConvert(recursive: false)) { + return _convert(model)!; + } + for (final mappr in _delegates) { + if (mappr.canConvert()) { + return mappr.convert(model)!; + } + } + + throw Exception('No ${_typeOf()} -> ${_typeOf()} mapping.'); + } + + /// {@macro AutoMapprInterface:tryConvert} + /// {@macro package:example/user_mappr.dart} + @override + TARGET? tryConvert(SOURCE? model) { + if (canConvert(recursive: false)) { + return _convert( + model, + canReturnNull: true, + ); + } + for (final mappr in _delegates) { + if (mappr.canConvert()) { + return mappr.tryConvert(model); + } + } + + return null; + } + + /// {@macro AutoMapprInterface:convertIterable} + /// {@macro package:example/user_mappr.dart} + @override + Iterable convertIterable(Iterable model) { + if (canConvert(recursive: false)) { + return model.map((item) => _convert(item)!); + } + for (final mappr in _delegates) { + if (mappr.canConvert()) { + return mappr.convertIterable(model); + } + } + + throw Exception('No ${_typeOf()} -> ${_typeOf()} mapping.'); + } + + /// For iterable items, converts from SOURCE to TARGET if such mapping is configured, into Iterable. + /// + /// When an item in the source iterable is null, uses `whenSourceIsNull` if defined or null + /// + /// {@macro package:example/user_mappr.dart} + @override + Iterable tryConvertIterable( + Iterable model) { + if (canConvert(recursive: false)) { + return model.map((item) => _convert(item, canReturnNull: true)); + } + for (final mappr in _delegates) { + if (mappr.canConvert()) { + return mappr.tryConvertIterable(model); + } + } + + throw Exception('No ${_typeOf()} -> ${_typeOf()} mapping.'); + } + + /// {@macro AutoMapprInterface:convertList} + /// {@macro package:example/user_mappr.dart} + @override + List convertList(Iterable model) { + if (canConvert(recursive: false)) { + return convertIterable(model).toList(); + } + for (final mappr in _delegates) { + if (mappr.canConvert()) { + return mappr.convertList(model); + } + } + + throw Exception('No ${_typeOf()} -> ${_typeOf()} mapping.'); + } + + /// For iterable items, converts from SOURCE to TARGET if such mapping is configured, into List. + /// + /// When an item in the source iterable is null, uses `whenSourceIsNull` if defined or null + /// + /// {@macro package:example/user_mappr.dart} + @override + List tryConvertList(Iterable model) { + if (canConvert(recursive: false)) { + return tryConvertIterable(model).toList(); + } + for (final mappr in _delegates) { + if (mappr.canConvert()) { + return mappr.tryConvertList(model); + } + } + + throw Exception('No ${_typeOf()} -> ${_typeOf()} mapping.'); + } + + /// {@macro AutoMapprInterface:convertSet} + /// {@macro package:example/user_mappr.dart} + @override + Set convertSet(Iterable model) { + if (canConvert(recursive: false)) { + return convertIterable(model).toSet(); + } + for (final mappr in _delegates) { + if (mappr.canConvert()) { + return mappr.convertSet(model); + } + } + + throw Exception('No ${_typeOf()} -> ${_typeOf()} mapping.'); + } + + /// For iterable items, converts from SOURCE to TARGET if such mapping is configured, into Set. + /// + /// When an item in the source iterable is null, uses `whenSourceIsNull` if defined or null + /// + /// {@macro package:example/user_mappr.dart} + @override + Set tryConvertSet(Iterable model) { + if (canConvert(recursive: false)) { + return tryConvertIterable(model).toSet(); + } + for (final mappr in _delegates) { + if (mappr.canConvert()) { + return mappr.tryConvertSet(model); + } + } + + throw Exception('No ${_typeOf()} -> ${_typeOf()} mapping.'); + } + + TARGET? _convert( + SOURCE? model, { + bool canReturnNull = false, + }) { + final sourceTypeOf = _typeOf(); + final targetTypeOf = _typeOf(); + if ((sourceTypeOf == _typeOf<_i2.UserDto>() || + sourceTypeOf == _typeOf<_i2.UserDto?>()) && + (targetTypeOf == _typeOf<_i3.User>() || + targetTypeOf == _typeOf<_i3.User?>())) { + if (canReturnNull && model == null) { + return null; + } + return (_map__i2$UserDto_To__i3$User((model as _i2.UserDto?)) as TARGET); + } + throw Exception('No ${model.runtimeType} -> $targetTypeOf mapping.'); + } + + _i3.User _map__i2$UserDto_To__i3$User(_i2.UserDto? input) { + final model = input; + if (model == null) { + throw Exception( + r'Mapping UserDto → User failed because UserDto was null, and no default value was provided. ' + r'Consider setting the whenSourceIsNull parameter on the MapType to handle null values during mapping.'); + } + return _i3.User( + id: model.id, + name: model.name, + age: model.age, + ); + } +} diff --git a/example/lib/user_mappr.dart b/example/lib/user_mappr.dart new file mode 100644 index 0000000..4742fbd --- /dev/null +++ b/example/lib/user_mappr.dart @@ -0,0 +1,10 @@ +import 'package:auto_mappr_annotation/auto_mappr_annotation.dart'; +import 'package:example/core_models/user.dart'; +import 'package:example/core_models/user_dto.dart'; + +import 'user_mappr.auto_mappr.dart'; + +@AutoMappr([ + MapType(), //ad +]) +class UserMappr extends $UserMappr {} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e0a7769..ebc265c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,31 +1,27 @@ name: example description: An example Flutter project to demonstrate the usage of cached_build_runner. -publish_to: 'none' - +publish_to: "none" version: 1.0.0+1 environment: - sdk: '>=2.19.2 <3.0.0' + sdk: ">=2.19.2 <3.0.0" dependencies: - flutter: - sdk: flutter - cupertino_icons: ^1.0.2 json_serializable: ^6.6.1 - json_annotation: ^4.8.0 - + json_annotation: ^4.8.1 + freezed_annotation: ^2.4.1 + auto_mappr_annotation: 2.1.0 dev_dependencies: - flutter_test: - sdk: flutter build_runner: ^2.3.3 cached_build_runner: path: ../ + auto_mappr: 2.2.0 flutter_lints: ^2.0.0 + freezed: ^2.4.6 flutter: uses-material-design: true - diff --git a/lib/args/args_parser.dart b/lib/args/args_parser.dart deleted file mode 100644 index 79aacac..0000000 --- a/lib/args/args_parser.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:args/args.dart'; - -import '../utils/log.dart'; -import '../utils/utils.dart'; -import 'args_utils.dart'; - -class ArgumentParser { - final ArgParser _argParser; - - ArgumentParser(this._argParser) { - _addFlagAndOption(); - } - - ArgParser _addFlagAndOption() { - return _argParser - ..addFlag( - ArgsUtils.quiet, - abbr: 'q', - help: 'Disables printing out logs during build.', - negatable: false, - ) - ..addFlag( - ArgsUtils.useRedis, - abbr: 'r', - help: - 'Use redis database, if installed on the system. Using redis allows multiple instance access. Ideal for usage in pipelines. Default implementation uses a file system storage (hive), which is idea for usage in local systems.', - negatable: false, - ) - ..addSeparator('') - ..addOption( - ArgsUtils.cacheDirectory, - abbr: 'c', - help: 'Provide the directory where this tool can keep the caches.', - ); - } - - void parseArgs(Iterable? arguments) { - if (arguments == null) return; - - /// parse all args - final result = _argParser.parse(arguments); - - /// cache directory - if (result.wasParsed(ArgsUtils.cacheDirectory)) { - Utils.appCacheDirectory = result[ArgsUtils.cacheDirectory]; - } else { - Utils.appCacheDirectory = Utils.getDefaultCacheDirectory(); - Logger.i( - "As no '${ArgsUtils.cacheDirectory}' was specified, using the default directory: ${Utils.appCacheDirectory}", - ); - } - - /// quiet - Utils.isVerbose = !result.wasParsed(ArgsUtils.quiet); - - /// use redis - Utils.isRedisUsed = result.wasParsed(ArgsUtils.useRedis); - } -} diff --git a/lib/args/args_utils.dart b/lib/args/args_utils.dart index bd229b9..1a7a463 100644 --- a/lib/args/args_utils.dart +++ b/lib/args/args_utils.dart @@ -1,10 +1,26 @@ abstract class ArgsUtils { - /// commands - static const watch = 'watch'; - static const build = 'build'; - - /// argument flags & options - static const quiet = 'quiet'; - static const useRedis = 'redis'; - static const cacheDirectory = 'cache-directory'; + static const commands = _Commands(); + static const args = _Args(); +} + +class _Commands { + String get watch => 'watch'; + String get build => 'build'; + String get cache => 'cache'; + + // Cache subcomammnds + String get prune => 'prune'; + String get list => 'list'; + + const _Commands(); +} + +class _Args { + String get verbose => 'verbose'; + String get debug => 'debug'; + String get cacheDirectory => 'cache-directory'; + String get lockPrune => 'prune'; + String get clear => 'clear'; + + const _Args(); } diff --git a/lib/args/argument_parser_base.dart b/lib/args/argument_parser_base.dart new file mode 100644 index 0000000..b30cd85 --- /dev/null +++ b/lib/args/argument_parser_base.dart @@ -0,0 +1,20 @@ +import 'package:args/args.dart'; +import 'package:cached_build_runner/args/args_utils.dart'; +import 'package:cached_build_runner/utils/logger.dart'; +import 'package:cached_build_runner/utils/utils.dart'; +import 'package:meta/meta.dart'; + +abstract class ArgumentParserBase { + @protected + void parseCacheDirectory(ArgResults result) { + if (result.wasParsed(ArgsUtils.args.cacheDirectory)) { + Utils.appCacheDirectory = result[ArgsUtils.args.cacheDirectory] as String; + Logger.i('Using "${Utils.appCacheDirectory}" as cache directory'); + } else { + Utils.appCacheDirectory = Utils.getDefaultCacheDirectory(); + Logger.i( + "As no '${ArgsUtils.args.cacheDirectory}' was specified, using the default directory: ${Utils.appCacheDirectory}", + ); + } + } +} diff --git a/lib/args/build_and_watch_argument_parser.dart b/lib/args/build_and_watch_argument_parser.dart new file mode 100644 index 0000000..ffa4f26 --- /dev/null +++ b/lib/args/build_and_watch_argument_parser.dart @@ -0,0 +1,55 @@ +import 'package:args/args.dart'; +import 'package:cached_build_runner/args/args_utils.dart'; +import 'package:cached_build_runner/args/argument_parser_base.dart'; +import 'package:cached_build_runner/utils/utils.dart'; + +class BuildAndWatchArgumentParser extends ArgumentParserBase { + final ArgParser _argParser; + + BuildAndWatchArgumentParser(this._argParser) { + _addFlagAndOption(); + } + + void parseArgs(Iterable? arguments) { + if (arguments == null) return; + + // parse all args + final result = _argParser.parse(arguments); + + parseCacheDirectory(result); + + Utils.isVerbose = result[ArgsUtils.args.verbose] as bool; + Utils.isDebug = result[ArgsUtils.args.debug] as bool; + + // enable prunning + Utils.isPruneEnabled = result[ArgsUtils.args.lockPrune] as bool; + } + + void _addFlagAndOption() { + _argParser + ..addFlag( + ArgsUtils.args.verbose, + abbr: 'v', + help: 'Enables verbose mode', + negatable: false, + ) + ..addFlag( + ArgsUtils.args.debug, + abbr: 'd', + help: 'Enables debug mode', + negatable: false, + ) + ..addFlag( + ArgsUtils.args.lockPrune, + abbr: 'p', + help: 'Enable pruning cache directory when pubspec.lock was changed since last build.', + defaultsTo: true, + ) + ..addSeparator('') + ..addOption( + ArgsUtils.args.cacheDirectory, + abbr: 'c', + help: 'Provide the directory where this tool can keep the caches.', + ); + } +} diff --git a/lib/args/cache_argument_parser.dart b/lib/args/cache_argument_parser.dart new file mode 100644 index 0000000..34d3710 --- /dev/null +++ b/lib/args/cache_argument_parser.dart @@ -0,0 +1,39 @@ +import 'package:args/args.dart'; +import 'package:cached_build_runner/args/args_utils.dart'; +import 'package:cached_build_runner/args/argument_parser_base.dart'; +import 'package:cached_build_runner/utils/utils.dart'; + +class CacheArgumentParser extends ArgumentParserBase { + final ArgParser _argParser; + + CacheArgumentParser(this._argParser) { + _addFlagAndOption(); + } + + void parseArgs(Iterable? arguments) { + if (arguments == null) return; + + final result = _argParser.parse(arguments); + + // cache directory + parseCacheDirectory(result); + + // verbose + Utils.isVerbose = result[ArgsUtils.args.verbose] as bool; + } + + void _addFlagAndOption() { + _argParser + ..addFlag( + ArgsUtils.args.verbose, + abbr: 'v', + help: 'Enables verbose mode', + negatable: false, + ) + ..addOption( + ArgsUtils.args.cacheDirectory, + abbr: 'c', + help: 'Provide the directory where this tool can keep the caches.', + ); + } +} diff --git a/lib/cached_build_runner.dart b/lib/cached_build_runner.dart index 1b81394..e15d35e 100644 --- a/lib/cached_build_runner.dart +++ b/lib/cached_build_runner.dart @@ -17,118 +17,60 @@ library cached_build_runner; import 'dart:async'; import 'dart:io'; -import 'package:cached_build_runner/core/dependency_visitor.dart'; +import 'package:ansicolor/ansicolor.dart'; +import 'package:barbecue/barbecue.dart'; +import 'package:cached_build_runner/core/build_runner_wrapper.dart'; +import 'package:cached_build_runner/core/cache_provider.dart'; +import 'package:cached_build_runner/core/file_parser.dart'; +import 'package:cached_build_runner/model/code_file.dart'; import 'package:cached_build_runner/utils/digest_utils.dart'; import 'package:cached_build_runner/utils/extension.dart'; +import 'package:cached_build_runner/utils/logger.dart'; +import 'package:cached_build_runner/utils/utils.dart'; +import 'package:get_it/get_it.dart'; import 'package:path/path.dart' as path; import 'package:synchronized/synchronized.dart' as sync; -import 'database/database_service.dart'; -import 'model/code_file.dart'; -import 'utils/log.dart'; -import 'utils/utils.dart'; +typedef _CachedFileInfo = ({String path, String digest, bool dirty}); -class CachedBuildRunner { - final DatabaseService _databaseService; - - CachedBuildRunner(this._databaseService); +class CachedBuildRunner implements Disposable { + final FileParser _fileParser; + final CacheProvider _cacheProvider; + final BuildRunnerWrapper _buildRunnerWrapper; final Map _contentDigestMap = {}; final _buildLock = sync.Lock(); - final _dependencyVisitor = DependencyVisitor(); - - bool _isCodeGenerationNeeded(FileSystemEvent e) { - switch (e.type) { - case FileSystemEvent.modify: - final newDigest = DigestUtils.generateDigestForSingleFile(e.path); - if (newDigest != _contentDigestMap[e.path]) { - _contentDigestMap[e.path] = newDigest; - return true; - } - return false; - - case FileSystemEvent.move: - case FileSystemEvent.create: - final digest = DigestUtils.generateDigestForSingleFile(e.path); - _contentDigestMap[e.path] = digest; - return true; - - case FileSystemEvent.delete: - _contentDigestMap.remove(e.path); - return true; - } - - return false; - } - - void _synchronizedBuild() { - _buildLock.synchronized(build); - } + StreamSubscription? _pubpsecWatch; + StreamSubscription? _libWatch; - void _onFileSystemEvent(FileSystemEvent event) { - if (_isCodeGenerationNeeded(event)) { - _synchronizedBuild(); - } - } - - void _generateContentHash(Directory directory) { - if (!directory.existsSync()) return; - for (final entity in directory.listSync( - recursive: true, - followLinks: false, - )) { - if (entity is File && entity.isDartSourceCodeFile()) { - _contentDigestMap[entity.path] = - DigestUtils.generateDigestForSingleFile( - entity.path, - ); - } - } - } - - void _watchForDependencyChanges() { - final pubspecFile = File(path.join(Utils.projectDirectory, 'pubspec.yaml')); - final pubspecFileDigest = DigestUtils.generateDigestForSingleFile( - pubspecFile.path, - ); - - pubspecFile.watch().listen((event) { - final newPubspecFileDigest = DigestUtils.generateDigestForSingleFile( - event.path, - ); - - if (newPubspecFileDigest != pubspecFileDigest) { - Logger.i( - 'As pubspec.yaml file has been modified, terminating cached_build_runner.\nNo further builds will be scheduled. Please restart the build.', - ); - exit(0); - } - }); - } + CachedBuildRunner({ + FileParser? fileParser, + CacheProvider? cacheProvider, + BuildRunnerWrapper? buildRunnerWrapper, + }) : _fileParser = fileParser ?? GetIt.I(), + _cacheProvider = cacheProvider ?? GetIt.I(), + _buildRunnerWrapper = buildRunnerWrapper ?? GetIt.I(); Future watch() async { _watchForDependencyChanges(); final libDirectory = Directory(path.join(Utils.projectDirectory, 'lib')); - final testDirectory = Directory(path.join(Utils.projectDirectory, 'test')); - Utils.logHeader( + Logger.header( 'Preparing to watch files in directory: ${Utils.projectDirectory}', ); _generateContentHash(libDirectory); - _generateContentHash(testDirectory); /// perform a first build operation await build(); - Utils.logHeader('Watching for file changes.'); + Logger.header('Watching for file changes.'); - /// let's listen for file changes in the project directory - /// specifically in "lib" & "test" directory - libDirectory.watchDartSourceCodeFiles().listen(_onFileSystemEvent); - testDirectory.watchDartSourceCodeFiles().listen(_onFileSystemEvent); + // let's listen for file changes in the project directory + // specifically in "lib" irectory + _libWatch = libDirectory.watchDartSourceCodeFiles().listen(_onFileSystemEvent); } /// Runs an efficient version of `build_runner build` by determining which @@ -152,292 +94,184 @@ class CachedBuildRunner { /// - [Exception] if there is an error while running `build_runner build` command. Future build() async { - Utils.logHeader('Determining Files that needs code generation'); + Logger.header('Determining Files that needs code generation'); - final libFiles = _fetchFilePathsFromLib(); - final testFiles = await _fetchFilePathsFromTest(); - final files = List.from(libFiles)..addAll(testFiles); + await _cacheProvider.ensurePruning(); - final List goodFiles = []; - final List badFiles = []; + final libFiles = _fileParser.getFilesNeedingGeneration(); + final files = List.of(libFiles); - final bulkMapping = await _databaseService.isMappingAvailableForBulk( - files.map((f) => f.digest), - ); + final mappedResult = await _cacheProvider.mapFilesToCache(files); - /// segregate good and bad files - /// good files -> files for whom the generated codes are available - /// bad files -> files for whom no generated codes are available in the cache - for (final file in files) { - final isGeneratedCodeAvailable = bulkMapping[file.digest] == true; - - /// mock generated files are always considered badFiles, - /// as they depends on various services, and to keep track of changes can become complicated - if (isGeneratedCodeAvailable) { - goodFiles.add(file); - } else { - badFiles.add(file); - } - } + final goodFiles = mappedResult.good; + final badFiles = mappedResult.bad; + + Logger.i('No. of cached files: ${goodFiles.length}'); + Logger.i('No. of non-cached files: ${badFiles.length}'); - Logger.v('No. of cached files: ${goodFiles.length}'); - Logger.v('No. of non-cached files: ${badFiles.length}'); + Logger.v(badFiles.map((e) => e.path).join('\n')); /// let's handle bad files - by generating the .g.dart / .mocks.dart files for them - final success = _generateCodesFor(badFiles); + final success = _buildRunnerWrapper.runBuild(badFiles); + if (!success) return; /// let's handle the good files - by copying the cached generated files to appropriate path - await _copyGeneratedCodesFor(goodFiles); + await _cacheProvider.copyGeneratedCodesFor(goodFiles); /// at last, let's cache the bad files - they may be required next time - await _cacheGeneratedCodesFor(badFiles); - - /// let's flush Hive, to make sure everything is committed to disk - await _databaseService.flush(); + await _cacheProvider.cacheFiles(badFiles); /// We are done, probably? } - /// Copies the cached generated files to the project directory for the given files list. - /// The cached file paths are obtained from the DatabaseService for the given files. - /// The method uses the _getGeneratedFilePathFrom() method to get the file path where - /// the generated file should be copied to in the project directory. - Future _copyGeneratedCodesFor(List files) async { - Utils.logHeader('Copying cached codes to project directory'); - - for (final file in files) { - final cachedGeneratedCodePath = await _databaseService.getCachedFilePath( - file.digest, - ); - Logger.v( - 'Copying file: ${Utils.getFileName(_getGeneratedFilePathFrom(file))}', - ); - File(cachedGeneratedCodePath).copySync(_getGeneratedFilePathFrom(file)); - - /// check if the file was copied successfully - if (!File(_getGeneratedFilePathFrom(file)).existsSync()) { - Logger.e( - 'ERROR: _copyGeneratedCodesFor: failed to copy the cached file $file', - ); - } - } - } - - /// converts "./cta_model.dart" to "./cta_model.g.dart" - /// OR - /// converts "./otp_screen_test.dart" to "./otp_screen_test.mocks.dart"; - String _getGeneratedFilePathFrom(CodeFile file) { - final path = file.path; - final lastDotDart = path.lastIndexOf('.dart'); - final extension = file.isTestFile ? '.mocks.dart' : '.g.dart'; - - if (lastDotDart >= 0) { - return '${path.substring(0, lastDotDart)}$extension'; - } - - return path; + Future listAllCachedFiles() async { + final libFiles = _fileParser.getFilesNeedingGeneration(); + final files = List.of(libFiles); + + final mappedResult = await _cacheProvider.mapFilesToCache(files); + + final goodFiles = mappedResult.good.map<_CachedFileInfo>((e) => (path: e.path, digest: e.digest, dirty: false)); + final badFiles = mappedResult.bad.map((e) => (path: e.path, digest: e.digest, dirty: true)); + + final mappedFiles = [...goodFiles, ...badFiles]..sort((a, b) => a.path.compareTo(b.path)); + + // final table = const TableRenderer(border: Border.simple).render( + // mappedFiles.map((e) => [e.path, e.digest, e.dirty].toList()), + // columns: [ColSpec(name: 'File'), ColSpec(name: 'Digest'), ColSpec(name: 'Dirty')], + // width: 100, + // ); + final redPen = AnsiPen()..red(bold: true); + final table = Table( + cellStyle: const CellStyle( + borderBottom: true, + borderRight: true, + borderLeft: true, + borderTop: true, + alignment: TextAlignment.MiddleLeft, + ), + header: const TableSection( + rows: [ + Row( + cells: [Cell('Path'), Cell('Digest'), Cell('Dirty')], + cellStyle: CellStyle(borderBottom: true), + ), + ], + ), + body: TableSection( + rows: mappedFiles + .map( + (e) => e.dirty + ? Row( + cells: [Cell(redPen(e.path)), Cell(redPen(e.digest)), Cell(redPen(e.dirty.toString()))], + ) + : Row( + cells: [Cell(e.path), Cell(e.digest), Cell(e.dirty.toString())], + ), + ) + .toList(), + ), + ).render(border: TextBorder.DEFAULT); + + //ignore: avoid_print, printing table. + print(table); } - /// Returns a comma-separated string of the file paths from the given list of [CodeFile]s - /// formatted for use as the argument for the --build-filter flag in the build_runner build command. - /// - /// The method maps the list of [CodeFile]s to a list of generated file paths, and then - /// returns a comma-separated string of the generated file paths. - /// - /// For example: - /// - /// final files = [CodeFile(path: 'lib/foo.dart', digest: 'abc123')]; - /// final buildFilter = _getBuildFilterList(files); - /// print(buildFilter); // 'lib/foo.g.dart' - String _getBuildFilterList(List files) { - final paths = files - .map((codeFile) => _getGeneratedFilePathFrom(codeFile)) - .toList(); - return paths.join(','); + @override + Future onDispose() async { + await _pubpsecWatch?.cancel(); + await _libWatch?.cancel(); } - /// this method runs build_runner build method with --build-filter - /// to only generate the required codes, thus avoiding unnecessary builds - bool _generateCodesFor(List files) { - if (files.isEmpty) return true; - Utils.logHeader( - 'Generating Codes for non-cached files, found ${files.length} files', - ); - - /// following command needs to be executed - /// flutter pub run build_runner build --build-filter="..." -d - /// where ... contains the list of files that needs generation - Logger.v('Running build_runner build...', showPrefix: false); - - /// TODO: let's check how we can use the build_runner package and include in this project - /// instead of relying on the flutter pub run command - /// there can be issues with flutter being in the path. - final process = Process.runSync( - 'flutter', - [ - 'pub', - 'run', - 'build_runner', - 'build', - '--build-filter', - _getBuildFilterList(files), - '--delete-conflicting-outputs' - ], - workingDirectory: Utils.projectDirectory, - ); + Future prune() { + Logger.header('Pruning cache directory'); - if (process.stderr.toString().isNotEmpty) { - if (process.stdout.toString().isNotEmpty) Logger.e(process.stdout.trim()); - Logger.e(process.stderr.trim()); - return false; - } else { - Logger.v(process.stdout.trim(), showPrefix: false); - return true; - } + return _cacheProvider.prune(); } - /// Fetches all the Dart test files in the 'test/' directory that contain the @Generate annotation for code generation. - /// Generates the [CodeFile] object for each file, which includes the file path, the MD5 digest of the file - /// and a flag indicating that it's a test file. - /// If the generateTestMocks flag in [Utils] is false, returns an empty list. - /// Searches for the dependencies of each test file in the 'test/' directory recursively, and adds them to the - /// dependencies list. If a dependency is from the main library, the dependency file is added to the list. - /// If there are no dependencies, the test file is skipped. - /// Returns a list of [CodeFile] objects representing all the test files in the 'test/' directory - /// that need code generation. - Future> _fetchFilePathsFromTest() async { - final List> testFiles = []; - - final testDirectory = Directory(path.join(Utils.projectDirectory, 'test')); - if (!testDirectory.existsSync()) return const []; - - for (FileSystemEntity entity in testDirectory.listSync( - recursive: true, - followLinks: false, - )) { - final List dependencies = []; + bool _isCodeGenerationNeeded(FileSystemEvent e) { + switch (e.type) { + case FileSystemEvent.modify: + final newDigest = DigestUtils.generateDigestForSingleFile(e.path); + if (newDigest != _contentDigestMap[e.path]) { + _contentDigestMap[e.path] = newDigest; - if (entity is File && entity.isDartSourceCodeFile()) { - final filePath = entity.path.trim(); - final fileContent = entity.readAsStringSync(); + return true; + } - /// if the file doesn't contain `@Generate` string, meaning no annotation for generations were marked - /// thus we can safely assume we don't need to generate mocks for those files - if (!fileContent.contains('@Generate')) continue; + return false; - dependencies.add(filePath); + case FileSystemEvent.move: + case FileSystemEvent.create: + final digest = DigestUtils.generateDigestForSingleFile(e.path); + _contentDigestMap[e.path] = digest; - final importDependency = - _dependencyVisitor.convertImportStatementsToAbsolutePaths( - fileContent, - ); + return true; - dependencies.addAll(importDependency); - testFiles.add(dependencies); - } - } + case FileSystemEvent.delete: + if (_contentDigestMap.containsKey(e.path)) { + final _ = _contentDigestMap.remove(e.path); - final List codeFiles = []; + return true; + } - for (final files in testFiles) { - codeFiles.add( - CodeFile( - path: files[0], - digest: DigestUtils.generateDigestForMultipleClassFile( - _dependencyVisitor, - files, - ), - isTestFile: true, - ), - ); + return false; } - Logger.v( - 'Found ${codeFiles.length} files in "test/" that needs code generation', - ); - - return codeFiles; + return false; } - /// This method returns all the files in the 'lib/' directory that need code generation. It first identifies the files - /// containing part '.g.dart'; statements using a regular expression. It then uses the grep command to find those - /// files and exclude any files that already have a .g.dart extension. Finally, it maps the file paths to a list of - /// CodeFile instances, which contains the file path and its corresponding digest calculated using the Utils.calculateDigestFor - /// method. - /// - /// Returns a list of [CodeFile] instances that represent the files that need code generation. - List _fetchFilePathsFromLib() { - /// Files in "lib/" that needs code generation - final libRegExp = RegExp(r"part '.+\.g\.dart';"); - final libDirectory = Directory(path.join(Utils.projectDirectory, 'lib')); + void _synchronizedBuild() { + unawaited(_buildLock.synchronized(build)); + } - final List libPathList = []; + void _onFileSystemEvent(FileSystemEvent event) { + if (_isCodeGenerationNeeded(event)) { + _synchronizedBuild(); + } + } - for (final entity in libDirectory.listSync( + void _generateContentHash(Directory directory) { + if (!directory.existsSync()) return; + for (final entity in directory.listSync( recursive: true, followLinks: false, )) { if (entity is File && entity.isDartSourceCodeFile()) { - final filePath = entity.path.trim(); - final fileContent = entity.readAsStringSync(); - - if (libRegExp.hasMatch(fileContent)) { - libPathList.add(filePath); - } + _contentDigestMap[entity.path] = DigestUtils.generateDigestForSingleFile( + entity.path, + ); } } - - Logger.v( - 'Found ${libPathList.length} files in "lib/" that needs code generation', - ); - - return libPathList - .map( - (path) => CodeFile( - path: path, - digest: DigestUtils.generateDigestForClassFile( - _dependencyVisitor, - path, - ), - ), - ) - .toList(); } - /// Copies the generated files from the project directory to cache directory, and make an entry in database. - Future _cacheGeneratedCodesFor(List files) async { - if (files.isEmpty) return; - - Utils.logHeader( - 'Caching new generated codes, caching ${files.length} files', + void _watchForDependencyChanges() { + final pubspecFile = File(path.join(Utils.projectDirectory, 'pubspec.yaml')); + final pubspecFileDigest = DigestUtils.generateDigestForSingleFile( + pubspecFile.path, ); - final cacheEntry = {}; - - for (final file in files) { - final generatedCodeFile = File(_getGeneratedFilePathFrom(file)); - Logger.v( - 'Caching generated code for: ${Utils.getFileName(generatedCodeFile.path)}', + _pubpsecWatch = pubspecFile.watch().listen((event) { + final newPubspecFileDigest = DigestUtils.generateDigestForSingleFile( + event.path, ); - final cachedFilePath = path.join(Utils.appCacheDirectory, file.digest); - if (generatedCodeFile.existsSync()) { - generatedCodeFile.copySync(cachedFilePath); - } else { - continue; - } - /// if file has been successfully copied, let's make an entry to the db - if (File(cachedFilePath).existsSync()) { - cacheEntry[file.digest] = cachedFilePath; - } else { - Logger.e( - 'ERROR: _cacheGeneratedCodesFor: failed to copy generated file $file', + if (newPubspecFileDigest != pubspecFileDigest) { + Logger.i( + 'As pubspec.yaml file has been modified, terminating cached_build_runner.\nNo further builds will be scheduled. Please restart the build.', ); + exit(0); } - } - - /// create a bulk entry - await _databaseService.createEntryForBulk(cacheEntry); + }); } } + +extension FileSystemEventExtensions on FileSystemEvent { + String get name => switch (type) { + FileSystemEvent.create => 'create', + FileSystemEvent.move => 'move', + FileSystemEvent.delete => 'delete', + FileSystemEvent.modify => 'modify', + _ => 'unknown $type', + }; +} diff --git a/lib/commands/build_command.dart b/lib/commands/build_command.dart index d71cd96..3173948 100644 --- a/lib/commands/build_command.dart +++ b/lib/commands/build_command.dart @@ -1,33 +1,34 @@ import 'dart:async'; import 'package:args/command_runner.dart'; - -import '../args/args_parser.dart'; -import '../args/args_utils.dart'; -import 'initializer.dart'; - -class BuildCommand extends Command { - late final ArgumentParser _argumentParser; +import 'package:cached_build_runner/args/args_utils.dart'; +import 'package:cached_build_runner/args/build_and_watch_argument_parser.dart'; +import 'package:cached_build_runner/cached_build_runner.dart'; +import 'package:cached_build_runner/commands/initializer.dart'; +import 'package:get_it/get_it.dart'; + +class BuildCommand extends Command { + late final BuildAndWatchArgumentParser _argumentParser; final Initializer _initializer; - - BuildCommand() : _initializer = Initializer() { - _argumentParser = ArgumentParser(argParser); - } + final CachedBuildRunner _cachedBuildRunner; @override - String get description => - 'Performs a single build on the specified targets and then exits.'; + String get description => 'Performs a single build on the specified targets and then exits.'; @override - String get name => ArgsUtils.build; + String get name => ArgsUtils.commands.build; + + BuildCommand({CachedBuildRunner? cachedBuildRunner}) + : _initializer = const Initializer(), + _cachedBuildRunner = cachedBuildRunner ?? GetIt.I() { + _argumentParser = BuildAndWatchArgumentParser(argParser); + } @override - FutureOr? run() async { - /// parse args for the command + Future run() { _argumentParser.parseArgs(argResults?.arguments); + _initializer.init(); - /// let's get the cachedBuildRunner and execute the build - final cachedBuildRunner = await _initializer.init(); - return cachedBuildRunner.build(); + return _cachedBuildRunner.build(); } } diff --git a/lib/commands/cache/cache_command.dart b/lib/commands/cache/cache_command.dart new file mode 100644 index 0000000..354a58f --- /dev/null +++ b/lib/commands/cache/cache_command.dart @@ -0,0 +1,17 @@ +import 'package:args/command_runner.dart'; +import 'package:cached_build_runner/args/args_utils.dart'; +import 'package:cached_build_runner/commands/cache/cache_prune_sub_command.dart'; +import 'package:cached_build_runner/commands/cache/list_cache_sub_command.dart'; + +class CacheCommand extends Command { + @override + String get description => 'Commands for inspecting and manipulating cache directory'; + + @override + String get name => ArgsUtils.commands.cache; + + CacheCommand() { + addSubcommand(CachePruneSubCommand()); + addSubcommand(ListCacheSubCommand()); + } +} diff --git a/lib/commands/cache/cache_prune_sub_command.dart b/lib/commands/cache/cache_prune_sub_command.dart new file mode 100644 index 0000000..34415c1 --- /dev/null +++ b/lib/commands/cache/cache_prune_sub_command.dart @@ -0,0 +1,41 @@ +import 'dart:async'; + +import 'package:args/command_runner.dart'; +import 'package:cached_build_runner/args/args_utils.dart'; +import 'package:cached_build_runner/args/cache_argument_parser.dart'; +import 'package:cached_build_runner/commands/initializer.dart'; +import 'package:cached_build_runner/core/cache_provider.dart'; +import 'package:cached_build_runner/utils/logger.dart'; +import 'package:get_it/get_it.dart'; + +class CachePruneSubCommand extends Command { + late final CacheArgumentParser _argumentParser; + final Initializer _initializer; + final CacheProvider _cacheProvider; + + @override + String get description => 'Prune cache directory'; + + @override + String get name => ArgsUtils.commands.prune; + + @override + bool get takesArguments => true; + + CachePruneSubCommand({CacheProvider? cacheProvider}) + : _cacheProvider = cacheProvider ?? GetIt.I(), + _initializer = const Initializer() { + _argumentParser = CacheArgumentParser(argParser); + } + + @override + Future run() async { + _argumentParser.parseArgs(argResults?.arguments); + _initializer.init(); + + Logger.i('Clearing cache...'); + await _cacheProvider.prune(); + + Logger.i('Done'); + } +} diff --git a/lib/commands/cache/list_cache_sub_command.dart b/lib/commands/cache/list_cache_sub_command.dart new file mode 100644 index 0000000..2bebff2 --- /dev/null +++ b/lib/commands/cache/list_cache_sub_command.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +import 'package:args/command_runner.dart'; +import 'package:cached_build_runner/args/args_utils.dart'; +import 'package:cached_build_runner/args/cache_argument_parser.dart'; +import 'package:cached_build_runner/cached_build_runner.dart'; +import 'package:cached_build_runner/commands/initializer.dart'; +import 'package:get_it/get_it.dart'; + +class ListCacheSubCommand extends Command { + late final CacheArgumentParser _argumentParser; + final Initializer _initializer; + final CachedBuildRunner _runner; + + @override + String get description => 'List cache directory'; + + @override + String get name => ArgsUtils.commands.list; + + @override + bool get takesArguments => true; + + ListCacheSubCommand({CachedBuildRunner? cachedBuildRunner}) + : _runner = cachedBuildRunner ?? GetIt.I(), + _initializer = const Initializer() { + _argumentParser = CacheArgumentParser(argParser); + } + + @override + Future run() { + _argumentParser.parseArgs(argResults?.arguments); + _initializer.init(); + + return _runner.listAllCachedFiles(); + } +} diff --git a/lib/commands/initializer.dart b/lib/commands/initializer.dart index 42e8dd9..c32ff72 100644 --- a/lib/commands/initializer.dart +++ b/lib/commands/initializer.dart @@ -1,32 +1,20 @@ import 'dart:io'; -import '../cached_build_runner.dart'; -import '../database/database_service.dart'; -import '../utils/utils.dart'; +import 'package:cached_build_runner/utils/utils.dart'; /// This class configures the CachedBuildRunner as per the arguments parsed. /// And also initializes any variables, or create cache directory if non-existing. class Initializer { - Future init() async { - /// the project directory is always where the `flutter run` command is executed - /// which is the current directory - Utils.projectDirectory = - Platform.environment['CACHED_BUILD_RUNNER_PROJECT_DIRECTORY'] ?? - Directory.current.path; + const Initializer(); + void init() { + // the project directory is always where the `flutter run` command is executed + // which is the current directory + Utils.projectDirectory = Platform.environment['CACHED_BUILD_RUNNER_PROJECT_DIRECTORY'] ?? Directory.current.path; - /// let's make the appCacheDirectory if not existing already + // let's make the appCacheDirectory if not existing already Directory(Utils.appCacheDirectory).createSync(recursive: true); - /// init package name of project + // init package name of project Utils.initAppPackageName(); - - /// initialize the database - final databaseService = Utils.isRedisUsed - ? RedisDatabaseService() - : HiveDatabaseService(Utils.appCacheDirectory); - await databaseService.init(); - - /// let's initiate the build - return CachedBuildRunner(databaseService); } } diff --git a/lib/commands/watch_command.dart b/lib/commands/watch_command.dart index 8a902ed..a3aea25 100644 --- a/lib/commands/watch_command.dart +++ b/lib/commands/watch_command.dart @@ -1,33 +1,35 @@ import 'dart:async'; import 'package:args/command_runner.dart'; - -import '../args/args_parser.dart'; -import '../args/args_utils.dart'; -import 'initializer.dart'; - -class WatchCommand extends Command { - late final ArgumentParser _argumentParser; +import 'package:cached_build_runner/args/args_utils.dart'; +import 'package:cached_build_runner/args/build_and_watch_argument_parser.dart'; +import 'package:cached_build_runner/cached_build_runner.dart'; +import 'package:cached_build_runner/commands/initializer.dart'; +import 'package:get_it/get_it.dart'; + +class WatchCommand extends Command { + late final BuildAndWatchArgumentParser _argumentParser; final Initializer _initializer; - - WatchCommand() : _initializer = Initializer() { - _argumentParser = ArgumentParser(argParser); - } + final CachedBuildRunner _cachedBuildRunner; @override String get description => 'Builds the specified targets, watching the file system for updates and rebuilding as appropriate.'; @override - String get name => ArgsUtils.watch; + String get name => ArgsUtils.commands.watch; + + WatchCommand({CachedBuildRunner? cachedBuildRunner}) + : _initializer = const Initializer(), + _cachedBuildRunner = cachedBuildRunner ?? GetIt.I() { + _argumentParser = BuildAndWatchArgumentParser(argParser); + } @override - FutureOr? run() async { - /// parse args for the command + Future run() { _argumentParser.parseArgs(argResults?.arguments); + _initializer.init(); - /// let's get the cachedBuildRunner and execute the build - final cachedBuildRunner = await _initializer.init(); - return cachedBuildRunner.watch(); + return _cachedBuildRunner.watch(); } } diff --git a/lib/core/build_runner_wrapper.dart b/lib/core/build_runner_wrapper.dart new file mode 100644 index 0000000..ffd3085 --- /dev/null +++ b/lib/core/build_runner_wrapper.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import 'package:cached_build_runner/model/code_file.dart'; +import 'package:cached_build_runner/utils/logger.dart'; +import 'package:cached_build_runner/utils/utils.dart'; + +class BuildRunnerWrapper { + const BuildRunnerWrapper(); + + bool runBuild(List files) { + if (files.isEmpty) return true; + Logger.header( + 'Generating Codes for non-cached files, found ${files.length} files', + ); + + Logger.v('Running build_runner build...', showPrefix: false); + + final filterList = _getBuildFilterList(files); + + Logger.d('Run: "flutter pub run build_runner build --build-filter $filterList"'); + final process = Process.runSync( + 'flutter', + ['pub', 'run', 'build_runner', 'build', '--delete-conflicting-outputs', '--build-filter', filterList], + workingDirectory: Utils.projectDirectory, + runInShell: true, + ); + final stdOut = process.stdout?.toString() ?? ''; + final stdErrr = process.stderr?.toString() ?? ''; + Logger.v(stdOut.trim(), showPrefix: false); + + if (stdErrr.trim().isNotEmpty) { + Logger.e(stdErrr.trim()); + } + + return process.exitCode == 0; + } + + /// Returns a comma-separated string of the file paths from the given list of [CodeFile]s + /// formatted for use as the argument for the --build-filter flag in the build_runner build command. + /// + /// The method maps the list of [CodeFile]s to a list of generated file paths, and then + /// returns a comma-separated string of the generated file paths. + /// + /// For example: + /// + /// final files = [CodeFile(path: 'lib/foo.dart', digest: 'abc123')]; + /// final buildFilter = _getBuildFilterList(files); + /// print(buildFilter); // 'lib/foo.g.dart'. + String _getBuildFilterList(List files) { + final paths = files.map((x) => x.getGeneratedFilePath()).toList(); + + return paths.join(','); + } +} diff --git a/lib/core/cache_provider.dart b/lib/core/cache_provider.dart new file mode 100644 index 0000000..43f8073 --- /dev/null +++ b/lib/core/cache_provider.dart @@ -0,0 +1,164 @@ +import 'dart:io'; + +import 'package:cached_build_runner/database/database_factory.dart'; +import 'package:cached_build_runner/database/database_service.dart'; +import 'package:cached_build_runner/model/code_file.dart'; +import 'package:cached_build_runner/utils/constants.dart'; +import 'package:cached_build_runner/utils/digest_utils.dart'; +import 'package:cached_build_runner/utils/logger.dart'; +import 'package:cached_build_runner/utils/utils.dart'; +import 'package:get_it/get_it.dart'; +import 'package:path/path.dart' as path; + +typedef CachedFilesResult = ({List good, List bad}); + +class CacheProvider { + final DatabaseFactory _databaseFactory; + + DatabaseService? __database; + + Future get _database async { + final dbInstance = __database; + if (dbInstance == null) { + final db = await _databaseFactory.create(); + __database = db; + + return db; + } + + return dbInstance; + } + + CacheProvider({DatabaseFactory? databaseFactory}) : _databaseFactory = databaseFactory ?? GetIt.I(); + + Future ensurePruning() async { + if (!Utils.isPruneEnabled) return; + + final database = await _database; + + Logger.i('Prunning is enabled - checking pubpsec.lock'); + + final pubspecLockPath = path.join(Utils.projectDirectory, Constants.pubpsecLockFileName); + final pubspecLock = File(pubspecLockPath); + + final fileExists = pubspecLock.existsSync(); + + if (!fileExists) { + Logger.e('No ${Constants.pubpsecLockFileName} exits'); + + return; + } + + final digest = DigestUtils.generateDigestForSingleFile(pubspecLockPath); + + final existingDigest = await database.getEntryByKey(Constants.pubpsecLockFileName); + + Logger.v('Pubspec.lock digest: $digest'); + Logger.v('Existing Pubspec.lock digest: $digest'); + Logger.v('Will prune? ${digest != existingDigest ? 'YES' : 'NO'}'); + + if (existingDigest != null && digest != existingDigest) { + Logger.i('!!! Pruning cache as pubspec.lock was changed from last time !!!'); + await database.prune(keysToKeep: [Constants.pubpsecLockFileName]); + } + + await _dbOperation((db) => db.createCustomEntry(Constants.pubpsecLockFileName, digest)); + } + + Future mapFilesToCache(List files) async { + final goodFiles = []; + final badFiles = []; + + final bulkMapping = await _dbOperation( + (db) async => await db.isMappingAvailableForBulk( + files.map((f) => f.digest), + ), + ); + + /// segregate good and bad files + /// good files -> files for whom the generated codes are available + /// bad files -> files for whom no generated codes are available in the cache + for (final file in files) { + final isGeneratedCodeAvailable = bulkMapping[file.digest] ?? false; + + /// mock generated files are always considered badFiles, + /// as they depends on various services, and to keep track of changes can become complicated + if (isGeneratedCodeAvailable) { + goodFiles.add(file); + } else { + badFiles.add(file); + } + } + + return (good: goodFiles, bad: badFiles); + } + + Future cacheFiles(List files) async { + if (files.isEmpty) { + return Logger.header('No new files to cache'); + } + + Logger.header('Caching ${files.length} files'); + + final cacheEntry = {}; + + for (final file in files) { + final generatedCodeFile = File(file.getGeneratedFilePath()); + Logger.v('Caching: ${Utils.getFileName(generatedCodeFile.path)}'); + + final cachedFilePath = path.join(Utils.appCacheDirectory, file.digest); + if (generatedCodeFile.existsSync()) { + final _ = generatedCodeFile.copySync(cachedFilePath); + } else { + continue; + } + + /// if file has been successfully copied, let's make an entry to the db + if (File(cachedFilePath).existsSync()) { + cacheEntry[file.digest] = cachedFilePath; + } else { + Logger.e( + 'ERROR: _cacheGeneratedCodesFor: failed to copy generated file $file', + ); + } + } + + /// create a bulk entry + await _dbOperation((db) => db.createEntryForBulk(cacheEntry)); + } + + Future copyGeneratedCodesFor(List files) async { + Logger.i('Copying cached files to project directory (${files.length} total)'); + + for (final file in files) { + final cachedGeneratedCodePath = await _dbOperation( + (db) async => await db.getCachedFilePath(file.digest), + ); + final generatedFilePath = file.getGeneratedFilePath(); + + Logger.v('Copying file: ${Utils.getFileName(generatedFilePath)}'); + final copiedFile = File(cachedGeneratedCodePath).copySync(generatedFilePath); + + /// check if the file was copied successfully + if (!copiedFile.existsSync()) { + Logger.e( + 'ERROR: _copyGeneratedCodesFor: failed to copy the cached file $file', + ); + } + } + } + + Future prune() { + return _dbOperation((db) => db.prune(keysToKeep: [])); + } + + Future> listCachedFiles() { + return _dbOperation((db) => db.getAllData()); + } + + Future _dbOperation(Transaction op) async { + final db = await _database; + + return db.transaction(op); + } +} diff --git a/lib/core/dependency_visitor.dart b/lib/core/dependency_visitor.dart index 1760938..b0a3b89 100644 --- a/lib/core/dependency_visitor.dart +++ b/lib/core/dependency_visitor.dart @@ -1,119 +1,58 @@ import 'dart:io'; +import 'package:cached_build_runner/utils/constants.dart'; +import 'package:cached_build_runner/utils/logger.dart'; +import 'package:cached_build_runner/utils/utils.dart'; import 'package:path/path.dart' as path; -import '../utils/utils.dart'; - +/// Used for calculating hash. class DependencyVisitor { static const _relativeImportsConst = 'relative-imports'; static const _absoluteImportsConst = 'absolute-imports'; - /// Regex for class name - final classNameRegex = RegExp(r"(?:mixin|abstract class|class)\s+(\w+)"); - - /// Regex for `extends`, `implements` & `with` - final extendsRegex = RegExp(r"extends\s+(\w+)"); - final implementsRegex = RegExp(r"implements\s+([\w\s,]+)"); - final withRegex = RegExp(r"with\s+([\w\s,]+)"); - - /// Regex for parsing import statements - final relativeImportRegex = RegExp(r'''import\s+(?!(\w+:))(?:'|")(.*?)('|");?'''); - final packageImportRegex = RegExp( - 'import\\s+\'package:${Utils.appPackageName}(.*)\';', - ); - final Map _visitorMap = {}; String _dirName = ''; - bool _hasNotVisited(String filePath) { - return _visitorMap[filePath] == null; - } - - void _markVisited(String filePath) { - _visitorMap[filePath] = true; - } - void reset() { _dirName = ''; _visitorMap.clear(); } - /// Method which returns back the dependant's paths of a class file + /// Method which returns back the dependant's paths of a class file. Set getDependenciesPath(String filePath) { _dirName = path.dirname(filePath); + Logger.d('Depdencies for $filePath'); final paths = _getDependenciesPath(filePath); - reset(); - return paths; - } - - Set _getDependenciesPath(String filePath) { - final dependencies = {}; - final contents = File(filePath).readAsStringSync(); - final classDependencies = _getClassDependency(contents); - final dependencyPaths = _resolveUri(contents, classDependencies); - - dependencies.addAll(dependencyPaths); - - /// Find out transitive dependencies - for (final dependencyPath in dependencyPaths) { - /// There can be a cyclic dependency, so to make sure we are not visiting the same node multiple times - if (_hasNotVisited(dependencyPath)) { - _markVisited(dependencyPath); - final transitiveDependencies = _getDependenciesPath(dependencyPath); - dependencies.addAll(transitiveDependencies); - } - } - - return dependencies; - } - - Map> _getImportLines(String dartSource) { - final relativeImports = []; - final absoluteImports = []; - - final lines = dartSource.split('\n'); - - for (final line in lines) { - final relativeMatch = relativeImportRegex.firstMatch(line); - final packageMatch = packageImportRegex.firstMatch(line); - - if (relativeMatch != null) { - final importedPath = relativeMatch.group(1); - if (importedPath != null) relativeImports.add(importedPath); - } - - if (packageMatch != null) { - final importedPath = packageMatch.group(1); - if (importedPath != null) absoluteImports.add(importedPath); - } - } + Logger.d('================================='); + reset(); - return { - _relativeImportsConst: relativeImports, - _absoluteImportsConst: absoluteImports, - }; + return paths; } - List convertImportStatementsToAbsolutePaths( + List _convertImportStatementsToAbsolutePaths( + String filePath, String contents, { String directory = 'lib', }) { + Logger.d('File: $filePath'); + final importLines = _getImportLines(contents); + final res = importLines.entries.map((value) => '${value.key}: ${value.value}').join('\n'); + Logger.d('$res\n'); + final relativeImportLines = importLines[_relativeImportsConst] ?? const []; final absoluteImportLines = importLines[_absoluteImportsConst] ?? const []; final paths = []; - /// absolute import lines + // absolute import lines for (final import in absoluteImportLines) { - paths.add( - path.join(Utils.projectDirectory, directory, import.substring(1)), - ); + paths.add(path.join(Utils.projectDirectory, directory, import)); } - /// relative import lines + // relative import lines for (final import in relativeImportLines) { paths.add(path.normalize(path.join(_dirName, import))); } @@ -121,47 +60,63 @@ class DependencyVisitor { return paths; } - Set _resolveUri(String contents, Set dependencies) { - final importPaths = convertImportStatementsToAbsolutePaths(contents); - final paths = {}; + bool _hasNotVisited(String filePath) { + return _visitorMap[filePath] == null; + } + + void _markVisited(String filePath) { + _visitorMap[filePath] = true; + } + + Set _getDependenciesPath(String filePath) { + final dependencies = {}; + final contents = File(filePath).readAsStringSync(); + + final imports = _convertImportStatementsToAbsolutePaths(filePath, contents); - for (final dependencyPath in importPaths) { - final dependencyContents = File(dependencyPath).readAsStringSync(); + final _ = dependencies.add(filePath); - for (final dependencyClass in dependencies) { - final classNameMatch = classNameRegex.firstMatch(dependencyContents); - final className = classNameMatch?.group(1); - if (className == dependencyClass) { - paths.add(dependencyPath); - } + // Find out transitive dependencies + for (final import in imports) { + // There can be a cyclic dependency, so to make sure we are not visiting the same node multiple times + if (_hasNotVisited(import)) { + _markVisited(import); + // ignore: avoid-recursive-calls, recursive call is ok. + final transitiveDependencies = _getDependenciesPath(import); + dependencies.addAll(transitiveDependencies); } } - return paths; + return dependencies; } - Set _getClassDependency(String contents) { - final dependencies = {}; + Map> _getImportLines(String dartSource) { + final relativeImports = []; + final absoluteImports = []; - /// find matches - final extendsMatch = extendsRegex.firstMatch(contents); - final implementsMatch = implementsRegex.firstMatch(contents); - final withMatch = withRegex.firstMatch(contents); + final relativeMatches = Constants.relativeOrPartFileImportRegex.allMatches(dartSource); + final packageMatches = Constants.appPackageImportRegex.allMatches(dartSource); - if (extendsMatch != null) { - dependencies.add(extendsMatch.group(1) ?? ''); - } + for (final match in relativeMatches) { + final importedPath = match.group(1); + if (importedPath != null) { + Logger.d('Rel. import -> $importedPath'); - if (implementsMatch != null) { - final interfaces = implementsMatch.group(1)?.split(',') ?? const []; - dependencies.addAll(interfaces.map((i) => i.trim())); + relativeImports.add(importedPath); + } } - if (withMatch != null) { - final mixins = withMatch.group(1)?.split(',') ?? const []; - dependencies.addAll(mixins.map((m) => m.trim())); + for (final match in packageMatches) { + final importedPath = match.group(1); + if (importedPath != null) { + Logger.d('Abs. import -> $importedPath'); + absoluteImports.add(importedPath); + } } - return dependencies; + return { + _absoluteImportsConst: absoluteImports, + _relativeImportsConst: relativeImports, + }; } } diff --git a/lib/core/file_parser.dart b/lib/core/file_parser.dart new file mode 100644 index 0000000..5cd7b73 --- /dev/null +++ b/lib/core/file_parser.dart @@ -0,0 +1,79 @@ +import 'dart:io'; + +import 'package:cached_build_runner/core/dependency_visitor.dart'; +import 'package:cached_build_runner/model/code_file.dart'; +import 'package:cached_build_runner/model/code_file_generated_type.dart'; +import 'package:cached_build_runner/utils/constants.dart'; +import 'package:cached_build_runner/utils/digest_utils.dart'; +import 'package:cached_build_runner/utils/extension.dart'; +import 'package:cached_build_runner/utils/logger.dart'; +import 'package:cached_build_runner/utils/utils.dart'; +import 'package:get_it/get_it.dart'; +import 'package:path/path.dart' as path; + +typedef CodeFileBuild = ({String path, String? suffix, CodeFileGeneratedType type}); + +class FileParser { + final DependencyVisitor _dependencyVisitor; + + FileParser({DependencyVisitor? dependencyVisitor}) + : _dependencyVisitor = dependencyVisitor ?? GetIt.I(); + + /// Returns a list of [CodeFile] instances that represent the files that need code generation. + List getFilesNeedingGeneration() { + // Files in "lib/" that needs code generation + final libDirectory = Directory(path.join(Utils.projectDirectory, 'lib')); + + final libPathList = []; + + final libFiles = libDirectory.listSync( + recursive: true, + followLinks: false, + ); + + for (final entity in libFiles) { + if (entity is! File || !entity.isDartSourceCodeFile()) continue; + + final result = _parseFile(entity); + + if (result != null) libPathList.add(result); + } + + Logger.i( + 'Found ${libPathList.length} files in "lib/" that supports code generation', + ); + + return libPathList + .map( + (f) => CodeFile( + path: f.path, + digest: DigestUtils.generateDigestForClassFile( + _dependencyVisitor, + f.path, + ), + suffix: f.suffix, + generatedType: f.type, + ), + ) + .toList(); + } + + CodeFileBuild? _parseFile(File entity) { + final filePath = entity.path.trim(); + final fileContent = entity.readAsStringSync(); + + final partMatch = Constants.partGeneratedFileRegex.firstMatch(fileContent); + + if (partMatch != null) { + return (path: filePath, suffix: partMatch.group(1), type: CodeFileGeneratedType.partFile); + } + + final importMatch = Constants.generatedFileImportRegExp.firstMatch(fileContent); + + if (importMatch != null) { + return (path: filePath, suffix: importMatch.group(1), type: CodeFileGeneratedType.import); + } + + return null; + } +} diff --git a/lib/database/database_factory.dart b/lib/database/database_factory.dart new file mode 100644 index 0000000..30db13a --- /dev/null +++ b/lib/database/database_factory.dart @@ -0,0 +1,19 @@ +import 'package:cached_build_runner/database/database_service.dart'; +import 'package:cached_build_runner/database/hive_database_service.dart'; +import 'package:cached_build_runner/utils/utils.dart'; + +// ignore: one_member_abstracts, it is ok +abstract class DatabaseFactory { + Future create(); +} + +class HiveDatabaseFactory extends DatabaseFactory { + @override + Future create() async { + final service = HiveDatabaseService(Utils.appCacheDirectory); + + await service.init(); + + return service; + } +} diff --git a/lib/database/database_service.dart b/lib/database/database_service.dart index 81c7136..5cdeb9e 100644 --- a/lib/database/database_service.dart +++ b/lib/database/database_service.dart @@ -1,18 +1,15 @@ import 'dart:async'; -import 'dart:io'; -import 'package:hive/hive.dart'; -import 'package:path/path.dart' as path; -import 'package:redis/redis.dart'; - -import '../utils/log.dart'; -import '../utils/utils.dart'; +typedef Transaction = Future Function(DatabaseService db); /// An interface for a database service used to cache generated code. abstract class DatabaseService { /// Initializes the database service. Future init(); + /// Returns all stored keys and their associated value. + Future> getAllData(); + /// Checks if the mapping is available for the given digests in bulk. FutureOr> isMappingAvailableForBulk( Iterable digests, @@ -35,246 +32,18 @@ abstract class DatabaseService { /// Creates an entry for the given digest and cached file path. Future createEntry(String digest, String cachedFilePath); - /// Flushes the database service. Flushing to disk, or closing network connections - /// could be done here. - Future flush(); -} - -/// An implementation of [DatabaseService] using Redis. -class RedisDatabaseService implements DatabaseService { - static const _tag = 'RedisDatabaseService'; - - static const _redisHost = 'localhost'; - static const _redisPort = 6379; - - late RedisConnection _connection; - late Command _command; - - final Map _cache = {}; - - @override - Future createEntry(String digest, String cachedFilePath) async { - _command.set(digest, cachedFilePath); - } - - @override - Future flush() async { - /// a short delay to make sure all network connections are done before we close the connection - _command.pipe_end(); - await _connection.close(); - } - - @override - FutureOr getCachedFilePath(String digest) async { - final data = _cache[digest]; - if (data == null) { - throw Exception( - '$_tag: getCachedFilePath: asked path for non existing digest', - ); - } - - return data; - } - - File _generateConfigurationFile() { - final configuration = """ -# Redis configuration file - -# Specify the directory where Redis will store its data -dir ${Utils.appCacheDirectory} - -# Specify the save duration to disk: save -# this saves to disk every 1 min if at least 1 key has changed -save 60 1 -"""; - - final configurationFile = File( - path.join(Utils.appCacheDirectory, 'redis.conf'), - ); - configurationFile.writeAsStringSync(configuration); - return configurationFile; - } - - @override - Future init() async { - final configurationPath = _generateConfigurationFile(); - _connection = RedisConnection(); - try { - _command = await _connection.connect(_redisHost, _redisPort); - } on SocketException catch (_) { - final process = await Process.start( - 'redis-server', - [configurationPath.path], - mode: ProcessStartMode.detached, - ); - Logger.v('Redis started with PID ${process.pid}'); - - /// assumption: redis would fire up within this delayed duration - await Utils.delay500ms(); - _command = await _connection.connect(_redisHost, _redisPort); - } - - _command.pipe_start(); - } - - @override - FutureOr isMappingAvailable(String digest) async { - final resp = await _command.get(digest); - if (resp != null) { - _cache[digest] = resp.toString(); - return true; - } - - return false; - } - - @override - Future createEntryForBulk(Map cachedFilePaths) async { - final transaction = await _command.multi(); - final futures = >[]; - - for (final cache in cachedFilePaths.entries) { - futures.add(transaction.set(cache.key, cache.value)); - } - - final response = await transaction.exec(); - if (response.toString() == 'OK') { - await Future.wait(futures); - } else { - throw Exception('$_tag: createEntryForBulk: Redis Error $response'); - } - } - - @override - FutureOr> getCachedFilePathForBulk( - Iterable digests, - ) { - for (final digest in digests) { - if (!_cache.containsKey(digest)) { - throw Exception( - '$_tag: getCachedFilePathForBulk: asked path for non existing digest: $digest', - ); - } - } - return _cache; - } - - Future> _waitMapFutures( - Map> map, - ) async { - final result = {}; - final keys = map.keys.toList(); - final values = await Future.wait(map.values); - for (int i = 0; i < keys.length; i++) { - result[keys[i]] = values[i]; - } - return result; - } + /// Creates custom [entry] under [key]. + Future createCustomEntry(String key, String entry); - @override - FutureOr> isMappingAvailableForBulk( - Iterable digests, - ) async { - final transaction = await _command.multi(); - - final futures = >{}; - - for (final digest in digests) { - futures[digest] = transaction.get(digest); - } - - late Map data; - - final response = await transaction.exec(); - if (response.toString() == 'OK') { - data = await _waitMapFutures(futures); - - _cache.clear(); - for (var entry in data.entries) { - if (entry.value != null) _cache[entry.key] = entry.value!; - } - } else { - throw Exception( - '$_tag: isMappingAvailableForBulk: Redis Error $response', - ); - } - - return data.map((key, value) => MapEntry(key, value != null)); - } -} - -/// An implementation of [DatabaseService] using Hive. -class HiveDatabaseService implements DatabaseService { - final String dirPath; - - HiveDatabaseService(this.dirPath); - - static const _tag = 'HiveDatabaseService'; - static const _boxName = 'generated-file-box'; - - late Box _box; - - @override - Future init() async { - Hive.init(dirPath); - _box = await Hive.openBox(_boxName); - } - - @override - FutureOr isMappingAvailable(String digest) { - return _box.containsKey(digest); - } + /// Gets entry by key. + Future getEntryByKey(String key); - @override - FutureOr getCachedFilePath(String digest) { - final filePath = _box.get(digest); - if (filePath == null) { - throw Exception( - '$_tag: getCachedFilePath: asked path for non existing digest', - ); - } + /// Delete all records except [keysToKeep]. + Future prune({required List keysToKeep}); - return filePath; - } - - @override - Future createEntry(String digest, String cachedFilePath) { - return _box.put(digest, cachedFilePath); - } - - @override - Future flush() { - return _box.flush(); - } - - @override - Future createEntryForBulk(Map cachedFilePaths) { - return _box.putAll(cachedFilePaths); - } - - @override - FutureOr> getCachedFilePathForBulk( - Iterable digests, - ) { - final Map data = {}; - - for (final digest in digests) { - data[digest] = getCachedFilePath(digest) as String; - } - - return data; - } - - @override - FutureOr> isMappingAvailableForBulk( - Iterable digests, - ) { - final Map data = {}; - - for (final digest in digests) { - data[digest] = isMappingAvailable(digest) as bool; - } + /// Flushes the database service. Flushing to disk, or closing network connections + /// could be done here. + Future flush(); - return data; - } + Future transaction(Transaction transactionCallback); } diff --git a/lib/database/hive_database_service.dart b/lib/database/hive_database_service.dart new file mode 100644 index 0000000..330d98d --- /dev/null +++ b/lib/database/hive_database_service.dart @@ -0,0 +1,129 @@ +import 'dart:async'; + +import 'package:cached_build_runner/database/database_service.dart'; +import 'package:hive/hive.dart'; + +/// An implementation of [DatabaseService] using Hive. +class HiveDatabaseService implements DatabaseService { + final String dirPath; + + static const _tag = 'HiveDatabaseService'; + static const _boxName = 'generated-file-box'; + + late Box _box; + + HiveDatabaseService(this.dirPath); + + @override + Future init() async { + Hive.init(dirPath); + _box = await Hive.openBox(_boxName); + } + + @override + FutureOr isMappingAvailable(String digest) { + return _box.containsKey(digest); + } + + @override + FutureOr getCachedFilePath(String digest) { + final filePath = _box.get(digest); + if (filePath == null) { + throw Exception( + '$_tag: getCachedFilePath: asked path for non existing digest', + ); + } + + return filePath; + } + + @override + Future createEntry(String digest, String cachedFilePath) { + return _box.put(digest, cachedFilePath); + } + + @override + Future flush() { + return _box.flush(); + } + + @override + Future createEntryForBulk(Map cachedFilePaths) { + return _box.putAll(cachedFilePaths); + } + + @override + FutureOr> getCachedFilePathForBulk( + Iterable digests, + ) { + final data = {}; + + for (final digest in digests) { + data[digest] = getCachedFilePath(digest) as String; + } + + return data; + } + + @override + FutureOr> isMappingAvailableForBulk( + Iterable digests, + ) { + final data = {}; + + for (final digest in digests) { + data[digest] = isMappingAvailable(digest) as bool; + } + + return data; + } + + @override + Future createCustomEntry(String key, String entry) { + return _box.put(key, entry); + } + + @override + Future getEntryByKey(String key) async { + return _box.get(key); + } + + @override + Future prune({required List keysToKeep}) async { + final saved = {}; + + for (final key in keysToKeep) { + final value = _box.get(key); + if (value != null) saved[key] = value; + } + + final _ = await _box.clear(); + + for (final key in keysToKeep) { + await _box.put(key, saved[key]!); + } + + await flush(); + } + + @override + Future transaction(Transaction transactionCallback) async { + final result = await transactionCallback(this); + + await flush(); + + return result; + } + + @override + Future> getAllData() { + final result = {}; + for (final key in _box.keys) { + final value = _box.get(key); + + result[key as String] = value ?? ''; + } + + return Future.value(result); + } +} diff --git a/lib/di_container.dart b/lib/di_container.dart new file mode 100644 index 0000000..abc0fb0 --- /dev/null +++ b/lib/di_container.dart @@ -0,0 +1,19 @@ +import 'package:cached_build_runner/cached_build_runner.dart'; +import 'package:cached_build_runner/core/build_runner_wrapper.dart'; +import 'package:cached_build_runner/core/cache_provider.dart'; +import 'package:cached_build_runner/core/dependency_visitor.dart'; +import 'package:cached_build_runner/core/file_parser.dart'; +import 'package:cached_build_runner/database/database_factory.dart'; +import 'package:get_it/get_it.dart'; + +class DiContainer { + static void setup() { + GetIt.instance + ..registerSingleton(DependencyVisitor()) + ..registerFactory(HiveDatabaseFactory.new) + ..registerFactory(FileParser.new) + ..registerFactory(CachedBuildRunner.new) + ..registerFactory(BuildRunnerWrapper.new) + ..registerSingleton(CacheProvider()); + } +} diff --git a/lib/model/code_file.dart b/lib/model/code_file.dart index 8e5d14a..227f8ca 100644 --- a/lib/model/code_file.dart +++ b/lib/model/code_file.dart @@ -1,16 +1,34 @@ +import 'package:cached_build_runner/model/code_file_generated_type.dart'; +import 'package:cached_build_runner/utils/utils.dart'; +import 'package:path/path.dart' as path_utils; + class CodeFile { final String path; final String digest; final bool isTestFile; + final String? suffix; + final CodeFileGeneratedType generatedType; - CodeFile({ + const CodeFile({ required this.path, required this.digest, + required this.suffix, + required this.generatedType, this.isTestFile = false, }); + String getGeneratedFilePath() { + final lastDotDart = path.lastIndexOf('.dart'); + + final fileExtension = '.${suffix ?? 'g'}.dart'; + // ignore: avoid-substring, should be ok. + final subPath = '${path.substring(0, lastDotDart)}$fileExtension'; + + return path_utils.relative(subPath, from: Utils.projectDirectory); + } + @override String toString() { - return '($path, $digest, $isTestFile)'; + return '($path, $digest, $isTestFile, Suffix: ${suffix ?? 'null'})'; } } diff --git a/lib/model/code_file_generated_type.dart b/lib/model/code_file_generated_type.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart new file mode 100644 index 0000000..0caae86 --- /dev/null +++ b/lib/utils/constants.dart @@ -0,0 +1,71 @@ +import 'package:cached_build_runner/utils/utils.dart'; + +class Constants { + static const String pubpsecLockFileName = 'pubspec.lock'; + + /// Regex match "generated" file "suffix". + /// + /// E.g.: + /// + /// `user.g.dart` -> matches with `g` + /// + /// `user.freezed.dart` -> matches `freezed`. + static final partFileExtensionRegex = RegExp(r'.+\..+\.dart'); + + /// Regex matches part file with "generated" file. Group 1 match suffix. + /// Handles relative parts as well. + /// + /// E.g.: + /// + /// `part user.g.dart` -> matches with `g` + /// + /// `part user.freezed.dart` -> matches with `freezed`. + static final partGeneratedFileRegex = RegExp(r"part '.+\.(.+)\.dart';"); + + /// Regex matches part files except with "generated" suffix. + /// Handles relative parts as well. + /// E.g.: + /// + /// `part user.dart` -> matches + /// + /// `part user.freezed.dart` -> does not. + static final partFileRegex = RegExp(r'''^part\s+['\"]((?:.+\/)*[^\.]+\.dart)['\"];'''); + + /// Regex matches any `import package:PACKAGE/...` - any import within same package. + /// + /// E.g.: + /// + /// `import 'package:PACKAGE/foo.dart'` -> matches + /// + /// `import 'package:json_serializable/json_serializable.dart'` -> doesn't match. + static final appPackageImportRegex = + RegExp('import\\s+[\'"]package:${Utils.appPackageName}/(.*)[\'"];', multiLine: true); + + /// Regex matches any relative import or part file except generated ones. + /// + /// E.g.: + ///``` + /// import 'x.dart' -> matches + /// import '../../x.dart' -> matches + /// import 'x/z.dart' -> matches + /// part 'x.dart' -> matches + /// + /// import 'package:json_serializable/json_serializable.dart' -> doesn't match. + /// import 'x.g.dart' -> doesn't match. + /// part 'x.g.dart' -> doesn't match. + /// ``` + static final relativeOrPartFileImportRegex = RegExp( + r'''^\s*(?:import|part)\s+(?:\'|\")((?:(?!package:).+\/[^\.]+.dart)|(?:(?!package:)[^\.\/]+.dart))(?:\'|\")\s*;''', + multiLine: true, + ); + + /// Regex matches any import with generated part except "foo.g.dart" part. + /// + /// E.g.: + ///``` + /// import 'package:PACAKGE/x.auto_mappr.dart' -> matches + /// + /// import 'package:PACAKGE/x.g.dart' -> doesn't match. + /// ``` + static final generatedFileImportRegExp = RegExp(r'''import\s+['\"].+\.([^g\/\.].*|g.+)\.dart['\"]'''); +} diff --git a/lib/utils/digest_utils.dart b/lib/utils/digest_utils.dart index 8013279..4292e1c 100644 --- a/lib/utils/digest_utils.dart +++ b/lib/utils/digest_utils.dart @@ -1,10 +1,11 @@ +// ignore_for_file: avoid-weak-cryptographic-algorithms + import 'dart:convert'; import 'dart:io'; +import 'package:cached_build_runner/core/dependency_visitor.dart'; import 'package:crypto/crypto.dart'; -import '../core/dependency_visitor.dart'; - abstract class DigestUtils { /// Calculates the MD5 digest of a given string [string]. static String generateDigestForRawString(String string) { @@ -27,19 +28,21 @@ abstract class DigestUtils { final sb = StringBuffer(); for (final file in filePaths) { - sb.write(generateDigestForSingleFile(file)); - sb.write('-'); + sb + ..write(generateDigestForSingleFile(file)) + ..write('-'); } return generateDigestForRawString(sb.toString()); } - /// Calculates a MD5 digest of a given class file, considering it's dependencies as well + /// Calculates a MD5 digest of a given class file, considering it's dependencies as well. static String generateDigestForClassFile( DependencyVisitor visitor, String filePath, ) { final dependencies = visitor.getDependenciesPath(filePath); + return generateDigestForMultipleFile([filePath, ...dependencies]); } @@ -57,8 +60,9 @@ abstract class DigestUtils { final sb = StringBuffer(); for (final file in filePaths) { - sb.write(generateDigestForClassFile(visitor, file)); - sb.write('-'); + sb + ..write(generateDigestForClassFile(visitor, file)) + ..write('-'); } return generateDigestForRawString(sb.toString()); diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart index 26a8e96..5ee9d2c 100644 --- a/lib/utils/extension.dart +++ b/lib/utils/extension.dart @@ -1,20 +1,21 @@ +// ignore_for_file: prefer-match-file-name + import 'dart:io'; +import 'package:cached_build_runner/utils/constants.dart'; + extension DirectoryExtn on Directory { Stream watchDartSourceCodeFiles() { return watch(recursive: true).where( - (e) => - e.path.endsWith('.dart') && - !e.path.endsWith('.g.dart') && - !e.path.endsWith('.mocks.dart'), + (e) => e.path.endsWith('.dart') && Constants.partFileExtensionRegex.allMatches(e.path).isEmpty, ); } } extension FileExtn on File { bool isDartSourceCodeFile() { - return path.endsWith('.dart') && - !path.endsWith('.g.dart') && - !path.endsWith('.mocks.dart'); + final matches = Constants.partFileExtensionRegex.allMatches(path); + + return path.endsWith('.dart') && matches.isEmpty; } } diff --git a/lib/utils/log.dart b/lib/utils/logger.dart similarity index 66% rename from lib/utils/log.dart rename to lib/utils/logger.dart index 9dacab0..694106b 100644 --- a/lib/utils/log.dart +++ b/lib/utils/logger.dart @@ -1,7 +1,6 @@ +import 'package:cached_build_runner/utils/utils.dart'; import 'package:logger/logger.dart' as logger; -import 'utils.dart'; - /// A class that provides logging functionality. class Logger { static final _logger = logger.Logger( @@ -9,10 +8,7 @@ class Logger { printer: logger.PrettyPrinter( methodCount: 0, errorMethodCount: 0, - lineLength: 120, - colors: true, printEmojis: false, - printTime: false, noBoxingByDefault: true, ), ); @@ -22,27 +18,34 @@ class Logger { printer: logger.PrettyPrinter( methodCount: 0, errorMethodCount: 0, - lineLength: 120, - colors: true, printEmojis: false, - printTime: false, ), ); /// Logs a verbose message. /// /// If [showPrefix] is `true`, the message will be prefixed with a vertical bar. - static v(String message, {bool showPrefix = true}) { + static void v(String message, {bool showPrefix = true}) { + // ignore: avoid-non-ascii-symbols, ascii here is ok. _logger.v('${showPrefix ? '│ ' : ''}$message'); } + /// Logs a debug message. + static void d(String message) { + _logger.d(message); + } + /// Logs an information message. - static i(String message) { + static void i(String message) { + _logger.i(message); + } + + static void header(String message) { _loggerWithBox.i(message); } /// Logs an error message. - static e(String message) { + static void e(String message) { _loggerWithBox.e(message); } } @@ -54,6 +57,11 @@ class _LogFilter extends logger.LogFilter { @override bool shouldLog(logger.LogEvent event) { if (event.level == logger.Level.error) return true; - return Utils.isVerbose; + + if (event.level == logger.Level.verbose && Utils.isVerbose) return true; + + if (event.level == logger.Level.debug && Utils.isDebug) return true; + + return event.level != logger.Level.verbose && event.level != logger.Level.debug; } } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index cb2bc56..b2848ad 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,26 +1,28 @@ +// ignore_for_file: avoid-global-state + import 'dart:io'; +import 'package:cached_build_runner/utils/logger.dart'; import 'package:path/path.dart' as path; -import 'log.dart'; - /// A utility class that provides helper methods for various operations. abstract class Utils { static String appPackageName = ''; static String appCacheDirectory = ''; static String projectDirectory = ''; static bool isVerbose = true; + static bool isDebug = false; static bool isRedisUsed = false; + static bool isPruneEnabled = false; /// Initializes the app package name by reading it from pubspec.yaml. static void initAppPackageName() { const pubspecFileName = 'pubspec.yaml'; const searchString = 'name:'; - final pubspecFile = File(path.join( - Utils.projectDirectory, - pubspecFileName, - )); + final pubspecFile = File( + path.join(Utils.projectDirectory, pubspecFileName), + ); if (!pubspecFile.existsSync()) { reportError( @@ -30,18 +32,14 @@ abstract class Utils { for (final line in pubspecFile.readAsLinesSync()) { if (line.contains(searchString)) { - appPackageName = line.split(searchString).last.trim(); + appPackageName = line.split(searchString).lastOrNull?.trim() ?? ''; } } } - /// Logs a header [title] to the console. - static void logHeader(String title) { - Logger.i(title); - } - /// Retrieves the file name from the given [path]. static String getFileName(String path) { + // ignore: avoid-unsafe-collection-methods, its safe. return path.split('/').last; } @@ -55,12 +53,7 @@ abstract class Utils { const defaultCacheDirectoryName = '.cached_build_runner'; String homeDir; - if (Platform.isWindows) { - homeDir = Platform.environment['USERPROFILE'] ?? ''; - } else { - homeDir = Platform.environment['HOME'] ?? ''; - } - + homeDir = Platform.isWindows ? Platform.environment['USERPROFILE'] ?? '' : Platform.environment['HOME'] ?? ''; if (homeDir.isEmpty) { reportError( 'Could not set default cache directory. Please use the --cache-directory flag to provide a cache directory.', diff --git a/pubspec.yaml b/pubspec.yaml index adbf994..3cec055 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,22 +1,24 @@ name: cached_build_runner description: Optimizes the build_runner by caching generated codes for non changed .dart files -version: 0.0.7 +version: 0.1.0-pre3 homepage: https://github.com/jyotirmoy-paul/cached_build_runner maintainer: Jyotirmoy Paul (@jyotirmoy-paul) - environment: - sdk: '>=2.18.5 <3.0.0' + sdk: ">=3.0.0 <4.0.0" dependencies: + ansicolor: ^2.0.2 args: ^2.4.0 + barbecue: ^0.5.0 crypto: ^3.0.1 + get_it: ^7.6.7 hive: ^2.2.3 logger: ^1.1.0 + meta: ^1.11.0 path: ^1.8.2 - redis: ^3.1.0 synchronized: ^3.0.1 + text_table: ^4.0.3 dev_dependencies: - lints: 2.0.0 - test: 1.16.0 \ No newline at end of file + netglade_analysis: ^7.0.0