google / google/grinder.dart

Using extensions

Open
#355 1 comment 0 reactions 0 assignees View on GitHub
enhancement
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.