| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:io'; | |
| import 'package:analyzer/dart/analysis/utilities.dart'; | |
| import 'package:analyzer/dart/ast/ast.dart'; | |
| import 'package:package_config/package_config.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'utils.dart'; | |
| const String _defaultVersionConstraint = 'any'; | |
| const String _evalWorkspaceName = 'dart_eval_workspace'; | |
| final RegExp _sdkVersionRegExp = RegExp(r'^(\d+\.\d+\.\d+)'); | |
| /// Manages resolution of package dependencies for `dart run -e` evaluation. | |
| abstract final class EvalPackageResolver { | |
| /// Extracts non-core package names imported in the code snippet using the analyzer AST parser. | |
| static Set<String> extractPackageImports(String code) { | |
| final packages = <String>{}; | |
| final unit = parseString( | |
| content: code, | |
| throwIfDiagnostics: false, | |
| ).unit; | |
| for (final directive in unit.directives) { | |
| if (directive is ImportDirective) { | |
| final uriString = directive.uri.stringValue; | |
| if (uriString != null) { | |
| final pkgName = Uri.tryParse(uriString)?.packageName; | |
| if (pkgName != null) { | |
| packages.add(pkgName); | |
| } | |
| } | |
| } | |
| } | |
| return packages; | |
| } | |
| /// Merges package dependencies from auto-imports and `-P` / `--package-constraint` CLI flags. | |
| static Map<String, String> collectDependencies( | |
| String code, { | |
| List<String>? packages, | |
| }) { | |
| final deps = <String, String>{}; | |
| // 1. Auto-detected package imports from code snippet | |
| for (final pkg in extractPackageImports(code)) { | |
| deps[pkg] = _defaultVersionConstraint; | |
| } | |
| // 2. CLI -P / --package-constraint flags (e.g. ["http:^1.2.0", "path"]) | |
| if (packages != null) { | |
| for (final spec in packages) { | |
| if (spec.trim().isEmpty) continue; | |
| final parts = spec.split(':'); | |
| final pkg = parts[0].trim(); | |
| final version = parts.length > 1 | |
| ? parts.sublist(1).join(':').trim() | |
| : _defaultVersionConstraint; | |
| if (pkg.isNotEmpty) { | |
| deps[pkg] = version.isEmpty ? _defaultVersionConstraint : version; | |
| } | |
| } | |
| } | |
| return deps; | |
| } | |
| /// Resolves `.dart_tool/package_config.json` for the required dependencies. | |
| /// | |
| /// Priority: | |
| /// 1. Local project `.dart_tool/package_config.json` if present and contains all dependencies (only when no explicit package constraints are specified). | |
| /// 2. Ephemeral `dart pub get` execution into a temporary directory. | |
| static Future<String?> resolvePackageConfig( | |
| String code, { | |
| List<String>? packageConstraints, | |
| String? localPackageConfig, | |
| bool offline = false, | |
| }) async { | |
| final deps = collectDependencies(code, packages: packageConstraints); | |
| if (deps.isEmpty) { | |
| return localPackageConfig; | |
| } | |
| // 1. Check local package config | |
| if (localPackageConfig != null) { | |
| final configFile = File(localPackageConfig); | |
| if (!configFile.existsSync()) { | |
| throw StateError( | |
| 'Specified package configuration file does not exist: "$localPackageConfig"', | |
| ); | |
| } | |
| final PackageConfig config; | |
| try { | |
| config = await loadPackageConfig(configFile); | |
| } catch (e) { | |
| throw StateError( | |
| 'Failed to parse package configuration file "$localPackageConfig": $e', | |
| ); | |
| } | |
| // Only reuse local config when no explicit package constraints were passed. | |
| if (packageConstraints == null || packageConstraints.isEmpty) { | |
| final localPkgNames = {for (final p in config.packages) p.name}; | |
| if (deps.keys.every(localPkgNames.contains)) { | |
| return localPackageConfig; | |
| } | |
| } | |
| } | |
| // 2. Ephemeral pub get in a temporary directory (no persistent caching) | |
| final tempDir = Directory.systemTemp.createTempSync('dart_eval_'); | |
| final match = _sdkVersionRegExp.firstMatch(Platform.version); | |
| if (match == null) { | |
| throw StateError( | |
| 'Could not parse SDK version from Platform.version: "${Platform.version}"', | |
| ); | |
| } | |
| final sdkVersion = match.group(1)!; | |
| final sortedDeps = deps.entries.toList() | |
| ..sort((a, b) => a.key.compareTo(b.key)); | |
| // Append '-0' (`preReleaseSuffix`) so the SemVer caret constraint matches | |
| // pre-release SDKs (e.g., 3.14.0-75.0.dev or 3.14.0-edge), which are | |
| // otherwise considered strictly lower than the base 'X.Y.Z' version by pub. | |
| const preReleaseSuffix = '-0'; | |
| final pubspecBuffer = StringBuffer(''' | |
| name: $_evalWorkspaceName | |
| environment: | |
| sdk: "^$sdkVersion$preReleaseSuffix" | |
| dependencies: | |
| '''); | |
| for (final MapEntry(:key, :value) in sortedDeps) { | |
| if (value.startsWith('{') || | |
| value.startsWith('[') || | |
| value.contains('\n')) { | |
| pubspecBuffer.writeln(' $key: $value'); | |
| } else { | |
| pubspecBuffer.writeln(' $key: "$value"'); | |
| } | |
| } | |
| File( | |
| path.join(tempDir.path, 'pubspec.yaml'), | |
| ).writeAsStringSync(pubspecBuffer.toString()); | |
| final dartBin = Platform.resolvedExecutable; | |
| final result = await Process.run( | |
| dartBin, | |
| [ | |
| 'pub', | |
| 'get', | |
| if (offline) '--offline', | |
| ], | |
| workingDirectory: tempDir.path, | |
| ); | |
| final generatedConfig = File( | |
| path.join(tempDir.path, '.dart_tool', 'package_config.json'), | |
| ); | |
| if (result.exitCode != 0 || !generatedConfig.existsSync()) { | |
| final stderrOut = result.stderr.toString().trim(); | |
| final stdoutOut = result.stdout.toString().trim(); | |
| throw StateError( | |
| 'Failed to resolve package dependencies for evaluation snippet:\n\n' | |
| 'stdout:\n' | |
| '$stdoutOut\n\n' | |
| 'stderr:\n' | |
| '$stderrOut', | |
| ); | |
| } | |
| return generatedConfig.path; | |
| } | |
| } |