diff --git a/lib/src/core/dependencies/dependency_wirer.dart b/lib/src/core/dependencies/dependency_wirer.dart index c47e9ec3..2480125f 100644 --- a/lib/src/core/dependencies/dependency_wirer.dart +++ b/lib/src/core/dependencies/dependency_wirer.dart @@ -95,22 +95,39 @@ class DependencyWirer { static const zuraffaGitUrl = 'https://github.com/arrrrny/zuraffa'; /// Git source for the zorphy monorepo (contains `zorphy` and - /// `zorphy_annotation` sub-packages). - static const zorphyGitUrl = 'https://github.com/arrrrny/zorphy'; + /// `zorphy_annotation` sub-packages). Must match the exact URL string used + /// by the zuraffa root pubspec (https://github.com/arrrrny/zorphy.git): + /// pub treats git sources as identical only when the URL string matches + /// verbatim, and a `.git`-vs-bare mismatch breaks version solving when + /// `zuraffa_flutter` pulls `zuraffa` transitively. + static const zorphyGitUrl = 'https://github.com/arrrrny/zorphy.git'; /// Default git ref — tracks the development branch which is where active /// v6 work lands before merging to master. static const defaultGitRef = 'development'; - /// The analyzer version zuraffa pins (see root pubspec.yaml). Overriding - /// analyzer in downstream apps prevents version-conflict failures when - /// `dart pub get` resolves the transitive graph. + /// The analyzer version zuraffa pins in pure-Dart packages (see root + /// pubspec.yaml). Overriding analyzer in downstream apps prevents + /// version-conflict failures when `dart pub get` resolves the transitive + /// graph. static const analyzerOverrideVersion = '14.1.0'; + /// Flutter apps cannot use the [analyzerOverrideVersion] override: the + /// Flutter SDK pins `meta 1.18.0` while analyzer >=13.1.0 requires + /// `meta ^1.18.3`, so version solving always fails. These match + /// `zuraffa_flutter`'s own `dependency_overrides` + /// (zuraffa_flutter/pubspec.yaml): analyzer is capped at ^13.1.0 and meta + /// is overlaid with ^1.19.0 so the graph resolves under the Flutter SDK. + static const flutterAnalyzerOverrideVersion = '^13.1.0'; + static const flutterMetaOverrideVersion = '^1.19.0'; + /// Returns the standard zuraffa dependency set for the given project type. /// /// [isFlutter] selects `zuraffa_flutter` + `flutter_lints` (Flutter apps) - /// versus `zuraffa` (pure Dart packages). All other entries are shared. + /// versus `zuraffa` (pure Dart packages), and picks the override set: + /// Flutter apps get the analyzer + meta overrides that match + /// `zuraffa_flutter`'s own pubspec, pure Dart packages pin analyzer + /// directly. All other entries are shared. static List standardSet({required bool isFlutter}) { return [ DependencySpec( @@ -131,11 +148,23 @@ class DependencyWirer { const DependencySpec(name: 'mocktail', kind: DependencyKind.dev), if (isFlutter) const DependencySpec(name: 'flutter_lints', kind: DependencyKind.dev), - const DependencySpec( - name: 'analyzer', - kind: DependencyKind.override, - version: analyzerOverrideVersion, - ), + if (isFlutter) ...[ + const DependencySpec( + name: 'analyzer', + kind: DependencyKind.override, + version: flutterAnalyzerOverrideVersion, + ), + const DependencySpec( + name: 'meta', + kind: DependencyKind.override, + version: flutterMetaOverrideVersion, + ), + ] else + const DependencySpec( + name: 'analyzer', + kind: DependencyKind.override, + version: analyzerOverrideVersion, + ), ]; } @@ -168,7 +197,14 @@ class DependencyWirer { case DependencyKind.dev: return !devDeps.containsKey(spec.name); case DependencyKind.override: - return !overrides.containsKey(spec.name); + if (!overrides.containsKey(spec.name)) { + return true; // missing entirely + } + // Key exists: check if the value matches the required version. + final existing = overrides[spec.name]; + final existingStr = (existing is String ? existing : existing.toString()).trim(); + final requiredStr = (spec.version ?? '').trim(); + return existingStr != requiredStr; // stale if different } }).toList(); } @@ -189,7 +225,9 @@ class DependencyWirer { /// /// - If the `dependency_overrides:` section does not exist, it is appended. /// - If it exists but [key] is absent, the entry is inserted under it. - /// - If [key] already exists, the content is returned unchanged (idempotent). + /// - If [key] already exists with the same [value], the content is returned + /// unchanged (idempotent). If the existing value differs, it is replaced + /// in place. /// /// Pure function — does not perform I/O. static String addOverrideToPubspec( @@ -224,7 +262,14 @@ class DependencyWirer { break; } if (line.startsWith(keyPrefix)) { - return content; // already present + // Key exists: check if the value matches. + final existingValue = line.substring(keyPrefix.length).trim(); + if (existingValue == value) { + return content; // already has the correct value + } + // Value differs: replace the line. + lines[i] = ' $key: $value'; + return lines.join('\n'); } } @@ -293,6 +338,33 @@ class DependencyWirer { .where((s) => s.kind == DependencyKind.override) .toList(); + // --- dependency_overrides via direct pubspec edit --- + // Written BEFORE `pub add` so the version resolution that command + // triggers already sees the overrides. In Flutter apps the analyzer + + // meta overrides are what make the transitive graph resolvable in the + // first place, so they must be in the pubspec before anything resolves. + if (overrideSpecs.isNotEmpty) { + var newContent = pubspecFile.readAsStringSync(); + for (final spec in overrideSpecs) { + newContent = addOverrideToPubspec( + newContent, + spec.name, + spec.version ?? '', + ); + print(' ✅ Added override:${spec.name}=${spec.version}'); + } + try { + await pubspecFile.writeAsString(newContent); + // Record overrides as added only after the write succeeds. + added.addAll(overrideSpecs.map((s) => s.name)); + } catch (e) { + print( + ' ⚠️ Failed to write dependency_overrides to pubspec.yaml: $e', + ); + failed.addAll(overrideSpecs.map((s) => s.name)); + } + } + // --- regular / dev deps via `pub add` --- // Flutter projects must use `flutter pub add`/`flutter pub get`: the // standalone `dart` executable cannot resolve `sdk: flutter` deps. @@ -320,28 +392,8 @@ class DependencyWirer { } } - // --- dependency_overrides via direct pubspec edit --- + // --- final re-resolve so the whole graph is consistent --- if (overrideSpecs.isNotEmpty) { - var newContent = pubspecFile.readAsStringSync(); - for (final spec in overrideSpecs) { - newContent = addOverrideToPubspec( - newContent, - spec.name, - spec.version ?? '', - ); - print(' ✅ Added override:${spec.name}=${spec.version}'); - } - try { - await pubspecFile.writeAsString(newContent); - // Record overrides as added only after the write succeeds. - added.addAll(overrideSpecs.map((s) => s.name)); - } catch (e) { - print( - ' ⚠️ Failed to write dependency_overrides to pubspec.yaml: $e', - ); - failed.addAll(overrideSpecs.map((s) => s.name)); - } - // Re-resolve so the override takes effect. try { final getResult = await Process.run( pubExecutable, @@ -352,7 +404,8 @@ class DependencyWirer { final err = getResult.stderr.toString().trim(); if (err.isNotEmpty) { print( - ' ⚠️ $pubExecutable pub get reported issues after override edit:', + ' ⚠️ $pubExecutable pub get reported issues after wiring ' + 'overrides ${overrideSpecs.map((s) => '${s.name}=${s.version}').join(', ')}:', ); print(' ${err.split('\n').take(3).join('\n ')}'); } diff --git a/test/commands/setup_command_test.dart b/test/commands/setup_command_test.dart index ef6c428a..45d9ba14 100644 --- a/test/commands/setup_command_test.dart +++ b/test/commands/setup_command_test.dart @@ -157,7 +157,8 @@ dev_dependencies: mocktail: ^1.0.4 flutter_lints: ^6.0.0 dependency_overrides: - analyzer: 14.1.0 + analyzer: ^13.1.0 + meta: ^1.19.0 '''; final missing = DependencyWirer.findMissing(fullPubspec, isFlutter: true); expect(missing, isEmpty); diff --git a/test/core/dependencies/dependency_wirer_test.dart b/test/core/dependencies/dependency_wirer_test.dart index 7b6d8bb9..f1728e03 100644 --- a/test/core/dependencies/dependency_wirer_test.dart +++ b/test/core/dependencies/dependency_wirer_test.dart @@ -59,6 +59,12 @@ void main() { expect(zorphyAnn.isGit, isTrue); expect(zorphyAnn.gitUrl, DependencyWirer.zorphyGitUrl); expect(zorphyAnn.gitPath, 'zorphy_annotation'); + // Must match the URL string used by the zuraffa root pubspec + // (https://github.com/arrrrny/zorphy.git): pub only unifies git + // sources when the URL matches verbatim, so a bare-zorphy variant + // breaks version solving once zuraffa_flutter pulls zuraffa + // transitively. + expect(zorphyAnn.gitUrl, 'https://github.com/arrrrny/zorphy.git'); }); test('build_runner and mocktail are dev dependencies', () { @@ -72,13 +78,30 @@ void main() { expect(mocktail.isGit, isFalse); }); - test('analyzer is an override with the pinned version', () { + test('flutter project overrides analyzer ^13.1.0 + meta ^1.19.0', () { final specs = DependencyWirer.standardSet(isFlutter: true); final analyzer = specs.firstWhere((s) => s.name == 'analyzer'); + final meta = specs.firstWhere((s) => s.name == 'meta'); + + expect(analyzer.kind, DependencyKind.override); + expect(analyzer.version, DependencyWirer.flutterAnalyzerOverrideVersion); + expect(analyzer.isOverride, isTrue); + // Flutter apps need the meta overlay too: the Flutter SDK pins + // meta 1.18.0 while analyzer >=13.1.0 requires meta ^1.18.3. + expect(meta.kind, DependencyKind.override); + expect(meta.version, DependencyWirer.flutterMetaOverrideVersion); + expect(meta.isOverride, isTrue); + }); + + test('dart project keeps the pinned analyzer 14.1.0 override only', () { + final specs = DependencyWirer.standardSet(isFlutter: false); + final analyzer = specs.firstWhere((s) => s.name == 'analyzer'); expect(analyzer.kind, DependencyKind.override); expect(analyzer.version, DependencyWirer.analyzerOverrideVersion); expect(analyzer.isOverride, isTrue); + // Pure Dart packages do not overlay meta. + expect(specs.map((s) => s.name), isNot(contains('meta'))); }); }); @@ -132,7 +155,8 @@ dev_dependencies: flutter_lints: ^6.0.0 dependency_overrides: - analyzer: 14.1.0 + analyzer: ^13.1.0 + meta: ^1.19.0 '''; final missing = DependencyWirer.findMissing( pubspec, @@ -162,7 +186,8 @@ dev_dependencies: flutter_lints: ^6.0.0 dependency_overrides: - analyzer: 14.1.0 + analyzer: ^13.1.0 + meta: ^1.19.0 '''; final missing = DependencyWirer.findMissing( pubspec, @@ -196,7 +221,8 @@ dev_dependencies: flutter_lints: ^6.0.0 dependency_overrides: - analyzer: 14.1.0 + analyzer: ^13.1.0 + meta: ^1.19.0 '''; final missing = DependencyWirer.findMissing( pubspec, @@ -208,7 +234,7 @@ dependency_overrides: expect(missing.first.kind, DependencyKind.dev); }); - test('detects missing analyzer override only', () { + test('detects missing analyzer + meta overrides only', () { final pubspec = ''' name: my_app environment: @@ -236,6 +262,38 @@ dev_dependencies: isFlutter: true, ); + expect(missing.length, 2); + expect(missing.map((s) => s.name), containsAll(['analyzer', 'meta'])); + expect( + missing.every((s) => s.kind == DependencyKind.override), + isTrue, + ); + }); + + test('dart project: missing analyzer override only', () { + final pubspec = ''' +name: my_pkg +environment: + sdk: ^3.11.0 + +dependencies: + zuraffa: + git: + url: https://github.com/arrrrny/zuraffa + zorphy_annotation: + git: + url: https://github.com/arrrrny/zorphy + path: zorphy_annotation + +dev_dependencies: + build_runner: ^2.15.2 + mocktail: ^1.0.4 +'''; + final missing = DependencyWirer.findMissing( + pubspec, + isFlutter: false, + ); + expect(missing.length, 1); expect(missing.first.name, 'analyzer'); expect(missing.first.kind, DependencyKind.override); @@ -271,6 +329,46 @@ dependency_overrides: expect(missing, isEmpty); }); + test('flutter project: stale analyzer override version is detected as missing', () { + final pubspec = ''' +name: my_app +environment: + sdk: ^3.11.0 + +dependencies: + flutter: + sdk: flutter + zuraffa_flutter: + git: + url: https://github.com/arrrrny/zuraffa + path: zuraffa_flutter + zorphy_annotation: + git: + url: https://github.com/arrrrny/zorphy + path: zorphy_annotation + +dev_dependencies: + build_runner: ^2.15.2 + mocktail: ^1.0.4 + flutter_lints: ^6.0.0 + +dependency_overrides: + analyzer: 14.1.0 + meta: ^1.19.0 +'''; + final missing = DependencyWirer.findMissing( + pubspec, + isFlutter: true, + ); + + // Flutter projects expect analyzer: ^13.1.0, not 14.1.0, so analyzer + // should be detected as needing an update. + expect(missing.length, 1); + expect(missing.first.name, 'analyzer'); + expect(missing.first.kind, DependencyKind.override); + expect(missing.first.version, DependencyWirer.flutterAnalyzerOverrideVersion); + }); + test('returns all specs for unparseable pubspec', () { final pubspec = 'this is not ::: valid yaml {{{'; final missing = DependencyWirer.findMissing( @@ -359,14 +457,14 @@ dependencies: expect(analyzerIdx, lessThan(depsIdx)); }); - test('is idempotent when key already exists', () { + test('is idempotent when key already exists with matching value', () { final pubspec = ''' name: my_app environment: sdk: ^3.11.0 dependency_overrides: - analyzer: 13.0.0 + analyzer: 14.1.0 dependencies: http: ^1.6.0 @@ -377,10 +475,37 @@ dependencies: '14.1.0', ); - // Should be unchanged — existing value preserved + // Should be unchanged — value already matches expect(result, equals(pubspec)); }); + test('replaces existing value when it differs from new value', () { + final pubspec = ''' +name: my_app +environment: + sdk: ^3.11.0 + +dependency_overrides: + analyzer: 14.1.0 + +dependencies: + http: ^1.6.0 +'''; + final result = DependencyWirer.addOverrideToPubspec( + pubspec, + 'analyzer', + '^13.1.0', + ); + + // Old value (14.1.0) should be replaced with new value (^13.1.0) + expect(result, contains(' analyzer: ^13.1.0')); + expect(result, isNot(contains(' analyzer: 14.1.0'))); + // Other content should be preserved + expect(result, contains('name: my_app')); + expect(result, contains('dependencies:')); + expect(result, contains('http: ^1.6.0')); + }); + test('handles pubspec with no trailing newline', () { final pubspec = 'name: my_app\nenvironment:\n sdk: ^3.11.0'; final result = DependencyWirer.addOverrideToPubspec(