Skip to content

feat(dart_frog_cli): support for Dart workspaces #1825

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

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ COPY ./pubspec_overrides.yaml ./pubspec_overrides.yaml
{{/hasExternalDependencies}}
# Resolve app dependencies.
COPY pubspec.* ./
COPY pubspec_overrides.yaml* ./
RUN dart pub get

# Copy app source code and AOT compile it.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export 'src/copy_workspace_pubspec_lock.dart';
export 'src/create_bundle.dart';
export 'src/create_external_packages_folder.dart';
export 'src/dart_pub_get.dart';
export 'src/disable_workspace_resolution.dart';
export 'src/exit_overrides.dart';
export 'src/get_internal_path_dependencies.dart';
export 'src/get_pubspec_lock.dart';
export 'src/get_pubspec.dart';
export 'src/uses_workspace_resolution.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'dart:io';
import 'package:mason/mason.dart';
import 'package:path/path.dart' as path;
import 'package:pubspec_parse/pubspec_parse.dart';

/// Copies the pubspec.lock from the workspace root into the build directory
/// in order to ensure the production build uses the exact same versions of all
/// dependencies.
void copyWorkspacePubspecLock(
HookContext context, {
required String buildDirectory,
required String workingDirectory,
required void Function(int exitCode) exit,
}) {
final workspaceRoot = _getWorkspaceRoot(workingDirectory);
if (workspaceRoot == null) {
context.logger.err(
'Unable to determine workspace root for $workingDirectory',
);
return exit(1);
}
final pubspecLockFile = File(path.join(workspaceRoot.path, 'pubspec.lock'));
if (!pubspecLockFile.existsSync()) return;

try {
pubspecLockFile.copySync(path.join(buildDirectory, 'pubspec.lock'));
} on Exception catch (error) {
context.logger.err('$error');
return exit(1);
}
}

/// Returns the root directory of the nearest Dart workspace.
Directory? _getWorkspaceRoot(String workingDirectory) {
final file = _findNearestAncestor(
where: (path) => _getWorkspaceRootPubspecYaml(cwd: Directory(path)),
cwd: Directory(workingDirectory),
);
if (file == null || !file.existsSync()) return null;
return Directory(path.dirname(file.path));
}

/// The workspace root `pubspec.yaml` file for this project.
File? _getWorkspaceRootPubspecYaml({required Directory cwd}) {
try {
final pubspecYamlFile = File(path.join(cwd.path, 'pubspec.yaml'));
if (!pubspecYamlFile.existsSync()) return null;
final pubspec = Pubspec.parse(pubspecYamlFile.readAsStringSync());
if (pubspec.workspace?.isEmpty ?? true) return null;
return pubspecYamlFile;
} on Exception {
return null;
}
}

/// Finds nearest ancestor file
/// relative to the [cwd] that satisfies [where].
File? _findNearestAncestor({
required File? Function(String path) where,
required Directory cwd,
}) {
Directory? prev;
var dir = cwd;
while (prev?.path != dir.path) {
final file = where(dir.path);
if (file?.existsSync() ?? false) return file;
prev = dir;
dir = dir.parent;
}
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:io';
import 'package:dart_frog_prod_server_hooks/dart_frog_prod_server_hooks.dart';
import 'package:io/io.dart' as io;
import 'package:path/path.dart' as path;
import 'package:pubspec_parse/pubspec_parse.dart';

/// Signature of [io.copyPath].
typedef CopyPath = Future<void> Function(String from, String to);
Expand All @@ -13,34 +14,27 @@ Future<List<String>> createExternalPackagesFolder({
CopyPath copyPath = io.copyPath,
}) async {
final pathResolver = path.context;
final pubspecLock = await getPubspecLock(
final pubspec = await getPubspec(
projectDirectory.path,
pathContext: path.context,
);

final externalPathDependencies = pubspecLock.packages
final externalPathDependencies = pubspec.dependencies.values
.map(
(dependency) {
final pathDescription = dependency.pathDescription;
if (pathDescription == null) {
return null;
}

final isExternal = !pathResolver.isWithin('', pathDescription.path);
if (!isExternal) return null;
if (dependency is! PathDependency) return null;
if (pathResolver.isWithin('', dependency.path)) return null;

return _ExternalPathDependency(
name: dependency.name,
path: path.join(projectDirectory.path, pathDescription.path),
name: path.basenameWithoutExtension(dependency.path),
path: path.join(projectDirectory.path, dependency.path),
);
},
)
.whereType<_ExternalPathDependency>()
.toList();

if (externalPathDependencies.isEmpty) {
return [];
}
if (externalPathDependencies.isEmpty) return [];

final packagesDirectory = Directory(
pathResolver.join(
Expand Down Expand Up @@ -68,17 +62,20 @@ Future<List<String>> createExternalPackagesFolder({
buildDirectory.path,
'pubspec_overrides.yaml',
),
).writeAsString('''
).writeAsString(
'''
dependency_overrides:
${copiedExternalPathDependencies.map(
(dependency) {
final name = dependency.name;
final path =
pathResolver.relative(dependency.path, from: buildDirectory.path);
return ' $name:\n path: $path';
},
).join('\n')}
''');
(dependency) {
final name = dependency.name;
final path =
pathResolver.relative(dependency.path, from: buildDirectory.path);
return ' $name:\n path: $path';
},
).join('\n')}
''',
mode: FileMode.append,
);

return copiedExternalPathDependencies
.map((dependency) => dependency.path)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'dart:io';
import 'package:mason/mason.dart';
import 'package:path/path.dart' as path;

/// Opts out of dart workspaces until we can generate per package lockfiles.
/// https://github.com/dart-lang/pub/issues/4594
void disableWorkspaceResolution(
HookContext context, {
required String buildDirectory,
required void Function(int exitCode) exit,
}) {
try {
File(
path.join(buildDirectory, 'pubspec_overrides.yaml'),
).writeAsStringSync('\nresolution: null\n', mode: FileMode.append);
} on Exception catch (e) {
context.logger.err('$e');
exit(1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import 'dart:io' as io;

const _asyncRunZoned = runZoned;

void defaultExit(int code) => ExitOverrides.current?.exit ?? io.exit;

abstract class ExitOverrides {
static final _token = Object();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,16 @@ import 'dart:io' as io;

import 'package:dart_frog_prod_server_hooks/dart_frog_prod_server_hooks.dart';
import 'package:path/path.dart' as path;
import 'package:pubspec_parse/pubspec_parse.dart';

Future<List<String>> getInternalPathDependencies(io.Directory directory) async {
final pubspecLock = await getPubspecLock(directory.path);
final pubspec = await getPubspec(directory.path);

final internalPathDependencies = pubspecLock.packages.where(
final internalPathDependencies = pubspec.dependencies.values.where(
(dependency) {
final pathDescription = dependency.pathDescription;
if (pathDescription == null) {
return false;
}

return path.isWithin('', pathDescription.path);
return dependency is PathDependency && path.isWithin('', dependency.path);
},
);
).cast<PathDependency>();

return internalPathDependencies
.map((dependency) => dependency.pathDescription!.path)
.toList();
return internalPathDependencies.map((dependency) => dependency.path).toList();
}
20 changes: 20 additions & 0 deletions bricks/dart_frog_prod_server/hooks/lib/src/get_pubspec.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'dart:io';

import 'package:path/path.dart' as path;
import 'package:pubspec_parse/pubspec_parse.dart';

Future<Pubspec> getPubspec(
String workingDirectory, {
path.Context? pathContext,
}) async {
const pubspecYaml = 'pubspec.yaml';
final pathResolver = pathContext ?? path.context;
final pubspecFile = File(
workingDirectory.isEmpty
? pubspecYaml
: pathResolver.join(workingDirectory, pubspecYaml),
);

final content = await pubspecFile.readAsString();
return Pubspec.parse(content);
}

This file was deleted.

Loading
Loading