diff --git a/frontend_server_client/analysis_options.yaml b/frontend_server_client/analysis_options.yaml index 1167fdd9f..aad260e08 100644 --- a/frontend_server_client/analysis_options.yaml +++ b/frontend_server_client/analysis_options.yaml @@ -1 +1 @@ -include: package:analysis_config/analysis_options.yaml +include: package:dart_flutter_team_lints/analysis_options.yaml \ No newline at end of file diff --git a/frontend_server_client/example/app/main.dart b/frontend_server_client/example/app/main.dart index 4decd86ec..6ee01dd23 100644 --- a/frontend_server_client/example/app/main.dart +++ b/frontend_server_client/example/app/main.dart @@ -8,7 +8,7 @@ Future main() async { print(message); while (!message.contains('goodbye')) { print('waiting for hot reload to change message'); - await Future.delayed(const Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); } print(message); } diff --git a/frontend_server_client/example/vm_client.dart b/frontend_server_client/example/vm_client.dart index 9499a7ce5..2974f7b86 100644 --- a/frontend_server_client/example/vm_client.dart +++ b/frontend_server_client/example/vm_client.dart @@ -12,18 +12,26 @@ import 'package:vm_service/vm_service.dart'; import 'package:vm_service/vm_service_io.dart'; void main(List args) async { + // Change to package root so relative paths work in CI + final scriptDir = p.dirname(p.fromUri(Platform.script)); + final packageRoot = p.dirname(scriptDir); + Directory.current = packageRoot; + try { watch.start(); if (args.isNotEmpty) { throw ArgumentError('No command line args are supported'); } + final packagesPath = findNearestPackageConfigPath(); final client = await FrontendServerClient.start( 'org-dartlang-root:///$app', outputDill, p.join(sdkDir, 'lib', '_internal', 'vm_platform_strong.dill'), + packagesJson: packagesPath ?? '.dart_tool/package_config.json', target: 'vm', - fileSystemRoots: [p.url.current], + // Use an absolute filesystem root so org-dartlang-root:/// URIs resolve reliably in CI + fileSystemRoots: [Directory.current.path], fileSystemScheme: 'org-dartlang-root', verbose: true, ); @@ -38,7 +46,7 @@ void main(List args) async { '--enable-vm-service=0', result.dillOutput!, ]); - final sawHelloWorld = Completer(); + final sawHelloWorld = Completer(); appProcess.stdout .transform(utf8.decoder) .transform(const LineSplitter()) @@ -52,7 +60,9 @@ void main(List args) async { )) { final observatoryUri = '${line.split(' ').last.replaceFirst('http', 'ws')}ws'; - vmServiceCompleter.complete(vmServiceConnectUri(observatoryUri)); + if (!vmServiceCompleter.isCompleted) { + vmServiceCompleter.complete(vmServiceConnectUri(observatoryUri)); + } } }); appProcess.stderr diff --git a/frontend_server_client/example/web_client.dart b/frontend_server_client/example/web_client.dart index f5b88fc6b..8e9c8fe44 100644 --- a/frontend_server_client/example/web_client.dart +++ b/frontend_server_client/example/web_client.dart @@ -14,6 +14,11 @@ import 'package:shelf_packages_handler/shelf_packages_handler.dart'; import 'package:shelf_static/shelf_static.dart'; void main(List args) async { + // Change to package root so relative paths work in CI + final scriptDir = p.dirname(p.fromUri(Platform.script)); + final packageRoot = p.dirname(scriptDir); + Directory.current = packageRoot; + try { watch.start(); if (args.isNotEmpty) { @@ -41,12 +46,15 @@ void main(List args) async { } _print('starting frontend server'); + final packagesPath = findNearestPackageConfigPath(); final client = await DartDevcFrontendServerClient.start( 'org-dartlang-root:///$app', outputDill, - fileSystemRoots: [p.current], + // Use an absolute filesystem root so org-dartlang-root:/// URIs resolve reliably in CI + fileSystemRoots: [Directory.current.path], fileSystemScheme: 'org-dartlang-root', platformKernel: p.toUri(sdkKernelPath).toString(), + packagesJson: packagesPath ?? '.dart_tool/package_config.json', verbose: true, ); @@ -58,7 +66,7 @@ void main(List args) async { _print('starting shelf server'); final cascade = Cascade() .add(_clientHandler(client)) - .add(createStaticHandler(p.current)) + .add(createStaticHandler(Directory.current.path)) .add(createFileHandler(dartSdkJs, url: 'example/app/dart_sdk.js')) .add( createFileHandler( diff --git a/frontend_server_client/lib/frontend_server_client.dart b/frontend_server_client/lib/frontend_server_client.dart index e3fabab2f..06fba641b 100644 --- a/frontend_server_client/lib/frontend_server_client.dart +++ b/frontend_server_client/lib/frontend_server_client.dart @@ -6,3 +6,4 @@ export 'src/dartdevc_frontend_server_client.dart' show DartDevcFrontendServerClient; export 'src/frontend_server_client.dart' show CompileResult, FrontendServerClient; +export 'src/package_config_utils.dart' show findNearestPackageConfigPath; diff --git a/frontend_server_client/lib/src/dartdevc_bootstrap_amd.dart b/frontend_server_client/lib/src/dartdevc_bootstrap_amd.dart index 74c6c228c..50ed7d9f0 100644 --- a/frontend_server_client/lib/src/dartdevc_bootstrap_amd.dart +++ b/frontend_server_client/lib/src/dartdevc_bootstrap_amd.dart @@ -41,12 +41,12 @@ document.head.appendChild(requireEl); /// method. /// /// RE: Object.keys usage in app.main: -/// This attaches the main entrypoint and hot reload functionality to the window. -/// The app module will have a single property which contains the actual application -/// code. The property name is based off of the entrypoint that is generated, for example -/// the file `foo/bar/baz.dart` will generate a property named approximately -/// `foo__bar__baz`. Rather than attempt to guess, we assume the first property of -/// this object is the module. +/// This attaches the main entrypoint and hot reload functionality to the +/// window. The app module will have a single property which contains the +/// actual application code. The property name is based off of the entrypoint +/// that is generated, for example the file `foo/bar/baz.dart` will generate +/// a property named approximately `foo__bar__baz`. Rather than attempt to +/// guess, we assume the first property of this object is the module. String generateAmdMainModule({required String entrypoint}) { return '''/* ENTRYPOINT_EXTENTION_MARKER */ // Create the main module loaded below. diff --git a/frontend_server_client/lib/src/frontend_server_client.dart b/frontend_server_client/lib/src/frontend_server_client.dart index a2dbd7247..92134ccb5 100644 --- a/frontend_server_client/lib/src/frontend_server_client.dart +++ b/frontend_server_client/lib/src/frontend_server_client.dart @@ -227,7 +227,8 @@ class FrontendServerClient { removedSources.add(diffUri); } else { throw StateError( - 'unrecognized diff line, should start with a + or - but got: $line', + 'unrecognized diff line, should start with a + or - ' + 'but got: $line', ); } continue; diff --git a/frontend_server_client/lib/src/package_config_utils.dart b/frontend_server_client/lib/src/package_config_utils.dart new file mode 100644 index 000000000..b3eaaa4e8 --- /dev/null +++ b/frontend_server_client/lib/src/package_config_utils.dart @@ -0,0 +1,46 @@ +// Utility functions to locate a package_config.json for pub/workspace setups. + +import 'dart:io'; + +import 'package:package_config/package_config.dart'; +import 'package:path/path.dart' as p; + +/// Walks up from [start] (or the current directory if omitted) to find the +/// nearest `.dart_tool/package_config.json`. +/// +/// Returns the absolute file path, or `null` if none is found. +String? findNearestPackageConfigPath([Directory? start]) { + var dir = (start ?? Directory.current).absolute; + while (true) { + final file = File(p.join(dir.path, '.dart_tool', 'package_config.json')); + if (file.existsSync()) return file.path; + final parent = dir.parent; + if (parent.path == dir.path) return null; + dir = parent; + } +} + +/// Returns an absolute path under the given [packageName]'s root directory, +/// resolving using the nearest workspace `.dart_tool/package_config.json`. +/// +/// This is robust for pub workspace monorepos where the nearest package +/// config lives at the repo root and contains individual entries for each +/// package with its own root. +Future pathFromNearestPackageConfig( + String relativePath, { + String packageName = 'frontend_server_client', +}) async { + final configPath = findNearestPackageConfigPath(); + if (configPath == null) { + throw StateError('Could not locate .dart_tool/package_config.json'); + } + final config = await loadPackageConfigUri(Uri.file(configPath)); + final pkg = config.packages.firstWhere( + (p0) => p0.name == packageName, + orElse: () => throw StateError( + 'Package $packageName not found in package config at $configPath', + ), + ); + final packageRootDir = p.fromUri(pkg.root); + return p.normalize(p.join(packageRootDir, relativePath)); +} diff --git a/frontend_server_client/pubspec.yaml b/frontend_server_client/pubspec.yaml index 08a816764..7dcf65851 100644 --- a/frontend_server_client/pubspec.yaml +++ b/frontend_server_client/pubspec.yaml @@ -9,12 +9,11 @@ environment: dependencies: async: ^2.5.0 + package_config: ^2.1.0 path: ^1.8.0 dev_dependencies: - analysis_config: - path: ../_analysis_config - package_config: ^2.0.0 + dart_flutter_team_lints: ^3.5.2 shelf: ^1.0.0 shelf_packages_handler: ^3.0.0 shelf_static: ^1.1.0 diff --git a/frontend_server_client/test/example/vm_client_test.dart b/frontend_server_client/test/example/vm_client_test.dart index 110553391..730c66881 100644 --- a/frontend_server_client/test/example/vm_client_test.dart +++ b/frontend_server_client/test/example/vm_client_test.dart @@ -8,14 +8,19 @@ library; import 'dart:io'; +import 'package:frontend_server_client/src/package_config_utils.dart'; import 'package:test/test.dart'; import 'package:test_process/test_process.dart'; void main() { test('vm client example can build and rebuild an app', () async { + // Resolve the example script path based on the package root. + final exampleFilePath = await pathFromNearestPackageConfig( + 'example/vm_client.dart', + ); final process = await TestProcess.start(Platform.resolvedExecutable, [ 'run', - 'example/vm_client.dart', + exampleFilePath, ]); await expectLater( process.stdout, diff --git a/frontend_server_client/test/example/web_client_test.dart b/frontend_server_client/test/example/web_client_test.dart index 3d4589d7f..bba6d71d8 100644 --- a/frontend_server_client/test/example/web_client_test.dart +++ b/frontend_server_client/test/example/web_client_test.dart @@ -8,14 +8,19 @@ library; import 'dart:io'; +import 'package:frontend_server_client/src/package_config_utils.dart'; import 'package:test/test.dart'; import 'package:test_process/test_process.dart'; void main() { test('web client example can build and rebuild an app', () async { + // Resolve the example script path based on the package root. + final exampleFilePath = await pathFromNearestPackageConfig( + 'example/web_client.dart', + ); final process = await TestProcess.start(Platform.resolvedExecutable, [ 'run', - 'example/web_client.dart', + exampleFilePath, ]); await expectLater( process.stdout, diff --git a/frontend_server_client/test/frontend_server_client_test.dart b/frontend_server_client/test/frontend_server_client_test.dart index 027d77e98..b85169534 100644 --- a/frontend_server_client/test/frontend_server_client_test.dart +++ b/frontend_server_client/test/frontend_server_client_test.dart @@ -18,9 +18,10 @@ import 'package:vm_service/vm_service.dart'; import 'package:vm_service/vm_service_io.dart'; void main() async { - late FrontendServerClient client; + FrontendServerClient? client; late PackageConfig packageConfig; late String packageRoot; + late String packagesJsonPath; setUp(() async { await d.dir('a', [ @@ -56,10 +57,13 @@ String get message => p.join('hello', 'world'); 'get', ], workingDirectory: packageRoot); packageConfig = (await findPackageConfig(Directory(packageRoot)))!; + packagesJsonPath = + findNearestPackageConfigPath(Directory(packageRoot)) ?? + p.join(packageRoot, '.dart_tool', 'package_config.json'); }); tearDown(() async { - await client.shutdown(); + await client?.shutdown(); }); test('can compile, recompile, and hot reload a vm app', () async { @@ -68,9 +72,10 @@ String get message => p.join('hello', 'world'); entrypoint, p.join(packageRoot, 'out.dill'), vmPlatformDill, + packagesJson: packagesJsonPath, ); - var result = await client.compile(); - client.accept(); + var result = await client!.compile(); + client!.accept(); expect(result.compilerOutputLines, isEmpty); expect(result.errorCount, 0); expect( @@ -93,11 +98,13 @@ String get message => p.join('hello', 'world'); final stdoutLines = StreamQueue( process.stdout.transform(utf8.decoder).transform(const LineSplitter()), ); + addTearDown(stdoutLines.cancel); final observatoryLine = await stdoutLines.next; final observatoryUri = '${observatoryLine.split(' ').last.replaceFirst('http', 'ws')}ws'; final vmService = await vmServiceConnectUri(observatoryUri); + addTearDown(vmService.dispose); final isolate = await waitForIsolatesAndResume(vmService); await expectLater(stdoutLines, emitsThrough(p.join('hello', 'world'))); @@ -107,9 +114,9 @@ String get message => p.join('hello', 'world'); final newContent = originalContent.replaceFirst('hello', 'goodbye'); await appFile.writeAsString(newContent); - result = await client.compile([File(entrypoint).uri]); + result = await client!.compile([File(entrypoint).uri]); - client.accept(); + client!.accept(); expect(result.newSources, isEmpty); expect(result.removedSources, isEmpty); expect(result.compilerOutputLines, isEmpty); @@ -135,10 +142,11 @@ String get message => p.join('hello', 'world'); entrypoint, p.join(packageRoot, 'out.dill'), vmPlatformDill, + packagesJson: packagesJsonPath, ); - var result = await client.compile(); + var result = await client!.compile(); - client.accept(); + client!.accept(); expect(result.errorCount, 2); expect( result.compilerOutputLines, @@ -165,11 +173,13 @@ String get message => p.join('hello', 'world'); final stdoutLines = StreamQueue( process.stdout.transform(utf8.decoder).transform(const LineSplitter()), ); + addTearDown(stdoutLines.cancel); final observatoryLine = await stdoutLines.next; final observatoryUri = '${observatoryLine.split(' ').last.replaceFirst('http', 'ws')}ws'; final vmService = await vmServiceConnectUri(observatoryUri); + addTearDown(vmService.dispose); final isolate = await waitForIsolatesAndResume(vmService); // The program actually runs regardless of the errors, as they don't affect @@ -179,8 +189,8 @@ String get message => p.join('hello', 'world'); await entrypointFile.writeAsString( originalContent.replaceFirst('hello', 'goodbye'), ); - result = await client.compile([entrypointFile.uri]); - client.accept(); + result = await client!.compile([entrypointFile.uri]); + client!.accept(); expect(result.errorCount, 0); expect(result.compilerOutputLines, isEmpty); expect(result.newSources, isEmpty); @@ -204,9 +214,10 @@ String get message => p.join('hello', 'world'); platformKernel: p .toUri(p.join(sdkDir, 'lib', '_internal', 'ddc_platform.dill')) .toString(), + packagesJson: packagesJsonPath, ); - var result = await client.compile(); - client.accept(); + var result = await client!.compile(); + client!.accept(); expect(result.compilerOutputLines, isEmpty); expect(result.errorCount, 0); @@ -235,8 +246,8 @@ String get message => p.join('hello', 'world'); final newContent = originalContent.replaceFirst('hello', 'goodbye'); await appFile.writeAsString(newContent); - result = await client.compile([entrypointUri]); - client.accept(); + result = await client!.compile([entrypointUri]); + client!.accept(); expect(result.newSources, isEmpty); expect(result.removedSources, isEmpty); expect(result.compilerOutputLines, isEmpty); @@ -269,9 +280,10 @@ void main() { p.join(packageRoot, 'out.dill'), vmPlatformDill, enabledExperiments: ['non-nullable'], + packagesJson: packagesJsonPath, ); - final result = await client.compile(); - client.accept(); + final result = await client!.compile(); + client!.accept(); expect(result.errorCount, 1); expect(result.compilerOutputLines, contains(contains('int x;'))); }); @@ -292,9 +304,10 @@ void main() { entrypoint, p.join(packageRoot, 'out with spaces.dill'), vmPlatformDill, + packagesJson: packagesJsonPath, ); - var result = await client.compile(); - client.accept(); + var result = await client!.compile(); + client!.accept(); expect(result.compilerOutputLines, isEmpty); expect(result.errorCount, 0); expect(result.newSources, containsAll([File(entrypoint).uri])); @@ -312,7 +325,7 @@ void main() { final originalContent = await appFile.readAsString(); final newContent = originalContent.replaceFirst('hello', 'goodbye'); await appFile.writeAsString(newContent); - result = await client.compile([appFile.uri]); + result = await client!.compile([appFile.uri]); expect(result.compilerOutputLines, isEmpty); expect(result.errorCount, 0); expect(result.newSources, isEmpty); @@ -330,14 +343,14 @@ Future waitForIsolatesAndResume(VmService vmService) async { var vm = await vmService.getVM(); var isolates = vm.isolates; while (isolates == null || isolates.isEmpty) { - await Future.delayed(const Duration(milliseconds: 100)); + await Future.delayed(const Duration(milliseconds: 100)); vm = await vmService.getVM(); isolates = vm.isolates; } final isolateRef = isolates.first; var isolate = await vmService.getIsolate(isolateRef.id!); while (isolate.pauseEvent?.kind != EventKind.kPauseStart) { - await Future.delayed(const Duration(milliseconds: 100)); + await Future.delayed(const Duration(milliseconds: 100)); isolate = await vmService.getIsolate(isolateRef.id!); } await vmService.resume(isolate.id!);