From 06d435ef2f8e6f54711d8dec785bb83fc13d55b3 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sat, 7 Mar 2026 20:59:48 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactoring=20runner?= =?UTF-8?q?=20to=20compatible=20with=20workspace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 + README.md | 47 +- packages/core/lib/settings/config.dart | 126 ++++-- packages/runner/build.yaml | 10 +- packages/runner/lib/flutter_gen_runner.dart | 425 ++++++++++++++---- packages/runner/pubspec.yaml | 3 + .../runner/test/workspace_build_test.dart | 258 +++++++++++ .../packages/app/assets/color/colors.xml | 4 + .../app/assets/fonts/Inter-Regular.ttf | 1 + .../packages/app/assets/images/logo.png | 1 + .../workspace/packages/app/lib/app.dart | 1 + .../packages/app/pubspec.yaml.template | 46 ++ .../workspace/packages/unused/pubspec.yaml | 6 + .../test_fixtures/workspace/pubspec.yaml | 12 + 14 files changed, 831 insertions(+), 119 deletions(-) create mode 100644 packages/runner/test/workspace_build_test.dart create mode 100644 packages/runner/test_fixtures/workspace/packages/app/assets/color/colors.xml create mode 100644 packages/runner/test_fixtures/workspace/packages/app/assets/fonts/Inter-Regular.ttf create mode 100644 packages/runner/test_fixtures/workspace/packages/app/assets/images/logo.png create mode 100644 packages/runner/test_fixtures/workspace/packages/app/lib/app.dart create mode 100644 packages/runner/test_fixtures/workspace/packages/app/pubspec.yaml.template create mode 100644 packages/runner/test_fixtures/workspace/packages/unused/pubspec.yaml create mode 100644 packages/runner/test_fixtures/workspace/pubspec.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3c5341e..59a331724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## Next + +**Feature** + +- Add `build_runner --workspace` support for `flutter_gen_runner` using a manifest + post-process materialization pipeline. + +**Development** + +- Document and align the minimum supported versions for the new build pipeline: Dart `>=3.7.0` and `build_runner >=2.12.0`. + ## 5.13.0+1 **Development** diff --git a/README.md b/README.md index c65d7637f..403428ef3 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,17 @@ Widget build(BuildContext context) { 1. Add [build_runner] and [FlutterGen] to your package's pubspec.yaml file: ```yaml + environment: + sdk: ^3.7.0 + dev_dependencies: - build_runner: + build_runner: ^2.12.0 flutter_gen_runner: ``` + `flutter_gen_runner` now relies on post-process builders with + `build_to: source`, so `build_runner >=2.12.0` is required. + 2. Install [FlutterGen] ```sh @@ -75,6 +81,41 @@ Widget build(BuildContext context) { dart run build_runner build ``` +#### Pub workspaces + +FlutterGen also supports [`dart pub` workspaces](https://dart.dev/tools/pub/workspaces). +Run `build_runner` from the workspace root, and make sure each workspace member +that should be built has `resolution: workspace` in its `pubspec.yaml`. + +```yaml +# workspace root pubspec.yaml +environment: + sdk: ^3.7.0 + +workspace: + - packages/app +``` + +```yaml +# packages/app/pubspec.yaml +name: app +resolution: workspace + +dev_dependencies: + flutter_gen_runner: + build_runner: ^2.12.0 +``` + +```sh +dart run build_runner build --workspace +``` + +FlutterGen will resolve each package from the active build target instead of the +process working directory, so package-local `pubspec.yaml` configuration and +output paths continue to work in workspace builds. + +For workspace builds, use Dart `>=3.7.0` together with `build_runner >=2.12.0`. + ### Pub Global Works with macOS, Linux and Windows. @@ -171,7 +212,9 @@ flutter: ### build.yaml -You can also configure generate options in the `build.yaml`, it will be read before the `pubspec.yaml` if it exists. +When using `build_runner`, you can also configure generator options in the +target `build.yaml`. These builder options are applied on top of the +`pubspec.yaml` configuration for the current target. ```yaml # build.yaml diff --git a/packages/core/lib/settings/config.dart b/packages/core/lib/settings/config.dart index 655b8e189..7e1f9635d 100644 --- a/packages/core/lib/settings/config.dart +++ b/packages/core/lib/settings/config.dart @@ -39,26 +39,23 @@ class Config { final int? formatterPageWidth; } -Config loadPubspecConfig(File pubspecFile, {File? buildFile}) { - final pubspecLocaleHint = normalize( - join(basename(pubspecFile.parent.path), basename(pubspecFile.path)), - ); - - log.info('v$packageVersion Loading ...'); - log.info('Reading options from $pubspecLocaleHint'); - - VersionConstraint? sdkConstraint; - - final defaultMap = loadYaml(configDefaultYamlContent) as YamlMap?; - - final pubspecContent = pubspecFile.readAsStringSync(); - final pubspecMap = loadYaml(pubspecContent) as YamlMap?; - if (safeCast(pubspecMap?['environment']?['sdk']) case final sdk?) { - sdkConstraint = VersionConstraint.parse(sdk); - } +class ConfigLoadInput { + const ConfigLoadInput({ + required this.pubspecFile, + required this.pubspecContent, + this.buildOptions, + this.pubspecLockContent, + this.analysisOptionsContent, + }); - Map mergedMap = mergeMap([defaultMap, pubspecMap]); + final File pubspecFile; + final String pubspecContent; + final Map? buildOptions; + final String? pubspecLockContent; + final String? analysisOptionsContent; +} +Config loadPubspecConfig(File pubspecFile, {File? buildFile}) { YamlMap? getBuildFileOptions(File file) { if (!file.existsSync()) { return null; @@ -74,6 +71,16 @@ Config loadPubspecConfig(File pubspecFile, {File? buildFile}) { return null; } + final pubspecLocaleHint = normalize( + join(basename(pubspecFile.parent.path), basename(pubspecFile.path)), + ); + + log.info('v$packageVersion Loading ...'); + log.info('Reading options from $pubspecLocaleHint'); + + final pubspecContent = pubspecFile.readAsStringSync(); + Map? buildOptions; + // Fallback to the build.yaml when no build file has been specified and // the default one has valid configurations. if (buildFile == null && getBuildFileOptions(File('build.yaml')) != null) { @@ -84,8 +91,7 @@ Config loadPubspecConfig(File pubspecFile, {File? buildFile}) { if (buildFile.existsSync()) { final optionBuildMap = getBuildFileOptions(buildFile); if (optionBuildMap != null) { - final buildMap = {'flutter_gen': optionBuildMap}; - mergedMap = mergeMap([mergedMap, buildMap]); + buildOptions = optionBuildMap; final buildLocaleHint = normalize( join(basename(buildFile.parent.path), basename(buildFile.path)), ); @@ -104,8 +110,6 @@ Config loadPubspecConfig(File pubspecFile, {File? buildFile}) { } } - final pubspec = Pubspec.fromJson(mergedMap); - final pubspecLockFile = File( normalize(join(pubspecFile.parent.path, 'pubspec.lock')), ); @@ -113,7 +117,58 @@ Config loadPubspecConfig(File pubspecFile, {File? buildFile}) { true => pubspecLockFile.readAsStringSync(), false => '', }; - final pubspecLockMap = loadYaml(pubspecLockContent) as YamlMap?; + + final analysisOptionsFile = File( + normalize(join(pubspecFile.parent.path, 'analysis_options.yaml')), + ); + final analysisOptionsContent = switch (analysisOptionsFile.existsSync()) { + true => analysisOptionsFile.readAsStringSync(), + false => '', + }; + + return loadPubspecConfigFromInput( + ConfigLoadInput( + pubspecFile: pubspecFile, + pubspecContent: pubspecContent, + buildOptions: buildOptions, + pubspecLockContent: pubspecLockContent, + analysisOptionsContent: analysisOptionsContent, + ), + ); +} + +Config loadPubspecConfigFromInput(ConfigLoadInput input) { + final pubspecLocaleHint = normalize( + join( + basename(input.pubspecFile.parent.path), + basename(input.pubspecFile.path), + ), + ); + + log.info('v$packageVersion Loading ...'); + log.info('Reading options from $pubspecLocaleHint'); + + VersionConstraint? sdkConstraint; + + final defaultMap = loadYaml(configDefaultYamlContent) as YamlMap?; + final pubspecMap = loadYaml(input.pubspecContent) as YamlMap?; + if (safeCast(pubspecMap?['environment']?['sdk']) case final sdk?) { + sdkConstraint = VersionConstraint.parse(sdk); + } + + Map mergedMap = mergeMap([defaultMap, pubspecMap]); + if (input.buildOptions case final Map buildOptions + when buildOptions.isNotEmpty) { + mergedMap = mergeMap([ + mergedMap, + {'flutter_gen': buildOptions}, + ]); + log.info('Reading options from BuilderOptions'); + } + + final pubspec = Pubspec.fromJson(mergedMap); + + final pubspecLockMap = loadYaml(input.pubspecLockContent ?? '') as YamlMap?; if (safeCast(pubspecLockMap?['sdks']?['dart']) case final sdk?) { sdkConstraint ??= VersionConstraint.parse(sdk); } @@ -131,14 +186,8 @@ Config loadPubspecConfig(File pubspecFile, {File? buildFile}) { } } - final analysisOptionsFile = File( - normalize(join(pubspecFile.parent.path, 'analysis_options.yaml')), - ); - final analysisOptionsContent = switch (analysisOptionsFile.existsSync()) { - true => analysisOptionsFile.readAsStringSync(), - false => '', - }; - final analysisOptionsMap = loadYaml(analysisOptionsContent) as YamlMap?; + final analysisOptionsMap = + loadYaml(input.analysisOptionsContent ?? '') as YamlMap?; // final formatterTrailingCommas = switch (safeCast( // analysisOptionsMap?['formatter']?['trailing_commas'], // )) { @@ -151,7 +200,7 @@ Config loadPubspecConfig(File pubspecFile, {File? buildFile}) { return Config._( pubspec: pubspec, - pubspecFile: pubspecFile, + pubspecFile: input.pubspecFile, sdkConstraint: sdkConstraint, integrationResolvedVersions: integrationResolvedVersions, integrationVersionConstraints: integrationVersionConstraints, @@ -172,3 +221,16 @@ Config? loadPubspecConfigOrNull(File pubspecFile, {File? buildFile}) { } return null; } + +Config? loadPubspecConfigFromInputOrNull(ConfigLoadInput input) { + try { + return loadPubspecConfigFromInput(input); + } on FileSystemException catch (e, s) { + log.severe('File system error when reading files.', e, s); + } on InvalidSettingsException catch (e, s) { + log.severe('Invalid settings in files.', e, s); + } on CheckedFromJsonException catch (e, s) { + log.severe('Invalid settings in files.', e, s); + } + return null; +} diff --git a/packages/runner/build.yaml b/packages/runner/build.yaml index 116a94193..d7257ca36 100644 --- a/packages/runner/build.yaml +++ b/packages/runner/build.yaml @@ -8,6 +8,14 @@ builders: flutter_gen_runner: import: 'package:flutter_gen_runner/flutter_gen_runner.dart' builder_factories: ['build'] - build_extensions: { '$package$': ['.gen.dart'] } + build_extensions: { '$package$': ['.flutter_gen.manifest.json'] } auto_apply: dependents + build_to: cache + applies_builders: ['flutter_gen_runner:flutter_gen_runner_post_process'] + +post_process_builders: + flutter_gen_runner_post_process: + import: 'package:flutter_gen_runner/flutter_gen_runner.dart' + builder_factory: 'postProcessBuild' + input_extensions: ['.flutter_gen.manifest.json'] build_to: source diff --git a/packages/runner/lib/flutter_gen_runner.dart b/packages/runner/lib/flutter_gen_runner.dart index 410fc0020..fa7b426ff 100644 --- a/packages/runner/lib/flutter_gen_runner.dart +++ b/packages/runner/lib/flutter_gen_runner.dart @@ -1,95 +1,191 @@ -import 'dart:collection'; -import 'dart:io'; +import 'dart:convert'; +import 'dart:io' show File; +import 'dart:isolate'; import 'package:build/build.dart'; -import 'package:collection/collection.dart'; -import 'package:crypto/crypto.dart'; import 'package:flutter_gen_core/flutter_generator.dart'; import 'package:flutter_gen_core/settings/config.dart'; -import 'package:flutter_gen_core/settings/flavored_asset.dart'; import 'package:glob/glob.dart'; +import 'package:package_config/package_config.dart'; import 'package:path/path.dart'; import 'package:yaml/yaml.dart'; -Builder build(BuilderOptions options) => FlutterGenBuilder(); +Builder build(BuilderOptions options) => FlutterGenBuilder(options); +PostProcessBuilder postProcessBuild(BuilderOptions options) => + FlutterGenPostProcessBuilder(); +/// Main builder for FlutterGen when used through `build_runner`. +/// +/// The implementation is intentionally split into two phases: +/// +/// 1. A normal [Builder] resolves the current target package, reads config from +/// the package-local assets visible to the current [BuildStep], and renders +/// the final generated file contents entirely in memory. +/// 2. The normal builder writes a single declared intermediate manifest. +/// 3. A [PostProcessBuilder] consumes that manifest and writes the actual +/// source outputs into the package. +/// +/// This indirection is required because `flutter_gen.output` is configuration +/// driven. A normal builder must declare its outputs up front in +/// [buildExtensions], but FlutterGen's real `.gen.dart` paths are only known +/// after reading the package's `pubspec.yaml` / builder options. The manifest +/// gives us one fixed declared output while still allowing the materialization +/// step to write package-relative source files. class FlutterGenBuilder extends Builder { - static AssetId _output(BuildStep buildStep, String path) { - return AssetId( - buildStep.inputId.package, - path, - ); - } + FlutterGenBuilder(this._options); - final generator = FlutterGenerator( - File('pubspec.yaml'), - buildFile: File('build.yaml'), - ); + static const _manifestExtension = '.flutter_gen.manifest.json'; + static const _assetsName = 'assets.gen.dart'; + static const _colorsName = 'colors.gen.dart'; + static const _fontsName = 'fonts.gen.dart'; - late final _config = loadPubspecConfigOrNull( - generator.pubspecFile, - buildFile: generator.buildFile, - ); - _FlutterGenBuilderState? _currentState; + final BuilderOptions _options; + + /// We resolve package roots from the runtime package configuration of the + /// build script isolate. + /// + /// `buildStep.packageConfig` is package-aware, but the URIs exposed there are + /// asset-style URIs. The legacy generator code still needs a real file-system + /// root so it can reuse the existing `dart:io` based generation pipeline. + /// Loading the isolate package config once gives us stable `file:` package + /// roots for all packages in the build graph, including workspace members. + static final Future _runtimePackageConfig = + loadPackageConfigUri(Isolate.packageConfigSync!); @override Future build(BuildStep buildStep) async { - if (_config case final config?) { - final state = await _createState(config, buildStep); - if (state.shouldSkipGenerate(_currentState)) { - return; - } - _currentState = state; + // Resolve the package being built from the current BuildStep instead of the + // process working directory. This is the key workspace-safe behavior. + final packageRoot = await _packageRoot(buildStep); + final pubspecId = AssetId(buildStep.inputId.package, 'pubspec.yaml'); - await generator.build( - config: config, - writer: (contents, path) { - buildStep.writeAsString(_output(buildStep, path), contents); - }, + // A missing pubspec means there is nothing meaningful to generate. We still + // emit an empty manifest so the post-process step has a deterministic input + // and can clean any previously owned outputs. + if (!await buildStep.canRead(pubspecId)) { + await _writeManifest( + buildStep, + FlutterGenManifest( + packageName: buildStep.inputId.package, + packageRoot: packageRoot, + outputs: const [], + ), ); + return; } + + // Read configuration through build_runner's asset APIs so that the read is + // scoped to the active package in both single-package and workspace builds. + final pubspecContent = await buildStep.readAsString(pubspecId); + final pubspecLockId = AssetId(buildStep.inputId.package, 'pubspec.lock'); + final analysisOptionsId = AssetId( + buildStep.inputId.package, + 'analysis_options.yaml', + ); + final pubspecLockContent = + await _readOptionalAsset(buildStep, pubspecLockId); + final analysisOptionsContent = await _readOptionalAsset( + buildStep, + analysisOptionsId, + ); + + // `Config` still carries a `File pubspecFile` because the lower-level core + // generator APIs remain file-system based. We construct a package-local file + // path here after resolving the correct package root above. + final pubspecFile = File(join(packageRoot, 'pubspec.yaml')); + final config = loadPubspecConfigFromInputOrNull( + ConfigLoadInput( + pubspecFile: pubspecFile, + pubspecContent: pubspecContent, + // BuilderOptions are now the supported way to pass build.yaml options in + // the build_runner path. This keeps config target-local and workspace + // aware without relying on process cwd. + buildOptions: _options.config, + pubspecLockContent: pubspecLockContent, + analysisOptionsContent: analysisOptionsContent, + ), + ); + + // Keep the manifest contract deterministic even for invalid or unsupported + // configurations. An empty manifest means "FlutterGen owns no outputs for + // this package right now". + if (config == null) { + await _writeManifest( + buildStep, + FlutterGenManifest( + packageName: buildStep.inputId.package, + packageRoot: packageRoot, + outputs: const [], + ), + ); + return; + } + + // The legacy `FlutterGenerator` still reads asset metadata through + // `dart:io`, but we must also teach build_runner which source assets this + // action depends on so incremental rebuilds behave correctly. Digesting the + // matched assets records those dependencies in the asset graph. + await _trackInputs(config, buildStep); + + final outputs = []; + + // Reuse the existing core generator and capture its final file contents in + // memory rather than writing them directly to disk. The post-process phase + // will materialize them later. + final generator = FlutterGenerator( + pubspecFile, + assetsName: _assetsName, + colorsName: _colorsName, + fontsName: _fontsName, + ); + await generator.build( + config: config, + writer: (contents, path) { + outputs.add( + FlutterGenManifestOutput( + path: relative(path, from: packageRoot), + contents: contents, + ), + ); + }, + ); + + // Persist the full generation result as a single manifest so the next phase + // can write arbitrary source outputs without needing to recompute config or + // re-read inputs. + await _writeManifest( + buildStep, + FlutterGenManifest( + packageName: buildStep.inputId.package, + packageRoot: packageRoot, + outputs: outputs, + ), + ); } @override - Map> get buildExtensions { - if (_config case final config?) { - final output = config.pubspec.flutterGen.output; - return { - r'$package$': [ - for (final name in [ - generator.assetsName, - generator.colorsName, - generator.fontsName, - ]) - join(output, name), - ], + Map> get buildExtensions => { + // The entire builder graph is anchored on one fixed declared output. + // This is what lets us support configuration-dependent final paths. + r'$package$': [_manifestExtension], }; - } else { - return {}; - } - } - Future<_FlutterGenBuilderState> _createState( + Future _trackInputs( Config config, BuildStep buildStep, ) async { final pubspec = config.pubspec; - final HashSet assets = HashSet(); + // Asset generation depends on every matched flutter asset path. We mirror + // the same glob expansion behavior here so build_runner can invalidate the + // manifest whenever one of those inputs changes. if (pubspec.flutterGen.assets.enabled) { for (final asset in pubspec.flutter.assets) { - final FlavoredAsset flavored; String assetInput; if (asset is YamlMap) { - flavored = FlavoredAsset( - path: asset['path'], - flavors: - (asset['flavors'] as YamlList?)?.toSet().cast() ?? {}, - ); assetInput = asset['path']; } else { - flavored = FlavoredAsset(path: asset as String); - assetInput = asset; + assetInput = asset as String; } if (assetInput.isEmpty) { continue; @@ -98,54 +194,215 @@ class FlutterGenBuilder extends Builder { assetInput += '*'; } await for (final assetId in buildStep.findAssets(Glob(assetInput))) { - assets.add(flavored.copyWith(path: assetId.path)); + await buildStep.digest(assetId); } } } - final HashMap colors = HashMap(); + // Color generation also depends on the contents of its configured input + // files. Digesting them is sufficient to establish the dependency edge. if (pubspec.flutterGen.colors.enabled) { for (final colorInput in pubspec.flutterGen.colors.inputs) { if (colorInput.isEmpty) { continue; } await for (final assetId in buildStep.findAssets(Glob(colorInput))) { - final digest = await buildStep.digest(assetId); - colors[assetId.path] = digest; + await buildStep.digest(assetId); } } } + } - final pubspecAsset = - await buildStep.findAssets(Glob(config.pubspecFile.path)).single; + Future _packageRoot(BuildStep buildStep) async { + final packageConfig = await _runtimePackageConfig; + final package = packageConfig[buildStep.inputId.package]; + if (package == null) { + throw StateError( + 'Unable to resolve package root for ${buildStep.inputId.package}.', + ); + } + return normalize(package.root.toFilePath()); + } - final pubspecDigest = await buildStep.digest(pubspecAsset); + /// Reads an optional package-local asset if it exists. + /// + /// We keep this helper small because `pubspec.lock` and + /// `analysis_options.yaml` are auxiliary configuration sources: their absence + /// should not fail the entire generation step. + Future _readOptionalAsset( + BuildStep buildStep, + AssetId assetId, + ) async { + if (!await buildStep.canRead(assetId)) { + return null; + } + return buildStep.readAsString(assetId); + } - return _FlutterGenBuilderState( - pubspecDigest: pubspecDigest, - assets: assets, - colors: colors, + /// Writes the single declared intermediate artifact for this package. + /// + /// The manifest is intentionally JSON so the post-process phase can stay + /// simple and self-contained: it reads one input, writes many outputs, and + /// does not need access to any additional resources. + Future _writeManifest( + BuildStep buildStep, + FlutterGenManifest manifest, + ) { + return buildStep.writeAsString( + AssetId(buildStep.inputId.package, _manifestExtension), + jsonEncode(manifest.toJson()), ); } } -class _FlutterGenBuilderState { - const _FlutterGenBuilderState({ - required this.pubspecDigest, - required this.assets, - required this.colors, - }); +/// Materializes final source files from the manifest written by +/// [FlutterGenBuilder]. +/// +/// A post-process builder is the only supported way in the current build_runner +/// model to write source outputs whose paths are not statically known to the +/// original builder. +class FlutterGenPostProcessBuilder extends PostProcessBuilder { + static const _manifestExtension = '.flutter_gen.manifest.json'; + static const _ownerFileName = 'flutter_gen_owner.json'; - final Digest pubspecDigest; - final HashSet assets; - final HashMap colors; + @override + Iterable get inputExtensions => const [_manifestExtension]; + + @override + Future build(PostProcessBuildStep buildStep) async { + // The manifest contains the entire desired output state for one package. + final manifest = FlutterGenManifest.fromJson( + jsonDecode(await buildStep.readInputAsString()) as Map, + ); - bool shouldSkipGenerate(_FlutterGenBuilderState? previous) { - if (previous == null) { - return false; + // We persist ownership information outside the source tree so stale output + // cleanup can compare "previously owned files" with "currently desired + // files" across builds. + final ownerFile = File( + join( + manifest.packageRoot, + '.dart_tool', + 'flutter_build', + 'flutter_gen', + _ownerFileName, + ), + ); + + final previousOutputs = await _readOwnedPaths(ownerFile); + final nextOutputs = manifest.outputs.map((output) => output.path).toSet(); + + // Explicit stale cleanup is required because these source outputs are not + // regular declared outputs of the original builder. build_runner manages the + // manifest lifecycle, but FlutterGen owns the lifecycle of the final + // materialized files. + for (final output in previousOutputs.difference(nextOutputs)) { + final absolutePath = normalize(join(manifest.packageRoot, output)); + if (!_isWithinPackage(manifest.packageRoot, absolutePath)) { + continue; + } + final file = File(absolutePath); + if (file.existsSync()) { + file.deleteSync(); + } + } + + // Materialize the exact output set described by the manifest. + for (final output in manifest.outputs) { + await buildStep.writeAsString( + AssetId(manifest.packageName, output.path), + output.contents, + ); + } + + if (!ownerFile.parent.existsSync()) { + ownerFile.parent.createSync(recursive: true); } - return pubspecDigest == previous.pubspecDigest && - const SetEquality().equals(assets, previous.assets) && - const MapEquality().equals(colors, previous.colors); + + // Persist the new ownership snapshot after successful writes. + ownerFile.writeAsStringSync( + jsonEncode({ + 'paths': nextOutputs.toList()..sort(), + }), + ); + } + + /// Reads the set of source files previously materialized for this package. + Future> _readOwnedPaths(File ownerFile) async { + if (!ownerFile.existsSync()) { + return {}; + } + final raw = + jsonDecode(await ownerFile.readAsString()) as Map; + final paths = raw['paths']; + if (paths is! List) { + return {}; + } + return paths.whereType().toSet(); + } + + /// Guards cleanup against deleting files outside the active package. + bool _isWithinPackage(String packageRoot, String candidatePath) { + final normalizedRoot = normalize(packageRoot); + final normalizedCandidate = normalize(candidatePath); + return normalizedCandidate == normalizedRoot || + isWithin(normalizedRoot, normalizedCandidate); } } + +/// Self-contained description of the desired FlutterGen outputs for one package. +/// +/// The manifest is designed to be replayable by the post-process builder with no +/// additional context: package root, package name, and final rendered file +/// contents are all embedded here. +class FlutterGenManifest { + const FlutterGenManifest({ + required this.packageName, + required this.packageRoot, + required this.outputs, + }); + + factory FlutterGenManifest.fromJson(Map json) { + return FlutterGenManifest( + packageName: json['package_name'] as String, + packageRoot: json['package_root'] as String, + outputs: (json['outputs'] as List? ?? const []) + .whereType>() + .map(FlutterGenManifestOutput.fromJson) + .toList(), + ); + } + + final String packageName; + final String packageRoot; + final List outputs; + + Map toJson() => { + 'schema_version': 1, + 'package_name': packageName, + 'package_root': packageRoot, + 'outputs': [for (final output in outputs) output.toJson()], + }; +} + +/// One final generated source file captured inside a [FlutterGenManifest]. +class FlutterGenManifestOutput { + const FlutterGenManifestOutput({ + required this.path, + required this.contents, + }); + + factory FlutterGenManifestOutput.fromJson(Map json) { + return FlutterGenManifestOutput( + path: json['path'] as String, + contents: json['contents'] as String, + ); + } + + final String path; + final String contents; + + Map toJson() => { + 'path': path, + 'contents': contents, + }; +} diff --git a/packages/runner/pubspec.yaml b/packages/runner/pubspec.yaml index b8d79c402..f0b9a5fe5 100644 --- a/packages/runner/pubspec.yaml +++ b/packages/runner/pubspec.yaml @@ -12,11 +12,14 @@ environment: dependencies: flutter_gen_core: 5.13.0+1 build: '>=2.0.0 <5.0.0' + build_config: ^1.1.2 collection: ^1.17.0 crypto: ^3.0.0 glob: ^2.0.0 + package_config: ^2.1.0 path: ^1.8.0 yaml: ^3.0.0 dev_dependencies: lints: any # Ignoring the version to allow editing across SDK versions. + test: ^1.25.0 diff --git a/packages/runner/test/workspace_build_test.dart b/packages/runner/test/workspace_build_test.dart new file mode 100644 index 000000000..5d67cc332 --- /dev/null +++ b/packages/runner/test/workspace_build_test.dart @@ -0,0 +1,258 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + test('supports build_runner --workspace and cleans stale outputs', () async { + final workspaceDir = await _createWorkspaceFixture(); + addTearDown(() async { + if (workspaceDir.existsSync()) { + workspaceDir.deleteSync(recursive: true); + } + }); + + await _runProcess( + 'flutter', + ['pub', 'get'], + workingDirectory: workspaceDir.path, + ); + + await _runProcess( + 'dart', + [ + 'run', + 'build_runner', + 'build', + '--workspace', + '--delete-conflicting-outputs', + ], + workingDirectory: workspaceDir.path, + ); + + final appDir = Directory(p.join(workspaceDir.path, 'packages', 'app')); + final ownerFile = File( + p.join( + appDir.path, + '.dart_tool', + 'flutter_build', + 'flutter_gen', + 'flutter_gen_owner.json', + ), + ); + + expect( + File(p.join(appDir.path, 'lib', 'gen', 'assets.gen.dart')).existsSync(), + isTrue, + ); + expect( + File(p.join(appDir.path, 'lib', 'gen', 'colors.gen.dart')).existsSync(), + isTrue, + ); + expect( + File(p.join(appDir.path, 'lib', 'gen', 'fonts.gen.dart')).existsSync(), + isTrue, + ); + expect(ownerFile.existsSync(), isTrue); + + final initialOwner = + jsonDecode(ownerFile.readAsStringSync()) as Map; + expect( + initialOwner['paths'], + containsAll([ + 'lib/gen/assets.gen.dart', + 'lib/gen/colors.gen.dart', + 'lib/gen/fonts.gen.dart', + ]), + ); + + final appPubspec = File(p.join(appDir.path, 'pubspec.yaml')); + appPubspec.writeAsStringSync( + appPubspec + .readAsStringSync() + .replaceFirst('output: lib/gen/', 'output: lib/alt_gen/'), + ); + + await _runProcess( + 'dart', + [ + 'run', + 'build_runner', + 'build', + '--workspace', + '--delete-conflicting-outputs', + ], + workingDirectory: workspaceDir.path, + ); + + expect( + File(p.join(appDir.path, 'lib', 'gen', 'assets.gen.dart')).existsSync(), + isFalse, + ); + expect( + File(p.join(appDir.path, 'lib', 'gen', 'colors.gen.dart')).existsSync(), + isFalse, + ); + expect( + File(p.join(appDir.path, 'lib', 'gen', 'fonts.gen.dart')).existsSync(), + isFalse, + ); + + expect( + File(p.join(appDir.path, 'lib', 'alt_gen', 'assets.gen.dart')) + .existsSync(), + isTrue, + ); + expect( + File(p.join(appDir.path, 'lib', 'alt_gen', 'colors.gen.dart')) + .existsSync(), + isTrue, + ); + expect( + File(p.join(appDir.path, 'lib', 'alt_gen', 'fonts.gen.dart')) + .existsSync(), + isTrue, + ); + + final updatedOwner = + jsonDecode(ownerFile.readAsStringSync()) as Map; + expect( + updatedOwner['paths'], + containsAll([ + 'lib/alt_gen/assets.gen.dart', + 'lib/alt_gen/colors.gen.dart', + 'lib/alt_gen/fonts.gen.dart', + ]), + ); + }); + + test('applies package build.yaml options in workspace mode', () async { + final workspaceDir = await _createWorkspaceFixture(); + addTearDown(() async { + if (workspaceDir.existsSync()) { + workspaceDir.deleteSync(recursive: true); + } + }); + + final appDir = Directory(p.join(workspaceDir.path, 'packages', 'app')); + final appBuildYaml = File(p.join(appDir.path, 'build.yaml')); + appBuildYaml.writeAsStringSync(r''' +targets: + $default: + builders: + flutter_gen_runner: + options: + output: lib/build_gen/ +'''); + + await _runProcess( + 'flutter', + ['pub', 'get'], + workingDirectory: workspaceDir.path, + ); + + await _runProcess( + 'dart', + [ + 'run', + 'build_runner', + 'build', + '--workspace', + '--delete-conflicting-outputs', + ], + workingDirectory: workspaceDir.path, + ); + + expect( + File(p.join(appDir.path, 'lib', 'build_gen', 'assets.gen.dart')) + .existsSync(), + isTrue, + ); + expect( + File(p.join(appDir.path, 'lib', 'build_gen', 'colors.gen.dart')) + .existsSync(), + isTrue, + ); + expect( + File(p.join(appDir.path, 'lib', 'build_gen', 'fonts.gen.dart')) + .existsSync(), + isTrue, + ); + + expect( + File(p.join(appDir.path, 'lib', 'gen', 'assets.gen.dart')).existsSync(), + isFalse, + ); + }); +} + +Future _createWorkspaceFixture() async { + final runnerDir = _runnerPackageDirectory(); + final fixtureDir = + Directory(p.join(runnerDir.path, 'test_fixtures', 'workspace')); + final tempDir = + await Directory.systemTemp.createTemp('flutter_gen_workspace_fixture_'); + + await _copyDirectory(fixtureDir, tempDir); + + final appPubspecTemplate = File( + p.join(tempDir.path, 'packages', 'app', 'pubspec.yaml.template'), + ); + final appPubspec = File( + p.join(tempDir.path, 'packages', 'app', 'pubspec.yaml'), + ); + final coreDir = Directory(p.join(runnerDir.parent.path, 'core')); + appPubspec.writeAsStringSync( + appPubspecTemplate + .readAsStringSync() + .replaceAll('__RUNNER_PATH__', _yamlPath(runnerDir.path)) + .replaceAll('__CORE_PATH__', _yamlPath(coreDir.path)), + ); + appPubspecTemplate.deleteSync(); + + return tempDir; +} + +Directory _runnerPackageDirectory() { + return Directory.current; +} + +String _yamlPath(String path) { + return p.normalize(path).replaceAll(r'\', '/'); +} + +Future _copyDirectory(Directory source, Directory destination) async { + await for (final entity in source.list(recursive: true, followLinks: false)) { + final relativePath = p.relative(entity.path, from: source.path); + final targetPath = p.join(destination.path, relativePath); + if (entity is Directory) { + Directory(targetPath).createSync(recursive: true); + } else if (entity is File) { + File(targetPath).parent.createSync(recursive: true); + entity.copySync(targetPath); + } + } +} + +Future _runProcess( + String executable, + List arguments, { + required String workingDirectory, +}) async { + final result = await Process.run( + executable, + arguments, + workingDirectory: workingDirectory, + runInShell: true, + ); + + if (result.exitCode != 0) { + fail( + 'Command failed: $executable ${arguments.join(' ')}\n' + 'exitCode: ${result.exitCode}\n' + 'stdout:\n${result.stdout}\n' + 'stderr:\n${result.stderr}', + ); + } +} diff --git a/packages/runner/test_fixtures/workspace/packages/app/assets/color/colors.xml b/packages/runner/test_fixtures/workspace/packages/app/assets/color/colors.xml new file mode 100644 index 000000000..a0f3af482 --- /dev/null +++ b/packages/runner/test_fixtures/workspace/packages/app/assets/color/colors.xml @@ -0,0 +1,4 @@ + + + #6750A4 + diff --git a/packages/runner/test_fixtures/workspace/packages/app/assets/fonts/Inter-Regular.ttf b/packages/runner/test_fixtures/workspace/packages/app/assets/fonts/Inter-Regular.ttf new file mode 100644 index 000000000..0d3d81359 --- /dev/null +++ b/packages/runner/test_fixtures/workspace/packages/app/assets/fonts/Inter-Regular.ttf @@ -0,0 +1 @@ +placeholder-font diff --git a/packages/runner/test_fixtures/workspace/packages/app/assets/images/logo.png b/packages/runner/test_fixtures/workspace/packages/app/assets/images/logo.png new file mode 100644 index 000000000..8e3ac5e66 --- /dev/null +++ b/packages/runner/test_fixtures/workspace/packages/app/assets/images/logo.png @@ -0,0 +1 @@ +not-a-real-png-but-good-enough-for-generator-tests diff --git a/packages/runner/test_fixtures/workspace/packages/app/lib/app.dart b/packages/runner/test_fixtures/workspace/packages/app/lib/app.dart new file mode 100644 index 000000000..ab73b3a23 --- /dev/null +++ b/packages/runner/test_fixtures/workspace/packages/app/lib/app.dart @@ -0,0 +1 @@ +void main() {} diff --git a/packages/runner/test_fixtures/workspace/packages/app/pubspec.yaml.template b/packages/runner/test_fixtures/workspace/packages/app/pubspec.yaml.template new file mode 100644 index 000000000..888961a3f --- /dev/null +++ b/packages/runner/test_fixtures/workspace/packages/app/pubspec.yaml.template @@ -0,0 +1,46 @@ +name: fixture_app +publish_to: 'none' +resolution: workspace + +environment: + sdk: ^3.7.0 + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_gen_runner: + path: __RUNNER_PATH__ + +dependency_overrides: + flutter_gen_core: + path: __CORE_PATH__ + +flutter_gen: + output: lib/gen/ + assets: + enabled: true + outputs: + class_name: FixtureAssets + package_parameter_enabled: false + style: dot-delimiter + exclude: [] + fonts: + enabled: true + outputs: + class_name: FixtureFonts + colors: + enabled: true + outputs: + class_name: FixtureColors + inputs: + - assets/color/colors.xml + +flutter: + assets: + - assets/images/ + fonts: + - family: Inter + fonts: + - asset: assets/fonts/Inter-Regular.ttf \ No newline at end of file diff --git a/packages/runner/test_fixtures/workspace/packages/unused/pubspec.yaml b/packages/runner/test_fixtures/workspace/packages/unused/pubspec.yaml new file mode 100644 index 000000000..23a354584 --- /dev/null +++ b/packages/runner/test_fixtures/workspace/packages/unused/pubspec.yaml @@ -0,0 +1,6 @@ +name: unused_package +publish_to: 'none' +resolution: workspace + +environment: + sdk: ^3.7.0 diff --git a/packages/runner/test_fixtures/workspace/pubspec.yaml b/packages/runner/test_fixtures/workspace/pubspec.yaml new file mode 100644 index 000000000..a1da41f13 --- /dev/null +++ b/packages/runner/test_fixtures/workspace/pubspec.yaml @@ -0,0 +1,12 @@ +name: workspace_root +publish_to: 'none' + +environment: + sdk: ^3.7.0 + +workspace: + - packages/app + - packages/unused + +dev_dependencies: + build_runner: ^2.12.2 From d96c489d60ed4e83b3e812f77586b8cd7b31bd07 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sat, 7 Mar 2026 21:03:03 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9C=A8=20Add=20command=20workspace=20sup?= =?UTF-8?q?port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + README.md | 20 +++ packages/command/bin/flutter_gen_command.dart | 87 +++++++++- packages/command/pubspec.yaml | 3 + .../test/flutter_gen_command_test.dart | 151 +++++++++++++++++- 5 files changed, 256 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a331724..a22f62600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ **Feature** - Add `build_runner --workspace` support for `flutter_gen_runner` using a manifest + post-process materialization pipeline. +- Add `fluttergen --workspace` support to the command package, including package-local `build.yaml` overrides for each workspace member. **Development** diff --git a/README.md b/README.md index 403428ef3..429842528 100644 --- a/README.md +++ b/README.md @@ -171,8 +171,28 @@ Run `fluttergen` after the configuration [`pubspec.yaml`](https://dart.dev/tools fluttergen -h fluttergen -c example/pubspec.yaml + +fluttergen --workspace -c pubspec.yaml ``` +Use `--workspace` to treat the config file as a workspace root pubspec and run +generation for each listed workspace member. Without `--workspace`, the command +generates for exactly one package. + +When `--workspace` is enabled, the root `pubspec.yaml` is used only to discover +workspace members from its `workspace:` section. FlutterGen then switches to +each member package and loads that package's own `pubspec.yaml` and, if +present, its local `build.yaml`. + +That means workspace-wide command execution behaves like this: + +- Root `pubspec.yaml`: selects which packages to visit. +- Member `pubspec.yaml`: provides `flutter` and `flutter_gen` configuration. +- Member `build.yaml`: overrides generator options for that member only. + +The `--build` option is only available in single-package mode. In workspace +mode, put any overrides in each package's local `build.yaml` instead. + ## Configuration file [FlutterGen] generates dart files based on the key **`flutter`** and **`flutter_gen`** of [`pubspec.yaml`](https://dart.dev/tools/pub/pubspec). diff --git a/packages/command/bin/flutter_gen_command.dart b/packages/command/bin/flutter_gen_command.dart index b8b9c5f91..5c39202bf 100644 --- a/packages/command/bin/flutter_gen_command.dart +++ b/packages/command/bin/flutter_gen_command.dart @@ -5,7 +5,11 @@ import 'package:flutter_gen_core/flutter_generator.dart'; import 'package:flutter_gen_core/utils/cast.dart' show safeCast; import 'package:flutter_gen_core/utils/log.dart' show log; import 'package:flutter_gen_core/version.gen.dart' show packageVersion; +import 'package:glob/glob.dart'; +import 'package:glob/list_local_fs.dart'; import 'package:logging/logging.dart' show Level; +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; void main(List args) async { log.onRecord.listen((record) { @@ -30,6 +34,13 @@ void main(List args) async { help: 'Set the path of build.yaml.', ); + parser.addFlag( + 'workspace', + abbr: 'w', + help: 'Generate for every workspace member listed in the config pubspec.', + defaultsTo: false, + ); + parser.addFlag( 'help', abbr: 'h', @@ -70,5 +81,79 @@ void main(List args) async { } final buildFile = buildPath == null ? null : File(buildPath).absolute; - await FlutterGenerator(pubspecFile, buildFile: buildFile).build(); + final workspace = results['workspace'] as bool; + if (workspace && buildFile != null) { + throw ArgumentError( + 'The --build option is not supported together with --workspace. ' + 'Use package-local build.yaml files inside each workspace member instead.', + 'build', + ); + } + + if (workspace) { + await _runWorkspace(pubspecFile); + return; + } + + await _runSinglePackage(pubspecFile, buildFile: buildFile); +} + +Future _runWorkspace(File workspacePubspecFile) async { + final workspacePubspecMap = + loadYaml(await workspacePubspecFile.readAsString()) as YamlMap?; + final entries = (workspacePubspecMap?['workspace'] as YamlList?) + ?.whereType() + .toList() ?? + const []; + + if (entries.isEmpty) { + throw ArgumentError( + 'No workspace members were found in ${workspacePubspecFile.path}.', + 'config', + ); + } + + final workspaceRoot = workspacePubspecFile.parent; + final packagePubspecs = {}; + for (final entry in entries) { + final glob = Glob(entry); + for (final entity in glob.listSync(root: workspaceRoot.path)) { + if (entity is! Directory) { + continue; + } + final pubspecFile = File(p.join(entity.path, 'pubspec.yaml')); + if (pubspecFile.existsSync()) { + packagePubspecs.add(pubspecFile.absolute); + } + } + } + + if (packagePubspecs.isEmpty) { + throw ArgumentError( + 'No workspace member pubspec.yaml files were found from ' + '${workspacePubspecFile.path}.', + 'config', + ); + } + + final orderedPubspecs = packagePubspecs.toList() + ..sort((a, b) => a.path.compareTo(b.path)); + for (final packagePubspec in orderedPubspecs) { + await _runSinglePackage(packagePubspec); + } +} + +Future _runSinglePackage( + File pubspecFile, { + File? buildFile, +}) async { + await FlutterGenerator( + pubspecFile, + buildFile: buildFile ?? _packageLocalBuildFile(pubspecFile), + ).build(); +} + +File? _packageLocalBuildFile(File pubspecFile) { + final buildFile = File(p.join(pubspecFile.parent.path, 'build.yaml')); + return buildFile.existsSync() ? buildFile : null; } diff --git a/packages/command/pubspec.yaml b/packages/command/pubspec.yaml index a16285bd0..00d52eef3 100644 --- a/packages/command/pubspec.yaml +++ b/packages/command/pubspec.yaml @@ -16,7 +16,10 @@ dependencies: flutter_gen_core: 5.13.0+1 args: ^2.0.0 + glob: ^2.1.3 logging: ^1.3.0 + path: ^1.9.1 + yaml: ^3.1.3 dev_dependencies: lints: any # Ignoring the version to allow editing across SDK versions. diff --git a/packages/command/test/flutter_gen_command_test.dart b/packages/command/test/flutter_gen_command_test.dart index ed5bdf6ec..735faf935 100644 --- a/packages/command/test/flutter_gen_command_test.dart +++ b/packages/command/test/flutter_gen_command_test.dart @@ -1,6 +1,7 @@ -import 'dart:io' show Platform; +import 'dart:io'; import 'package:flutter_gen_core/version.gen.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'package:test_process/test_process.dart'; @@ -40,12 +41,11 @@ void main() { await process.stdout.next, equals('[FlutterGen] Usage of the `fluttergen` command:'), ); - expect( - await process.stdout.next, - equals('-c, --config Set the path of pubspec.yaml.'), - ); + expect(await process.stdout.next, contains('--config')); final line = await process.stdout.next; expect(line.trim(), equals('(defaults to "pubspec.yaml")')); + expect(await process.stdout.next, contains('--build')); + expect(await process.stdout.next, contains('workspace')); await process.shouldExit(0); }); @@ -96,4 +96,145 @@ void main() { expect(rest, contains('package_parameter_enabled')); await process.shouldExit(255); }); + + test('Execute fluttergen --workspace', () async { + final workspaceDir = await _copyExampleWorkspace(); + addTearDown(() async { + if (workspaceDir.existsSync()) { + workspaceDir.deleteSync(recursive: true); + } + }); + + await _deleteGeneratedDirs(workspaceDir); + + final process = await TestProcess.start( + 'dart', + [ + 'run', + p.join(Directory.current.path, 'bin', 'flutter_gen_command.dart'), + '--workspace', + '--config', + p.join(workspaceDir.path, 'pubspec.yaml'), + ], + workingDirectory: workspaceDir.path, + ); + + await process.shouldExit(0); + + expect( + File( + p.join( + workspaceDir.path, + 'packages', + 'gallery_one', + 'lib', + 'gen', + 'assets.gen.dart', + ), + ).existsSync(), + isTrue, + ); + expect( + File( + p.join( + workspaceDir.path, + 'packages', + 'gallery_two', + 'lib', + 'gen', + 'assets.gen.dart', + ), + ).existsSync(), + isTrue, + ); + }); + + test('Execute fluttergen uses package-local build.yaml for config file', + () async { + final workspaceDir = await _copyExampleWorkspace(); + addTearDown(() async { + if (workspaceDir.existsSync()) { + workspaceDir.deleteSync(recursive: true); + } + }); + + final galleryOneDir = Directory( + p.join(workspaceDir.path, 'packages', 'gallery_one'), + ); + await _deleteGeneratedDirs(workspaceDir); + + File(p.join(galleryOneDir.path, 'build.yaml')).writeAsStringSync(r''' +targets: + $default: + builders: + flutter_gen_runner: + options: + output: lib/build_gen/ +'''); + + final process = await TestProcess.start( + 'dart', + [ + 'run', + p.join(Directory.current.path, 'bin', 'flutter_gen_command.dart'), + '--config', + p.join(galleryOneDir.path, 'pubspec.yaml'), + ], + workingDirectory: workspaceDir.path, + ); + + await process.shouldExit(0); + + expect( + File(p.join(galleryOneDir.path, 'lib', 'build_gen', 'assets.gen.dart')) + .existsSync(), + isTrue, + ); + expect( + File(p.join(galleryOneDir.path, 'lib', 'gen', 'assets.gen.dart')) + .existsSync(), + isFalse, + ); + }); +} + +Future _copyExampleWorkspace() async { + final source = Directory( + p.normalize( + p.join( + Directory.current.path, '..', '..', 'examples', 'example_workspace'), + ), + ); + final destination = await Directory.systemTemp.createTemp( + 'fluttergen_command_workspace_', + ); + await _copyDirectory(source, destination); + return destination; +} + +Future _deleteGeneratedDirs(Directory workspaceDir) async { + for (final relativePath in [ + p.join('packages', 'gallery_one', 'lib', 'gen'), + p.join('packages', 'gallery_one', 'lib', 'build_gen'), + p.join('packages', 'gallery_two', 'lib', 'gen'), + p.join('packages', 'gallery_two', 'lib', 'build_gen'), + ]) { + final directory = Directory(p.join(workspaceDir.path, relativePath)); + if (directory.existsSync()) { + directory.deleteSync(recursive: true); + } + } +} + +Future _copyDirectory(Directory source, Directory destination) async { + await for (final entity in source.list(recursive: true, followLinks: false)) { + final relativePath = p.relative(entity.path, from: source.path); + final targetPath = p.join(destination.path, relativePath); + if (entity is Directory) { + Directory(targetPath).createSync(recursive: true); + } else if (entity is File) { + File(targetPath).parent.createSync(recursive: true); + entity.copySync(targetPath); + } + } } From 6436f0f92723ba480232b68d982da7a06ac89df4 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sat, 7 Mar 2026 21:03:31 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=9A=80=20Add=20workspace=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 2 + examples/example_workspace/README.md | 36 ++++++ .../gallery_one/assets/images/flutter3.jpg | Bin 0 -> 24241 bytes .../packages/gallery_one/lib/gallery_one.dart | 3 + .../gallery_one/lib/gen/assets.gen.dart | 117 ++++++++++++++++++ .../packages/gallery_one/pubspec.yaml | 33 +++++ .../gallery_two/assets/images/dart.svg | 23 ++++ .../packages/gallery_two/lib/gallery_two.dart | 3 + .../gallery_two/lib/gen/assets.gen.dart | 26 ++++ .../packages/gallery_two/pubspec.yaml | 33 +++++ examples/example_workspace/pubspec.yaml | 12 ++ melos.yaml | 14 +++ 12 files changed, 302 insertions(+) create mode 100644 examples/example_workspace/README.md create mode 100644 examples/example_workspace/packages/gallery_one/assets/images/flutter3.jpg create mode 100644 examples/example_workspace/packages/gallery_one/lib/gallery_one.dart create mode 100644 examples/example_workspace/packages/gallery_one/lib/gen/assets.gen.dart create mode 100644 examples/example_workspace/packages/gallery_one/pubspec.yaml create mode 100644 examples/example_workspace/packages/gallery_two/assets/images/dart.svg create mode 100644 examples/example_workspace/packages/gallery_two/lib/gallery_two.dart create mode 100644 examples/example_workspace/packages/gallery_two/lib/gen/assets.gen.dart create mode 100644 examples/example_workspace/packages/gallery_two/pubspec.yaml create mode 100644 examples/example_workspace/pubspec.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 013d29425..0bc4e43ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,6 +61,8 @@ jobs: run: | melos run gen:examples:command --no-select melos run gen:examples:build_runner --no-select + melos run gen:examples:workspace:command --no-select + melos run gen:examples:workspace:build_runner --no-select dart format --set-exit-if-changed examples git --no-pager diff --exit-code examples diff --git a/examples/example_workspace/README.md b/examples/example_workspace/README.md new file mode 100644 index 000000000..6d4069e31 --- /dev/null +++ b/examples/example_workspace/README.md @@ -0,0 +1,36 @@ +# example_workspace + +A minimal pub workspace example for FlutterGen. + +## Workspace layout + +- `packages/gallery_one`: generates asset accessors for `flutter3.jpg` +- `packages/gallery_two`: generates asset accessors for `dart.svg` + +Both packages use `flutter_gen_runner` through `build_runner --workspace`. + +## Version requirements + +- Dart SDK: `>=3.7.0` +- `build_runner`: `>=2.12.0` + +FlutterGen's current `build_runner` integration relies on post-process builders +with `build_to: source`, which is only supported in `build_runner 2.12+`. + +## Getting started + +```sh +cd examples/example_workspace +flutter pub get +``` + +Generate all workspace members from the workspace root: + +```sh +dart run build_runner build --workspace --delete-conflicting-outputs +``` + +Generated files will be written to: + +- `packages/gallery_one/lib/gen/assets.gen.dart` +- `packages/gallery_two/lib/gen/assets.gen.dart` diff --git a/examples/example_workspace/packages/gallery_one/assets/images/flutter3.jpg b/examples/example_workspace/packages/gallery_one/assets/images/flutter3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cbe8ba87f88d5a94229204699106b28e1ee86b43 GIT binary patch literal 24241 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 z

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 KmG<)Q(*FQXkfQYf literal 0 HcmV?d00001 diff --git a/examples/example_workspace/packages/gallery_one/lib/gallery_one.dart b/examples/example_workspace/packages/gallery_one/lib/gallery_one.dart new file mode 100644 index 000000000..964b5e867 --- /dev/null +++ b/examples/example_workspace/packages/gallery_one/lib/gallery_one.dart @@ -0,0 +1,3 @@ +library; + +export 'gen/assets.gen.dart'; diff --git a/examples/example_workspace/packages/gallery_one/lib/gen/assets.gen.dart b/examples/example_workspace/packages/gallery_one/lib/gen/assets.gen.dart new file mode 100644 index 000000000..980d210c7 --- /dev/null +++ b/examples/example_workspace/packages/gallery_one/lib/gen/assets.gen.dart @@ -0,0 +1,117 @@ +// dart format width=80 + +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: deprecated_member_use,directives_ordering,implicit_dynamic_list_literal,unnecessary_import + +import 'package:flutter/widgets.dart'; + +class $AssetsImagesGen { + const $AssetsImagesGen(); + + /// File path: assets/images/flutter3.jpg + AssetGenImage get flutter3 => + const AssetGenImage('assets/images/flutter3.jpg'); + + /// List of all assets + List get values => [flutter3]; +} + +class GalleryOneAssets { + const GalleryOneAssets._(); + + static const $AssetsImagesGen images = $AssetsImagesGen(); +} + +class AssetGenImage { + const AssetGenImage( + this._assetName, { + this.size, + this.flavors = const {}, + this.animation, + }); + + final String _assetName; + + final Size? size; + final Set flavors; + final AssetGenImageAnimation? animation; + + Image image({ + Key? key, + AssetBundle? bundle, + ImageFrameBuilder? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? scale, + double? width, + double? height, + Color? color, + Animation? opacity, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = true, + bool isAntiAlias = false, + String? package, + FilterQuality filterQuality = FilterQuality.medium, + int? cacheWidth, + int? cacheHeight, + }) { + return Image.asset( + _assetName, + key: key, + bundle: bundle, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + scale: scale, + width: width, + height: height, + color: color, + opacity: opacity, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + package: package, + filterQuality: filterQuality, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + ); + } + + ImageProvider provider({AssetBundle? bundle, String? package}) { + return AssetImage(_assetName, bundle: bundle, package: package); + } + + String get path => _assetName; + + String get keyName => _assetName; +} + +class AssetGenImageAnimation { + const AssetGenImageAnimation({ + required this.isAnimation, + required this.duration, + required this.frames, + }); + + final bool isAnimation; + final Duration duration; + final int frames; +} diff --git a/examples/example_workspace/packages/gallery_one/pubspec.yaml b/examples/example_workspace/packages/gallery_one/pubspec.yaml new file mode 100644 index 000000000..d58756868 --- /dev/null +++ b/examples/example_workspace/packages/gallery_one/pubspec.yaml @@ -0,0 +1,33 @@ +name: gallery_one +publish_to: 'none' +resolution: workspace + +environment: + sdk: ^3.7.0 + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + build_runner: ^2.12.2 + flutter_gen_runner: any + +flutter_gen: + output: lib/gen/ + assets: + enabled: true + outputs: + class_name: GalleryOneAssets + package_parameter_enabled: false + style: dot-delimiter + exclude: [] + fonts: + enabled: false + colors: + enabled: false + inputs: [] + +flutter: + assets: + - assets/images/ diff --git a/examples/example_workspace/packages/gallery_two/assets/images/dart.svg b/examples/example_workspace/packages/gallery_two/assets/images/dart.svg new file mode 100644 index 000000000..b9ddcdfaa --- /dev/null +++ b/examples/example_workspace/packages/gallery_two/assets/images/dart.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/examples/example_workspace/packages/gallery_two/lib/gallery_two.dart b/examples/example_workspace/packages/gallery_two/lib/gallery_two.dart new file mode 100644 index 000000000..964b5e867 --- /dev/null +++ b/examples/example_workspace/packages/gallery_two/lib/gallery_two.dart @@ -0,0 +1,3 @@ +library; + +export 'gen/assets.gen.dart'; diff --git a/examples/example_workspace/packages/gallery_two/lib/gen/assets.gen.dart b/examples/example_workspace/packages/gallery_two/lib/gen/assets.gen.dart new file mode 100644 index 000000000..006c08334 --- /dev/null +++ b/examples/example_workspace/packages/gallery_two/lib/gen/assets.gen.dart @@ -0,0 +1,26 @@ +// dart format width=80 + +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: deprecated_member_use,directives_ordering,implicit_dynamic_list_literal,unnecessary_import + +class $AssetsImagesGen { + const $AssetsImagesGen(); + + /// File path: assets/images/dart.svg + String get dart => 'assets/images/dart.svg'; + + /// List of all assets + List get values => [dart]; +} + +class GalleryTwoAssets { + const GalleryTwoAssets._(); + + static const $AssetsImagesGen images = $AssetsImagesGen(); +} diff --git a/examples/example_workspace/packages/gallery_two/pubspec.yaml b/examples/example_workspace/packages/gallery_two/pubspec.yaml new file mode 100644 index 000000000..aec1a448e --- /dev/null +++ b/examples/example_workspace/packages/gallery_two/pubspec.yaml @@ -0,0 +1,33 @@ +name: gallery_two +publish_to: 'none' +resolution: workspace + +environment: + sdk: ^3.7.0 + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + build_runner: ^2.12.2 + flutter_gen_runner: any + +flutter_gen: + output: lib/gen/ + assets: + enabled: true + outputs: + class_name: GalleryTwoAssets + package_parameter_enabled: false + style: dot-delimiter + exclude: [] + fonts: + enabled: false + colors: + enabled: false + inputs: [] + +flutter: + assets: + - assets/images/ diff --git a/examples/example_workspace/pubspec.yaml b/examples/example_workspace/pubspec.yaml new file mode 100644 index 000000000..26fe470f7 --- /dev/null +++ b/examples/example_workspace/pubspec.yaml @@ -0,0 +1,12 @@ +name: example_workspace_root +publish_to: 'none' + +environment: + sdk: ^3.7.0 + +workspace: + - packages/gallery_one + - packages/gallery_two + +dev_dependencies: + build_runner: ^2.12.2 diff --git a/melos.yaml b/melos.yaml index caf2d9126..0c0fb5dfe 100644 --- a/melos.yaml +++ b/melos.yaml @@ -6,6 +6,7 @@ packages: - packages/runner - examples/example - examples/example_resources + - examples/example_workspace ide: intellij: @@ -48,6 +49,7 @@ scripts: ignore: - example - example_resources + - example_workspace_root dependsOn: build_runner description: dart run build_runner build --delete-conflicting-outputs @@ -69,6 +71,18 @@ scripts: - example_resources description: dart run build_runner build --delete-conflicting-outputs + gen:examples:workspace:command: + exec: dart ../../packages/command/bin/flutter_gen_command.dart --workspace -c pubspec.yaml + packageFilters: + scope: example_workspace_root + description: dart ../../packages/command/bin/flutter_gen_command.dart --workspace -c pubspec.yaml + + gen:examples:workspace:build_runner: + exec: dart run build_runner build --workspace -d + packageFilters: + scope: example_workspace_root + description: dart run build_runner build --workspace --delete-conflicting-outputs + gen:actual_data: run: dart packages/core/scripts/generate_facts.dart description: Generate actual data for tests. From 8cd3cfb9484419a63e1d1137ec03ccf582d5fac3 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sat, 7 Mar 2026 21:05:10 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=99=88=20Ignores=20lock=20files=20for?= =?UTF-8?q?=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.prettierignore b/.prettierignore index fb523e6ee..d2581e85e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,6 @@ +pnpm-lock.yaml +**/pubspec.lock + .dart_tool/ build/ packages/**/android/ From aa104cfc18757f1902800ddad74fc2bc24e9e3a9 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sat, 7 Mar 2026 21:22:02 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=8E=A8=20Try=20to=20fix=20formats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/example_workspace/packages/gallery_one/pubspec.yaml | 2 ++ examples/example_workspace/packages/gallery_two/pubspec.yaml | 2 ++ examples/example_workspace/pubspec.yaml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/examples/example_workspace/packages/gallery_one/pubspec.yaml b/examples/example_workspace/packages/gallery_one/pubspec.yaml index d58756868..710f8d3b9 100644 --- a/examples/example_workspace/packages/gallery_one/pubspec.yaml +++ b/examples/example_workspace/packages/gallery_one/pubspec.yaml @@ -10,6 +10,8 @@ dependencies: sdk: flutter dev_dependencies: + lints: any + build_runner: ^2.12.2 flutter_gen_runner: any diff --git a/examples/example_workspace/packages/gallery_two/pubspec.yaml b/examples/example_workspace/packages/gallery_two/pubspec.yaml index aec1a448e..0853955c1 100644 --- a/examples/example_workspace/packages/gallery_two/pubspec.yaml +++ b/examples/example_workspace/packages/gallery_two/pubspec.yaml @@ -10,6 +10,8 @@ dependencies: sdk: flutter dev_dependencies: + lints: any + build_runner: ^2.12.2 flutter_gen_runner: any diff --git a/examples/example_workspace/pubspec.yaml b/examples/example_workspace/pubspec.yaml index 26fe470f7..856766a0c 100644 --- a/examples/example_workspace/pubspec.yaml +++ b/examples/example_workspace/pubspec.yaml @@ -9,4 +9,6 @@ workspace: - packages/gallery_two dev_dependencies: + lints: any + build_runner: ^2.12.2 From 8dc70b88509e02833a2757607cee1c103ed2864b Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sat, 7 Mar 2026 22:06:04 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=90=9B=20Improves=20files=20recreatio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 11 ++++- .idea/modules.xml | 1 + .../example_workspace_root.iml | 16 ++++++++ .../packages/gallery_one/pubspec.yaml | 10 +---- .../packages/gallery_two/pubspec.yaml | 10 +---- examples/example_workspace/pubspec.yaml | 6 +++ melos.yaml | 4 +- .../test/flutter_gen_command_test.dart | 7 +++- packages/runner/lib/flutter_gen_runner.dart | 15 +++++-- .../runner/test/workspace_build_test.dart | 40 +++++++++++++++++++ 10 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 examples/example_workspace/example_workspace_root.iml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0bc4e43ee..9037ef35c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,10 +58,19 @@ jobs: files: ./packages/core/coverage.lcov - name: Generate example then check formats and changes - run: | + run: | # Run format and diff for each gen step. melos run gen:examples:command --no-select + dart format --set-exit-if-changed examples + git --no-pager diff --exit-code examples + melos run gen:examples:build_runner --no-select + dart format --set-exit-if-changed examples + git --no-pager diff --exit-code examples + melos run gen:examples:workspace:command --no-select + dart format --set-exit-if-changed examples + git --no-pager diff --exit-code examples + melos run gen:examples:workspace:build_runner --no-select dart format --set-exit-if-changed examples git --no-pager diff --exit-code examples diff --git a/.idea/modules.xml b/.idea/modules.xml index 76605f58a..a0546d877 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -4,6 +4,7 @@ + diff --git a/examples/example_workspace/example_workspace_root.iml b/examples/example_workspace/example_workspace_root.iml new file mode 100644 index 000000000..389d07a14 --- /dev/null +++ b/examples/example_workspace/example_workspace_root.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/example_workspace/packages/gallery_one/pubspec.yaml b/examples/example_workspace/packages/gallery_one/pubspec.yaml index 710f8d3b9..b9d496ac3 100644 --- a/examples/example_workspace/packages/gallery_one/pubspec.yaml +++ b/examples/example_workspace/packages/gallery_one/pubspec.yaml @@ -13,7 +13,7 @@ dev_dependencies: lints: any build_runner: ^2.12.2 - flutter_gen_runner: any + flutter_gen_runner: # overrode flutter_gen: output: lib/gen/ @@ -21,14 +21,6 @@ flutter_gen: enabled: true outputs: class_name: GalleryOneAssets - package_parameter_enabled: false - style: dot-delimiter - exclude: [] - fonts: - enabled: false - colors: - enabled: false - inputs: [] flutter: assets: diff --git a/examples/example_workspace/packages/gallery_two/pubspec.yaml b/examples/example_workspace/packages/gallery_two/pubspec.yaml index 0853955c1..b9f790e02 100644 --- a/examples/example_workspace/packages/gallery_two/pubspec.yaml +++ b/examples/example_workspace/packages/gallery_two/pubspec.yaml @@ -13,7 +13,7 @@ dev_dependencies: lints: any build_runner: ^2.12.2 - flutter_gen_runner: any + flutter_gen_runner: # overrode flutter_gen: output: lib/gen/ @@ -21,14 +21,6 @@ flutter_gen: enabled: true outputs: class_name: GalleryTwoAssets - package_parameter_enabled: false - style: dot-delimiter - exclude: [] - fonts: - enabled: false - colors: - enabled: false - inputs: [] flutter: assets: diff --git a/examples/example_workspace/pubspec.yaml b/examples/example_workspace/pubspec.yaml index 856766a0c..4a4330fec 100644 --- a/examples/example_workspace/pubspec.yaml +++ b/examples/example_workspace/pubspec.yaml @@ -12,3 +12,9 @@ dev_dependencies: lints: any build_runner: ^2.12.2 + +dependency_overrides: + flutter_gen_core: + path: ../../packages/core + flutter_gen_runner: + path: ../../packages/runner diff --git a/melos.yaml b/melos.yaml index 0c0fb5dfe..1ea5900b2 100644 --- a/melos.yaml +++ b/melos.yaml @@ -62,7 +62,7 @@ scripts: description: dart ../../packages/command/bin/flutter_gen_command.dart gen:examples:build_runner: - run: dart run build_runner build -d + run: dart run build_runner clean; dart run build_runner build -d exec: concurrency: 1 packageFilters: @@ -78,7 +78,7 @@ scripts: description: dart ../../packages/command/bin/flutter_gen_command.dart --workspace -c pubspec.yaml gen:examples:workspace:build_runner: - exec: dart run build_runner build --workspace -d + exec: dart run build_runner clean; dart run build_runner build --workspace -d packageFilters: scope: example_workspace_root description: dart run build_runner build --workspace --delete-conflicting-outputs diff --git a/packages/command/test/flutter_gen_command_test.dart b/packages/command/test/flutter_gen_command_test.dart index 735faf935..14b4437de 100644 --- a/packages/command/test/flutter_gen_command_test.dart +++ b/packages/command/test/flutter_gen_command_test.dart @@ -202,7 +202,12 @@ Future _copyExampleWorkspace() async { final source = Directory( p.normalize( p.join( - Directory.current.path, '..', '..', 'examples', 'example_workspace'), + Directory.current.path, + '..', + '..', + 'examples', + 'example_workspace', + ), ), ); final destination = await Directory.systemTemp.createTemp( diff --git a/packages/runner/lib/flutter_gen_runner.dart b/packages/runner/lib/flutter_gen_runner.dart index fa7b426ff..0ed855310 100644 --- a/packages/runner/lib/flutter_gen_runner.dart +++ b/packages/runner/lib/flutter_gen_runner.dart @@ -307,11 +307,18 @@ class FlutterGenPostProcessBuilder extends PostProcessBuilder { } // Materialize the exact output set described by the manifest. + // + // These files are intentionally managed outside build_runner's declared + // output model because their paths are configuration-dependent. Writing them + // directly avoids `InvalidOutputException` when the same files already + // exist, for example after a previous `fluttergen` command run or a stale + // checked-out generated file. for (final output in manifest.outputs) { - await buildStep.writeAsString( - AssetId(manifest.packageName, output.path), - output.contents, - ); + final file = File(join(manifest.packageRoot, output.path)); + if (!file.parent.existsSync()) { + file.parent.createSync(recursive: true); + } + file.writeAsStringSync(output.contents); } if (!ownerFile.parent.existsSync()) { diff --git a/packages/runner/test/workspace_build_test.dart b/packages/runner/test/workspace_build_test.dart index 5d67cc332..cb9b00b54 100644 --- a/packages/runner/test/workspace_build_test.dart +++ b/packages/runner/test/workspace_build_test.dart @@ -185,6 +185,46 @@ targets: isFalse, ); }); + + test('overwrites existing generated files in workspace mode', () async { + final workspaceDir = await _createWorkspaceFixture(); + addTearDown(() async { + if (workspaceDir.existsSync()) { + workspaceDir.deleteSync(recursive: true); + } + }); + + final appDir = Directory(p.join(workspaceDir.path, 'packages', 'app')); + final generatedFile = File( + p.join(appDir.path, 'lib', 'gen', 'assets.gen.dart'), + ); + generatedFile.parent.createSync(recursive: true); + generatedFile.writeAsStringSync('// stale contents\n'); + + await _runProcess( + 'flutter', + ['pub', 'get'], + workingDirectory: workspaceDir.path, + ); + + await _runProcess( + 'dart', + [ + 'run', + 'build_runner', + 'build', + '--workspace', + '--delete-conflicting-outputs', + ], + workingDirectory: workspaceDir.path, + ); + + expect(generatedFile.existsSync(), isTrue); + expect( + generatedFile.readAsStringSync(), + isNot(contains('// stale contents')), + ); + }); } Future _createWorkspaceFixture() async { From c2d2e2b0a77b19e0f74e41e17199a9f9c1cbe70c Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sat, 7 Mar 2026 22:40:41 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=93=9D=20Document=20clean=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + README.md | 6 ++++++ examples/example_workspace/README.md | 8 +++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a22f62600..6db9ff59b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ **Development** - Document and align the minimum supported versions for the new build pipeline: Dart `>=3.7.0` and `build_runner >=2.12.0`. +- Document the current `build_runner --workspace` rebuild limitation for manually deleted generated files and recommend `build_runner clean` before rebuilding. ## 5.13.0+1 diff --git a/README.md b/README.md index 429842528..f232c15a5 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,12 @@ FlutterGen will resolve each package from the active build target instead of the process working directory, so package-local `pubspec.yaml` configuration and output paths continue to work in workspace builds. +If generated source files were removed manually while `.dart_tool/build` is +still present, run `dart run build_runner clean` from the workspace root before +running `build --workspace` again. The current post-process builder flow can +re-materialize files reliably after a clean build, but a warm incremental build +may skip unchanged manifests. + For workspace builds, use Dart `>=3.7.0` together with `build_runner >=2.12.0`. ### Pub Global diff --git a/examples/example_workspace/README.md b/examples/example_workspace/README.md index 6d4069e31..bed37ba8f 100644 --- a/examples/example_workspace/README.md +++ b/examples/example_workspace/README.md @@ -27,9 +27,15 @@ flutter pub get Generate all workspace members from the workspace root: ```sh -dart run build_runner build --workspace --delete-conflicting-outputs +dart run build_runner clean +dart run build_runner build --workspace ``` +The explicit `clean` step is recommended if generated files were deleted +manually. With the current `build_runner` post-process builder model, a warm +incremental build may skip unchanged manifests and therefore not recreate +missing generated source files on its own. + Generated files will be written to: - `packages/gallery_one/lib/gen/assets.gen.dart` From 0ecb24a75789a47b0d689873e1f4d16075926c8b Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sat, 7 Mar 2026 22:41:18 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E2=9C=85=20Add=20test=20for=20config=20loa?= =?UTF-8?q?ding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/test/config_test.dart | 240 ++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/packages/core/test/config_test.dart b/packages/core/test/config_test.dart index fb375f1d9..da7dc135a 100644 --- a/packages/core/test/config_test.dart +++ b/packages/core/test/config_test.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:flutter_gen_core/generators/integrations/lottie_integration.dart'; @@ -5,6 +6,7 @@ import 'package:flutter_gen_core/generators/integrations/rive_integration.dart'; import 'package:flutter_gen_core/generators/integrations/svg_integration.dart'; import 'package:flutter_gen_core/generators/registry.dart'; import 'package:flutter_gen_core/settings/config.dart'; +import 'package:flutter_gen_core/utils/error.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; @@ -228,4 +230,242 @@ sdks: } }); }); + + group('ConfigLoadInput', () { + test('ignores a missing explicit build file', () { + final tempDir = Directory.systemTemp.createTempSync('flutter_gen_test'); + try { + final tempPubspec = File('${tempDir.path}/pubspec.yaml'); + tempPubspec.writeAsStringSync(''' +name: missing_build_file_test +environment: + sdk: ^3.7.0 +flutter_gen: + output: lib/gen/ +flutter: + assets: + - assets/ +'''); + + final config = loadPubspecConfig( + tempPubspec, + buildFile: File('${tempDir.path}/missing_build.yaml'), + ); + + expect(config.pubspec.flutterGen.output, equals('lib/gen/')); + } finally { + tempDir.deleteSync(recursive: true); + } + }); + + test('merges builder options and analysis options from direct input', () { + final config = loadPubspecConfigFromInput( + ConfigLoadInput( + pubspecFile: File('/virtual/pkg/pubspec.yaml'), + pubspecContent: ''' +name: input_test +environment: + sdk: ^3.7.0 +dependencies: + rive: ^0.13.0 +flutter_gen: + output: lib/gen/ + integrations: + rive: true +flutter: + assets: + - assets/images/ +''', + buildOptions: { + 'output': 'lib/build_gen/', + }, + pubspecLockContent: ''' +packages: + rive: + version: "0.13.5" +sdks: + dart: ">=3.7.0 <4.0.0" +''', + analysisOptionsContent: ''' +formatter: + page_width: 120 +''', + ), + ); + + expect(config.pubspec.flutterGen.output, equals('lib/build_gen/')); + expect(config.formatterPageWidth, equals(120)); + expect( + config.integrationResolvedVersions[RiveIntegration], + equals(Version(0, 13, 5)), + ); + expect( + config.integrationVersionConstraints[RiveIntegration], + equals(VersionConstraint.parse('^0.13.0')), + ); + expect(config.sdkConstraint, equals(VersionConstraint.parse('^3.7.0'))); + }); + + test('falls back to pubspec.lock sdk when pubspec omits sdk', () { + final config = loadPubspecConfigFromInput( + ConfigLoadInput( + pubspecFile: File('/virtual/pkg/pubspec.yaml'), + pubspecContent: ''' +name: lock_sdk_test +flutter_gen: + output: lib/gen/ +flutter: + assets: + - assets/images/ +''', + pubspecLockContent: ''' +sdks: + dart: ">=3.6.0 <4.0.0" +''', + ), + ); + + expect( + config.sdkConstraint, + equals(VersionConstraint.parse('>=3.6.0 <4.0.0')), + ); + }); + + test('ignores empty builder options in direct input', () { + final config = loadPubspecConfigFromInput( + ConfigLoadInput( + pubspecFile: File('/virtual/pkg/pubspec.yaml'), + pubspecContent: ''' +name: empty_build_options_test +flutter: + assets: + - assets/ +flutter_gen: + output: lib/gen/ +''', + buildOptions: const {}, + ), + ); + + expect(config.pubspec.flutterGen.output, equals('lib/gen/')); + }); + + test('returns null for invalid json mapping in direct input', () { + final config = loadPubspecConfigFromInputOrNull( + ConfigLoadInput( + pubspecFile: File('/virtual/pkg/pubspec.yaml'), + pubspecContent: ''' +name: invalid_json_test +environment: + dart: ^3.7.0 +flutter_gen: + output: lib/gen/ +flutter: + assets: + - assets/ +''', + ), + ); + + expect(config, isNull); + }); + + test( + 'returns null when direct input path access throws FileSystemException', + () { + final config = loadPubspecConfigFromInputOrNull( + ConfigLoadInput( + pubspecFile: _ThrowingFile.parent( + path: '/virtual/pkg/pubspec.yaml', + error: const FileSystemException('boom'), + ), + pubspecContent: ''' +name: file_system_error_test +flutter_gen: + output: lib/gen/ +flutter: + assets: + - assets/ +''', + ), + ); + + expect(config, isNull); + }); + + test( + 'returns null when direct input path access throws InvalidSettingsException', + () { + final config = loadPubspecConfigFromInputOrNull( + ConfigLoadInput( + pubspecFile: _ThrowingFile.parent( + path: '/virtual/pkg/pubspec.yaml', + error: const InvalidSettingsException('boom'), + ), + pubspecContent: ''' +name: invalid_settings_direct_test +flutter_gen: + output: lib/gen/ +flutter: + assets: + - assets/ +''', + ), + ); + + expect(config, isNull); + }); + }); + + group('loadPubspecConfigOrNull', () { + test('returns null when file reading throws InvalidSettingsException', () { + final config = loadPubspecConfigOrNull( + _ThrowingFile.read( + path: '/virtual/pkg/pubspec.yaml', + error: const InvalidSettingsException('boom'), + ), + ); + + expect(config, isNull); + }); + }); +} + +class _ThrowingFile implements File { + _ThrowingFile.read({ + required this.path, + required Object error, + }) : _readError = error, + _parentError = null; + + _ThrowingFile.parent({ + required this.path, + required Object error, + }) : _parentError = error, + _readError = null; + + final Object? _readError; + final Object? _parentError; + + @override + final String path; + + @override + Directory get parent { + if (_parentError case final Object error) { + throw error; + } + return Directory(Uri.file(path).resolve('.').toFilePath()); + } + + @override + String readAsStringSync({Encoding encoding = utf8}) { + if (_readError case final Object error) { + throw error; + } + return ''; + } + + @override + dynamic noSuchMethod(Invocation invocation) => null; }