Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial workspace support #12

Merged
merged 14 commits into from
Jan 7, 2025
4 changes: 2 additions & 2 deletions .github/workflows/static-analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ jobs:
brew install fvm
echo "$HOME/fvm/default/bin" >> $GITHUB_PATH
echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
fvm install 3.10.0 --setup
fvm install 3.24.0 --setup
fvm install stable --setup
fvm global 3.10.0
fvm global 3.24.0
fvm flutter doctor
fvm global stable
fvm flutter doctor
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.38.0

- Adds support for workspaces
- Fixes grammar when only one project is found

## 1.37.0

- `puby gen` and `puby run` now map to `dart run` instead of `[engine] pub run`
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ Add command exclusions to prevent them from running in a project
```yaml
exclude:
- test
- pub run build_runner
- run build_runner
```

Exclusions match from the start of a command, and the entire exclusion string must be present. Here are some examples:

| Exclusion | Example command excluded |
| ---------------------- | ------------------------------------- |
| `test` | `[engine] test --coverage` |
| `pub run build_runner` | `[engine] pub run build_runner build` |
| Exclusion | Example command excluded |
| ------------------ | ----------------------------- |
| `test` | `[engine] test --coverage` |
| `run build_runner` | `dart run build_runner build` |
7 changes: 7 additions & 0 deletions bin/link.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ Future<int> linkDependencies({
print('Resolving all dependencies...');
final dependencies = <PackageId>{};
final resolutionQueue = TaskQueue();

for (final project in projects) {
// Skip workspace members (the workspace will resolve them)
if (project.type == ProjectType.workspaceMember) {
print(yellow.wrap('Skipping workspace member: ${project.path}'));
continue;
}

unawaited(
resolutionQueue.add(() async {
final resolved = project.resolveWithCommand(command);
Expand Down
106 changes: 86 additions & 20 deletions bin/projects.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,56 +13,114 @@ import 'package:yaml/yaml.dart';

import 'commands.dart';

List<Project> findProjects() {
final pubspecEntities = Directory.current
List<Project> findProjects({Directory? directory}) {
directory ??= Directory.current;
final pubspecEntities = directory
.listSync(recursive: true, followLinks: false)
.whereType<File>()
.where((e) => e.path.endsWith('pubspec.yaml'));

final fvmPaths = Directory.current
final fvmPaths = directory
.listSync(recursive: true, followLinks: false)
.whereType<File>()
.where((e) => e.path.endsWith('.fvmrc'))
.map((e) => e.parent.path)
.toSet();

final projects = <Project>[];
// Absolute project path to intermediate
final projectIntermediates = <String, ProjectIntermediate>{};
for (final pubspecEntity in pubspecEntities) {
final absolutePath = pubspecEntity.parent.path;
final path = p.relative(absolutePath);
final config = PubyConfig.fromProjectPath(path);

final Pubspec pubspec;
final YamlMap pubspecYaml;
try {
pubspec = Pubspec.parse(pubspecEntity.readAsStringSync());
final pubspecContent = pubspecEntity.readAsStringSync();
pubspec = Pubspec.parse(pubspecContent);
pubspecYaml = loadYaml(pubspecContent) as YamlMap;
} catch (e) {
print(red.wrap('Error parsing pubspec: $path'));
continue;
}

final ProjectType type;
if (pubspecYaml.containsKey('workspace')) {
type = ProjectType.workspace;
} else if (pubspecYaml['resolution'] == 'workspace') {
type = ProjectType.workspaceMember;
} else {
type = ProjectType.standalone;
}

projectIntermediates[absolutePath] = ProjectIntermediate(
absolutePath: absolutePath,
path: path,
pubspec: pubspec,
type: type,
);
}

final projects = <Project>[];
for (final ProjectIntermediate(
:absolutePath,
:path,
:pubspec,
:type,
) in projectIntermediates.values) {
final config = PubyConfig.fromProjectPath(path);

final Engine engine;
if (pubspec.dependencies['flutter'] != null) {
engine = Engine.flutter;
} else {
engine = Engine.dart;
}

final example = path.split(Platform.pathSeparator).last == 'example';
final hidden = path
.split(Platform.pathSeparator)
.any((e) => e.length > 1 && e.startsWith('.'));
final splitPath = path.split(Platform.pathSeparator);
final example = splitPath.last == 'example';
final hidden = splitPath.any((e) => e.length > 1 && e.startsWith('.'));

var dependencies = <String>{};
try {
final lockFile = File(p.join(absolutePath, 'pubspec.lock'));
final lockFileContent = lockFile.readAsStringSync();
final Set<String> dependencies;

final projectLockFile = File(p.join(absolutePath, 'pubspec.lock'));
final workspaceRefParent = p.join(absolutePath, '.dart_tool', 'pub');
final workspaceRefFile =
File(p.join(workspaceRefParent, 'workspace_ref.json'));
final pubspecDependencies = {
...pubspec.dependencies.keys,
...pubspec.devDependencies.keys,
};

Set<String> dependenciesFromLockFile(File file) {
final lockFileContent = file.readAsStringSync();
final packagesMap = loadYaml(lockFileContent)['packages'] as YamlMap;
dependencies = packagesMap.keys.cast<String>().toSet();
} catch (e) {
// This is handled elsewhere
return packagesMap.keys.cast<String>().toSet();
}

if (projectLockFile.existsSync()) {
dependencies = dependenciesFromLockFile(projectLockFile);
} else if (workspaceRefFile.existsSync()) {
final workspaceRefContent = workspaceRefFile.readAsStringSync();
final json = jsonDecode(workspaceRefContent) as Map<String, dynamic>;
final workspaceRoot = json['workspaceRoot'] as String;
final workspaceLockFile =
File(p.join(workspaceRefParent, workspaceRoot, 'pubspec.lock'));
if (workspaceLockFile.existsSync()) {
dependencies = dependenciesFromLockFile(workspaceLockFile);
} else {
dependencies = pubspecDependencies;
}
} else {
dependencies = pubspecDependencies;
}

final fvm = fvmPaths.any(absolutePath.startsWith);
final splitAbsolutePath = p.split(absolutePath);
final absoluteParentPath =
p.joinAll(splitAbsolutePath.take(splitAbsolutePath.length - 1));
final parentIntermediate = projectIntermediates[absoluteParentPath];
final parentType = parentIntermediate?.type;

final project = Project(
engine: engine,
Expand All @@ -72,6 +130,8 @@ List<Project> findProjects() {
hidden: hidden,
dependencies: dependencies,
fvm: fvm,
type: type,
parentType: parentType,
);

projects.add(project);
Expand Down Expand Up @@ -108,8 +168,7 @@ extension ProjectExtension on Project {
}

bool _defaultExclude(Command command) {
final isPubGetInExample = example &&
command.args.length >= 2 &&
final isPubGet = command.args.length >= 2 &&
command.args[0] == 'pub' &&
command.args[1] == 'get';

Expand All @@ -129,10 +188,17 @@ extension ProjectExtension on Project {
} else if (path.startsWith('build/') || path.contains('/build/')) {
message = 'Skipping project in build folder: $path';
skip = true;
} else if (isPubGetInExample) {
} else if (isPubGet &&
example &&
{ProjectType.standalone, ProjectType.workspace}.contains(parentType)) {
// Skip pub get in example projects since it happens anyways
// Do not skip if parent is a workspace member
message = 'Skipping example project: $path';
skip = true;
} else if (isPubGet && type == ProjectType.workspaceMember) {
// Skip pub get in workspace members since they resolve with the workspace
message = 'Skipping workspace member: $path';
skip = true;
} else if (dartRunPackage != null &&
!dependencies.contains(dartRunPackage)) {
// Skip dart run commands if the project doesn't have the package
Expand Down
6 changes: 5 additions & 1 deletion bin/puby.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:pub_update_checker/pub_update_checker.dart';
import 'package:puby/command.dart';
import 'package:io/ansi.dart';
import 'package:puby/project.dart';
import 'package:puby/text.dart';
import 'package:puby/time.dart';

import 'commands.dart';
Expand Down Expand Up @@ -55,7 +56,10 @@ void main(List<String> arguments) async {
exit(ExitCode.usage.code);
}

print(green.wrap('Found ${projects.length} projects\n'));
final numProjects = projects.length;
print(
green.wrap('Found $numProjects ${pluralize('project', numProjects)}\n'),
);

if (projects.any((e) => e.fvm)) {
fvmCheck();
Expand Down
46 changes: 46 additions & 0 deletions lib/project.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:puby/config.dart';
import 'package:puby/engine.dart';

/// Intermediate data used during project resolution
class ProjectIntermediate {
/// The absolute path to the project
final String absolutePath;

/// The relative path to the project
final String path;

/// The parsed pubspec
final Pubspec pubspec;

/// The type of project this is
final ProjectType type;

/// Constructor
const ProjectIntermediate({
required this.absolutePath,
required this.path,
required this.pubspec,
required this.type,
});
}

/// A dart project
class Project {
/// The engine this project uses
Expand All @@ -27,6 +51,12 @@ class Project {
/// If this project is configured with FVM
final bool fvm;

/// The type of project this is
final ProjectType type;

/// The parent project's type
final ProjectType? parentType;

/// The arguments to prefix to any commands run in this project
List<String> get prefixArgs => [
if (fvm) 'fvm',
Expand All @@ -43,6 +73,8 @@ class Project {
this.exclude = false,
required this.dependencies,
required this.fvm,
required this.type,
this.parentType,
});

/// Create a copy of this [Project] with the specified changes
Expand All @@ -56,6 +88,20 @@ class Project {
exclude: exclude ?? this.exclude,
dependencies: dependencies,
fvm: fvm ?? this.fvm,
type: type,
parentType: parentType,
);
}
}

/// Types of projects
enum ProjectType {
/// This is a standalone project
standalone,

/// This is a workspace
workspace,

/// This is a workspace member
workspaceMember;
}
5 changes: 5 additions & 0 deletions lib/text.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// Returns the plural form of a word based on the count
String pluralize(String word, int count) {
if (count == 1) return word;
return '${word}s';
}
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: puby
description: Run commands in all projects in the current directory. Handle monorepos with ease.
version: 1.37.0
version: 1.38.0
homepage: https://github.com/Rexios80/puby

environment:
Expand All @@ -18,7 +18,7 @@ dependencies:

dev_dependencies:
test: ^1.20.2
rexios_lints: ^8.2.0
rexios_lints: ^9.1.0

executables:
puby: puby
4 changes: 2 additions & 2 deletions test/config_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ void main() {
test('exclude', () async {
final result = await testCommand(
['gen'],
projects: {
entities: {
'puby_yaml_test': {
'pubspec.yaml': pubspec(
'puby_yaml_test',
devDependencies: {'build_runner: any'},
),
'puby.yaml': '''
exclude:
- pub run build_runner
- run build_runner
''',
},
},
Expand Down
2 changes: 1 addition & 1 deletion test/fvm_version_not_installed_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ void main() {
test('FVM version not installed', () async {
final result = await testCommand(
['get'],
projects: {
entities: {
'fvm_version_not_installed_test': {
'pubspec.yaml': pubspec('fvm_version_not_installed_test'),
'.fvmrc': fvmrc('1.17.0'),
Expand Down
2 changes: 1 addition & 1 deletion test/gen_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ void main() {
() async {
final result = await testCommand(
['gen'],
projects: defaultProjects(devDependencies: {'build_runner: any'}),
entities: defaultProjects(devDependencies: {'build_runner: any'}),
link: true,
);
final stdout = result.stdout;
Expand Down
Loading
Loading