Using extensions
- Dominant language
- Dart
- Stars
- 266
- Forks
- 36
- Avg merge
- 23h 25m
- Merged PRs (30d)
- 2
Description
This is not an issue.
First off - thank you for the grinder tool. It is great find for me, since I found `make` didn't manage files with spaces and I have a bunch of them.
I've created a set of extensions to `String`, `Glob`, `File`, `Directory`, `Iterable`, `FileSet`, and `List` that I found quite useful so I decided to share them here if anyone wants to use them or add them to the package or for their personal use.
Consider them public domain. They are created for `grinder: "0.8.5"`. So they might not work for future visitors in case the package introduces breaking changes.
```dart
// grind_ext.dart
import 'dart:io';
import 'package:grinder/grinder.dart';
import 'package:glob/glob.dart';
// TODO: do we need more sophisticated glob detection?
final _globRegex = new RegExp(r'[*?{\[]');
extension FileSetStringExt on String {
/// Returns a [File]
File get asFile => File(this);
/// Returns a [Directory]
Directory get asDirectory => Directory(this);
/// Converts this to a [File] and then to a [FileSet]
///
/// If this includes [Glob] characters: *, ?, {, or [ it will first be
/// converted to a [Glob] and that to a [FileSet].
///
/// Note: [Glob] doesn't exclude files and dirs starting with a dot, while
/// a recursive [FileSet] does.
///
/// If you know this is a [Directory], then better use [allFiles].
FileSet get asFileSet {
if (this.contains(_globRegex)) {
return Glob(this).asFileSet;
}
return this.asFile.asFileSet;
}
/// Converts this to a [Directory] and passes all params to its [allFiles]
/// method
FileSet allFiles({String pattern, bool recurse = true}) =>
this.asDirectory.allFiles(pattern: pattern, recurse: recurse);
/// Converts this to a [Directory] and passes all params to its [allDartFiles]
/// method
FileSet allDartFiles({bool recurse = true}) =>
this.asDirectory.allDartFiles(recurse: recurse);
}
extension FileSetGlobExt on Glob {
/// Runs [listSync] and collects all [File] objects into a [FileSet]
FileSet get asFileSet =>
listSync(followLinks: false).whereType().asFileSet;
}
extension FileSetFileExt on File {
/// Returns a [FileSet]
FileSet get asFileSet => FileSet.fromFile(this);
}
extension FileSetDirExt on Directory {
/// Returns a [FileSet], passing all params to [fromDir]
FileSet fileSet({String pattern, bool recurse = true}) =>
FileSet.fromDir(this, pattern: pattern, recurse: recurse);
/// Returns the last modified date of the directory
DateTime get lastModified => this.statSync().modified;
/// Returns a [FileSet] using [fileSet]
FileSet allFiles({String pattern, bool recurse = true}) =>
this.fileSet(pattern: pattern, recurse: recurse);
/// Returns a [FileSet] using [fileSet], scoped to Dart files only
FileSet allDartFiles({bool recurse = true}) =>
this.fileSet(pattern: '*.dart', recurse: recurse);
}
extension ListFileExt on Iterable {
/// Converts to a [FileSet]
FileSet get asFileSet => FileSet.fromFile(this.first)..addFiles(this.skip(1));
}
extension ListStringExt on Iterable {
/// Converts to a list of [File] and then to a [FileSet]
FileSet get asFileSet => map((el) => el.asFile).asFileSet;
}
extension OperationsExt on FileSet {
/// Adds a [File] to the [files]
void addFile(File file) {
this.files.add(file);
}
/// Appends all [File] objects to the [files]
void addFiles(Iterable files) {
this.files.addAll(files);
}
/// Adds a [String], [File], [Directory], [Glob] or another [FileSet],
/// producing a new [FileSet]
FileSet operator +(dynamic value) {
if (value is FileSet) {
// break-out first
} else if (value is String) {
value = FileSetStringExt(value).asFileSet;
} else if (value is File) {
value = FileSetFileExt(value).asFileSet;
} else if (value is Directory) {
value = FileSetDirExt(value).fileSet();
} else if (value is Glob) {
value = FileSetGlobExt(value).asFileSet;
}
if (value is FileSet) {
return FileSet.fromFile(this.files.first)
..addFiles(this.files.skip(1))
..addFiles(value.files);
}
throw ArgumentError.value(value, 'value',
'must be either a String, File, Directory, Glob or another FileSet');
}
}
extension ListDateTimeExt on List {
/// Returns tha latest [DateTime] in the list (or `null`)
DateTime get latest {
if (isEmpty) return null;
final zero = DateTime.fromMillisecondsSinceEpoch(0);
return reduce(
(curr, el) => el != null && el.isAfter(curr ?? zero) ? el : curr);
}
}
```
Open up these details if you want to see how I use them.
```dart
import 'package:grinder/grinder.dart';
import 'package:pubspec_yaml/pubspec_yaml.dart';
import './grind_ext.dart';
const projectName = 'Runner';
const iosOutDir = 'build/ios';
// NOTE: these are evaluated lazily - when read for the first time
final pubspec = 'pubspec.yaml'.asFile;
final version =
pubspec.readAsStringSync().toPubspecYaml().version.valueOr(() => null);
final appFiles = pubspec.asFileSet + libDir.allDartFiles();
// --- iOS values
final iosFiles = appFiles +
'ios/Podfile' +
'ios/Flutter/{Debug,Release}.xcconfig'.asFileSet +
'ios/$projectName'.allFiles() +
'ios/$projectName.xcodeproj'.allFiles();
final iosFilesExists = iosFiles.exists;
final iosFilesLastModified = iosFiles.lastModified;
final iosIpa = '$iosOutDir/iphoneos/Runner.app'.asDirectory;
final iosXCArchiveName = '$projectName-$version';
final iosXCArchive = '$iosOutDir/$iosXCArchiveName.xcarchive'.asDirectory;
void main(List args) => grind(args, verifyProjectRoot: true);
// /////////////////////////////////////////////////////////////////////////////
@DefaultTask()
void usage() => print('Run `grind --help` to list available tasks.');
// /////////////////////////////////////////////////////////////////////////////
@Task('Builds a release iOS archive.')
void buildIos() {
if (version == null) fail('unable to read version in pubspec.yaml');
if (!iosFilesExists) fail('missing iOS files');
final hasArchive = iosXCArchive.existsSync();
if (!hasArchive || iosFilesLastModified.isAfter(iosXCArchive.lastModified)) {
// NOTE: we only want to depend on this if any of the iosFiles have changed!
// TODO: can we generate ios/Flutter/Generated.xcconfig without building via flutter?
flutterBuildIos();
if (hasArchive) iosXCArchive.deleteSync(recursive: true);
run('bash',
arguments: ['build_release.sh'],
workingDirectory: 'ios',
runOptions:
RunOptions(environment: {'XCARCHIVE_NAME': iosXCArchiveName}));
} else {
log('Archive is in ${iosXCArchive.path}');
}
}
// /////////////////////////////////////////////////////////////////////////////
void flutterBuildIos() {
if (version == null) fail('unable to read version in pubspec.yaml');
if (!iosFilesExists) fail('missing iOS files');
if (!iosIpa.existsSync() ||
iosFilesLastModified.isAfter(iosIpa.lastModified)) {
run('flutter', arguments: ['build', 'ios']);
}
}
// /////////////////////////////////////////////////////////////////////////////
@Task('Deploys a release iOS archive.')
@Depends(buildIos)
void deployIos() {
final exportOpts = 'ios/export_options.plist'.asFileSet;
if (!exportOpts.exists)
fail('Export options file is missing: ${exportOpts.files.first.path}');
if (!iosXCArchive.existsSync())
fail('Archive is missing: ${iosXCArchive.path}');
run('bash',
arguments: ['deploy_release.sh'],
workingDirectory: 'ios',
runOptions:
RunOptions(environment: {'XCARCHIVE_NAME': iosXCArchiveName}));
// TODO: add a file to track latest deployed version (hash?)
}
```
I couldn't find a way to put them in the codebase of the package, so no PR. Sorry.
Contributor guide
Assessment
This issue has not been assessed yet.