Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ce6cbdb
feat(dart_frog_cli): support for Dart workspaces
felangel Aug 8, 2025
0695bdb
chore: update barrel
felangel Aug 8, 2025
c1978a3
fix: use workspace lockfile in production build
felangel Aug 9, 2025
be60632
chore: adjust comment
felangel Aug 9, 2025
47779f4
chore: regenerate bundle
felangel Aug 9, 2025
46517a3
chore: more doc fixes
felangel Aug 9, 2025
d06d4a7
test: fix broken tests and add missing unit tests
felangel Aug 12, 2025
d34ad3d
test: add more tests
felangel Aug 12, 2025
63fb96b
more test fixes
felangel Aug 12, 2025
acff678
cleanup
felangel Aug 12, 2025
b390948
fix coverage
felangel Aug 12, 2025
e3db437
chore: coverage
felangel Aug 12, 2025
359538e
revert post_gen changes
felangel Aug 12, 2025
eeed72b
ci: use updated workflow
felangel Aug 12, 2025
60031d5
revert ci changes
felangel Aug 12, 2025
08478fd
chore: regenerate bundle
felangel Aug 12, 2025
b080916
refactor: go back to lockfiles
felangel Aug 27, 2025
29ed861
cleanup
felangel Aug 27, 2025
b7adf48
chore: regen bundle
felangel Aug 27, 2025
22cc6ff
minimize diff
felangel Aug 27, 2025
3d26766
regen bundle
felangel Aug 27, 2025
a3b8d81
fix analysis warning
felangel Aug 27, 2025
ff77d3b
various fixes
felangel Aug 29, 2025
be5d71d
regen bundles
felangel Aug 29, 2025
c64b055
refactor: simplify approach
felangel Sep 4, 2025
c44d129
chore: remove unnecessary dep
felangel Sep 4, 2025
64f1c28
regen bundle
felangel Sep 4, 2025
776f154
refactor: improve pubspec_overrides handling
felangel Sep 4, 2025
8ada490
more edge case fixes
felangel Sep 4, 2025
b3ccf5b
cleanup
felangel Sep 4, 2025
fa0834a
Merge branch 'main' into feat/workspaces
felangel Sep 4, 2025
6c110f4
chore: use `VoidCallback`
felangel Sep 5, 2025
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,8 @@
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/uses_workspace_resolution.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ Future<List<String>> createExternalPackagesFolder({
.map(
(dependency) {
final pathDescription = dependency.pathDescription;
if (pathDescription == null) {
return null;
}
if (pathDescription == null) return null;

final isExternal = !pathResolver.isWithin('', pathDescription.path);
if (!isExternal) return null;
Expand All @@ -38,9 +36,7 @@ Future<List<String>> createExternalPackagesFolder({
.whereType<_ExternalPathDependency>()
.toList();

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

final packagesDirectory = Directory(
pathResolver.join(
Expand All @@ -51,34 +47,38 @@ Future<List<String>> createExternalPackagesFolder({

final copiedExternalPathDependencies = await Future.wait(
externalPathDependencies.map(
(externalPathDependency) => externalPathDependency.copyTo(
copyPath: copyPath,
targetDirectory: Directory(
pathResolver.join(
packagesDirectory.path,
externalPathDependency.name,
(externalPathDependency) async {
final copy = await externalPathDependency.copyTo(
copyPath: copyPath,
targetDirectory: Directory(
pathResolver.join(
packagesDirectory.path,
externalPathDependency.name,
),
),
),
),
);
overrideResolutionInPubspecOverrides(copy.path);
return copy;
},
),
);

await File(
pathResolver.join(
buildDirectory.path,
'pubspec_overrides.yaml',
),
).writeAsString('''
File(
pathResolver.join(buildDirectory.path, 'pubspec_overrides.yaml'),
).writeAsStringSync(
'''
resolution: null
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')}
''',
);

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

/// A void callback function (e.g. `void Function()`).
typedef VoidCallback = void Function();

/// Opts out of dart workspaces until we can generate per package lockfiles.
/// https://github.com/dart-lang/pub/issues/4594
VoidCallback disableWorkspaceResolution(
HookContext context, {
required String projectDirectory,
required void Function(int exitCode) exit,
}) {
try {
return overrideResolutionInPubspecOverrides(projectDirectory);
} on Exception catch (e) {
context.logger.err('$e');
exit(1);
return () {}; // no-op
}
}

VoidCallback overrideResolutionInPubspecOverrides(String projectDirectory) {
final pubspecOverridesFile = File(
path.join(projectDirectory, 'pubspec_overrides.yaml'),
);

if (!pubspecOverridesFile.existsSync()) {
pubspecOverridesFile.writeAsStringSync('resolution: null');
return pubspecOverridesFile.deleteSync;
}

final contents = pubspecOverridesFile.readAsStringSync();
final pubspecOverrides = loadYaml(contents) as YamlMap?;

if (pubspecOverrides == null) {
pubspecOverridesFile.writeAsStringSync('resolution: null');
return () => pubspecOverridesFile.writeAsStringSync(contents);
}

if (pubspecOverrides['resolution'] == 'null') return () {}; // no-op

final editor = YamlEditor(contents)..update(['resolution'], null);
pubspecOverridesFile.writeAsStringSync(editor.toString());

return () => pubspecOverridesFile.writeAsStringSync(contents);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'dart:io';
import 'package:mason/mason.dart';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';

/// Determines whether the project in the provided [workingDirectory]
/// is configured to use `resolution: workspace`.
bool usesWorkspaceResolution(
HookContext context, {
required String workingDirectory,
required void Function(int exitCode) exit,
}) {
final pubspecFile = File(path.join(workingDirectory, 'pubspec.yaml'));
if (!pubspecFile.existsSync()) return false;

final YamlMap pubspec;
try {
final yaml = loadYaml(pubspecFile.readAsStringSync());
if (yaml is! YamlMap) {
throw Exception('Unable to parse ${pubspecFile.path}');
}
pubspec = yaml;
} on Exception catch (e) {
context.logger.err('$e');
exit(1);
return false;
}

return pubspec['resolution'] == 'workspace';
}
27 changes: 21 additions & 6 deletions bricks/dart_frog_prod_server/hooks/pre_gen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ Future<void> preGen(
Future<void> Function(String from, String to) copyPath = io_expanded.copyPath,
}) async {
final projectDirectory = directory ?? io.Directory.current;
final usesWorkspaces = usesWorkspaceResolution(
context,
workingDirectory: projectDirectory.path,
exit: exit,
);

VoidCallback? restoreWorkspaceResolution;

if (usesWorkspaces) {
// Disable workspace resolution until we can generate per-package lockfiles.
// https://github.com/dart-lang/pub/issues/4594
restoreWorkspaceResolution = disableWorkspaceResolution(
context,
projectDirectory: projectDirectory.path,
exit: exit,
);
}

// We need to make sure that the pubspec.lock file is up to date
await dartPubGet(
Expand All @@ -43,6 +60,8 @@ Future<void> preGen(
exit: exit,
);

restoreWorkspaceResolution?.call();

final RouteConfiguration configuration;
try {
configuration = buildConfiguration(projectDirectory);
Expand All @@ -62,9 +81,7 @@ Future<void> preGen(
'''Route conflict detected. ${lightCyan.wrap(originalFilePath)} and ${lightCyan.wrap(conflictingFilePath)} both resolve to ${lightCyan.wrap(conflictingEndpoint)}.''',
);
},
onViolationEnd: () {
exit(1);
},
onViolationEnd: () => exit(1),
);

reportRogueRoutes(
Expand All @@ -74,9 +91,7 @@ Future<void> preGen(
'''Rogue route detected.${defaultForeground.wrap(' ')}Rename ${lightCyan.wrap(filePath)} to ${lightCyan.wrap(idealPath)}.''',
);
},
onViolationEnd: () {
exit(1);
},
onViolationEnd: () => exit(1),
);

final customDockerFile = io.File(
Expand Down
1 change: 1 addition & 0 deletions bricks/dart_frog_prod_server/hooks/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies:
mason: ^0.1.0
path: ^1.8.1
yaml: ^3.1.2
yaml_edit: ^2.2.2

dev_dependencies:
mocktail: ^1.0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ void main() {
expect(
ExitOverrides.runZoned(
() => post_gen.run(_FakeHookContext(logger: logger)),
exit: (_) {},
),
completes,
);
Expand Down
118 changes: 77 additions & 41 deletions bricks/dart_frog_prod_server/hooks/test/pre_gen_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:test/test.dart';

import '../pre_gen.dart' as pre_gen;
import 'pubspec_locks.dart';
import 'pubspecs.dart';

class _FakeHookContext extends Fake implements HookContext {
_FakeHookContext({Logger? logger}) : _logger = logger ?? _MockLogger();
Expand Down Expand Up @@ -161,49 +162,84 @@ void main() {
expect(exitCalls, equals([1]));
});

test(
'works with external dependencies',
() async {
const configuration = RouteConfiguration(
middleware: [],
directories: [],
routes: [],
rogueRoutes: [],
endpoints: {},
);
test('works with workspaces', () async {
const configuration = RouteConfiguration(
middleware: [],
directories: [],
routes: [],
rogueRoutes: [],
endpoints: {},
);

final directory = Directory.systemTemp.createTempSync();
File(path.join(directory.path, 'pubspec.yaml')).writeAsStringSync(
'''
name: example
version: 0.1.0
environment:
sdk: ^2.17.0
dependencies:
mason: any
foo:
path: ../../foo
dev_dependencies:
test: any
''',
);
File(path.join(directory.path, 'pubspec.lock')).writeAsStringSync(
fooPath,
);
final exitCalls = <int>[];
await pre_gen.preGen(
context,
buildConfiguration: (_) => configuration,
exit: exitCalls.add,
directory: directory,
runProcess: successRunProcess,
copyPath: (_, __) async {},
);
final directory = Directory.systemTemp.createTempSync();
File(
path.join(directory.path, 'pubspec.yaml'),
).writeAsStringSync(workspaceRoot);
final server = Directory(
path.join(directory.path, 'server'),
)..createSync();
File(
path.join(server.path, 'pubspec.yaml'),
).writeAsStringSync(workspaceChild);
File(
path.join(server.path, 'pubspec.lock'),
).writeAsStringSync('''
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev"
source: hosted
version: "85.0.0"
''');
final exitCalls = <int>[];
await pre_gen.preGen(
context,
buildConfiguration: (_) => configuration,
exit: exitCalls.add,
directory: server,
runProcess: successRunProcess,
copyPath: (_, __) async {},
);

expect(exitCalls, isEmpty);
directory.delete(recursive: true).ignore();
},
);
expect(exitCalls, isEmpty);
directory.delete(recursive: true).ignore();
});

test('works with external dependencies', () async {
const configuration = RouteConfiguration(
middleware: [],
directories: [],
routes: [],
rogueRoutes: [],
endpoints: {},
);

final directory = Directory.systemTemp.createTempSync();
File(
path.join(directory.path, 'pubspec.lock'),
).writeAsStringSync(fooPath);
final exitCalls = <int>[];
await pre_gen.preGen(
context,
buildConfiguration: (_) => configuration,
exit: exitCalls.add,
directory: directory,
runProcess: successRunProcess,
copyPath: (from, to) async {
File(
path.join(to, 'pubspec_overrides.yaml'),
).createSync(recursive: true);
},
);

expect(exitCalls, isEmpty);
directory.delete(recursive: true).ignore();
});

test('retains invokeCustomEntrypoint (true)', () async {
const configuration = RouteConfiguration(
Expand Down
Loading
Loading