diff --git a/packages/material_ui/pubspec.yaml b/packages/material_ui/pubspec.yaml index 70489e1f2f61..e3a8b6073409 100644 --- a/packages/material_ui/pubspec.yaml +++ b/packages/material_ui/pubspec.yaml @@ -11,6 +11,7 @@ environment: workspace: - example + - tool/gen_defaults dependencies: cupertino_ui: diff --git a/packages/material_ui/tool/gen_defaults/README.md b/packages/material_ui/tool/gen_defaults/README.md new file mode 100644 index 000000000000..56581126a8ca --- /dev/null +++ b/packages/material_ui/tool/gen_defaults/README.md @@ -0,0 +1,24 @@ +## Token Defaults Generator + +Script that generates component theme data defaults based on token data. + +## Usage +Run this program from the root of the git repository: +```sh +dart packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart [-v] +``` + +This updates generated component theming files under +`packages/material_ui/lib/src/generated`. + +## Templates + +There is a template file for every component that needs defaults from the token +database. These templates are implemented as subclasses of either `M3TokenTemplate` or `M3ETokenTemplate`. + +Templates need to override the `generateContents` method to provide the +generated code block as a string. + +## Tokens + +Tokens are stored in `data/`, and are sourced from an internal Google database. \ No newline at end of file diff --git a/packages/material_ui/tool/gen_defaults/analysis_options.yaml b/packages/material_ui/tool/gen_defaults/analysis_options.yaml new file mode 100644 index 000000000000..1fb98de01a0b --- /dev/null +++ b/packages/material_ui/tool/gen_defaults/analysis_options.yaml @@ -0,0 +1,4 @@ +include: ../../../../analysis_options.yaml + +formatter: + page_width: 100 diff --git a/packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart b/packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart new file mode 100644 index 000000000000..2da295bc4e99 --- /dev/null +++ b/packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart @@ -0,0 +1,28 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// ## Usage +// +// Run from the root of flutter/packages: +// +// ``` +// dart packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart [-v] +// ``` + +import 'package:args/args.dart'; + +// TODO(elliette): Import template files. +// import '../templates/x_template.dart'; + +Future main(List args) async { + // Parse arguments + final parser = ArgParser(); + parser.addFlag('verbose', abbr: 'v', help: 'Enable verbose output', negatable: false); + final ArgResults argResults = parser.parse(args); + // TODO(elliette): Add token logger when verbose flag is used. + // ignore: unused_local_variable + final verbose = argResults['verbose'] as bool; + // TODO(elliette): Invoke template generators. + // const XTemplate().generateFile(verbose: verbose); +} diff --git a/packages/material_ui/tool/gen_defaults/pubspec.yaml b/packages/material_ui/tool/gen_defaults/pubspec.yaml new file mode 100644 index 000000000000..6ab022d87c3d --- /dev/null +++ b/packages/material_ui/tool/gen_defaults/pubspec.yaml @@ -0,0 +1,17 @@ +name: gen_defaults +description: A command line script to generate Material component defaults from the token database. +publish_to: none +version: 1.0.0 + +environment: + sdk: ^3.10.0-0 + +resolution: workspace + +dependencies: + args: any + meta: any + +dev_dependencies: + path: any + test: any diff --git a/packages/material_ui/tool/gen_defaults/templates/template.dart b/packages/material_ui/tool/gen_defaults/templates/template.dart new file mode 100644 index 000000000000..e10ffca26693 --- /dev/null +++ b/packages/material_ui/tool/gen_defaults/templates/template.dart @@ -0,0 +1,111 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:meta/meta.dart'; + +enum _MaterialVersion { material3, material3Expressive } + +abstract class M3TokenTemplate extends _TokenTemplate { + const M3TokenTemplate(); + + @override + _MaterialVersion get _version => _MaterialVersion.material3; +} + +abstract class M3ETokenTemplate extends _TokenTemplate { + const M3ETokenTemplate(); + + @override + _MaterialVersion get _version => _MaterialVersion.material3Expressive; +} + +abstract class _TokenTemplate { + const _TokenTemplate(); + + static const String copyrightHeader = ''' +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +'''; + + static const String headerComment = ''' + +// Do not edit by hand. The code is generated from data in the Material +// Design token database by the script: +// packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart. +'''; + + /// The Material version this template is for. + _MaterialVersion get _version; + + /// The name of the template, which corresponds to the target file name. + /// E.g., 'typography' for generating 'typography_defaults.g.dart'. + String get name; + + @visibleForTesting + String get materialLib { + const packagePath = 'packages/material_ui'; + const generatedDirectory = 'lib/src/generated'; + final String relativeOutputPath = switch (_version) { + _MaterialVersion.material3 => generatedDirectory, + _MaterialVersion.material3Expressive => '$generatedDirectory/material_3_expressive', + }; + if (Directory(packagePath).existsSync()) { + return '$packagePath/$relativeOutputPath'; + } + return relativeOutputPath; + } + + String generateContents(); + + void generateFile({bool verbose = false}) { + final fileName = '$materialLib/${name}_defaults.g.dart'; + if (verbose) { + stdout.writeln('Generating file: $fileName'); + stdout.writeln('Target parent file name: $name.dart'); + } + final file = File(fileName); + if (!file.existsSync()) { + if (verbose) { + stdout.writeln('File does not exist, creating it.'); + } + file.createSync(recursive: true); + } + + final parentName = '$name.dart'; + + if (verbose) { + stdout.writeln('Generating contents...'); + } + final buffer = StringBuffer(); + buffer.write(copyrightHeader); + buffer.write(headerComment); + final String partOfPath = switch (_version) { + _MaterialVersion.material3 => '../$parentName', + _MaterialVersion.material3Expressive => '../../material_3_expressive/$parentName', + }; + buffer.write("part of '$partOfPath';\n\n"); + buffer.write(generateContents()); + + if (verbose) { + stdout.writeln('Writing generated contents to $fileName...'); + } + file.writeAsStringSync(buffer.toString()); + if (verbose) { + stdout.writeln('Formatting $fileName...'); + } + final ProcessResult result = Process.runSync(Platform.resolvedExecutable, [ + 'format', + fileName, + ]); + if (result.exitCode != 0) { + stderr.writeln('Failed to format $fileName: ${result.stderr}'); + } + if (verbose) { + stdout.writeln('Done generating $fileName.'); + } + } +} diff --git a/packages/material_ui/tool/gen_defaults/test/gen_defaults_test.dart b/packages/material_ui/tool/gen_defaults/test/gen_defaults_test.dart new file mode 100644 index 000000000000..91053fcd959d --- /dev/null +++ b/packages/material_ui/tool/gen_defaults/test/gen_defaults_test.dart @@ -0,0 +1,112 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:test/test.dart'; +import 'test_fixtures/test_templates.dart'; + +void main() { + Directory? tempDir; + String testPath() => tempDir!.path; + + group('gen_defaults templates', () { + setUp(() { + tempDir = Directory.systemTemp.createTempSync('gen_defaults'); + }); + + tearDown(() { + tempDir!.deleteSync(recursive: true); + }); + + test('will generate a part file ending in _defaults.g.dart', () { + final template = ButtonTemplate(testPath()); + template.generateFile(verbose: true); + + final file = File('${testPath()}/button_defaults.g.dart'); + expect(file.existsSync(), isTrue); + }); + + test('will generate a file with the correct header text', () { + final template = ButtonTemplate(testPath()); + template.generateFile(); + + final file = File('${testPath()}/button_defaults.g.dart'); + final String fileContents = file.readAsStringSync(); + expect(fileContents, contains(_fileHeader)); + }); + + test('will generate a file with the expected contents', () { + final template = ButtonTemplate(testPath()); + template.generateFile(); + + final file = File('${testPath()}/button_defaults.g.dart'); + final String fileContents = file.readAsStringSync(); + expect(fileContents, contains(_buttonDefaultsClass)); + }); + + test('will completely overwrite any previous code', () { + final file = File('${testPath()}/button_defaults.g.dart'); + const randomText = 'Pre-existing random text.'; + file.writeAsStringSync(randomText); + + final template = ButtonTemplate(testPath()); + template.generateFile(); + final String fileContents = file.readAsStringSync(); + expect(fileContents, isNot(contains(randomText))); + expect(fileContents, contains(_buttonDefaultsClass)); + }); + + test('will run dart format over the generated file', () { + final template = UnformattedTemplate(testPath()); + template.generateFile(); + + final file = File('${testPath()}/unformatted_defaults.g.dart'); + expect(file.readAsStringSync(), contains(formattedClass)); + }); + + test('materialLib path resolves correctly based on MaterialVersion', () { + final m3Template = TestM3Template(); + final m3ExpressiveTemplate = TestM3ExpressiveTemplate(); + const materialUiDir = 'packages/material_ui'; + const generatedDir = 'lib/src/generated'; + + final bool hasPackageDir = Directory(materialUiDir).existsSync(); + if (hasPackageDir) { + expect(m3Template.materialLib, '$materialUiDir/$generatedDir'); + expect( + m3ExpressiveTemplate.materialLib, + '$materialUiDir/$generatedDir/material_3_expressive', + ); + } else { + expect(m3Template.materialLib, generatedDir); + expect(m3ExpressiveTemplate.materialLib, '$generatedDir/material_3_expressive'); + } + }); + }); +} + +const _fileHeader = ''' +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Do not edit by hand. The code is generated from data in the Material +// Design token database by the script: +// packages/material_ui/tool/gen_defaults/bin/gen_defaults.dart. +'''; + +const _buttonDefaultsClass = ''' +class _ButtonDefaults { + static const double height = 40.0; + static const double borderRadius = 8.0; +} +'''; + +const formattedClass = ''' +class UnformattedClass { + final int x = 1; + final String y = 'hello'; +} +'''; diff --git a/packages/material_ui/tool/gen_defaults/test/test_fixtures/button_token_data.dart b/packages/material_ui/tool/gen_defaults/test/test_fixtures/button_token_data.dart new file mode 100644 index 000000000000..dbebb961a1e7 --- /dev/null +++ b/packages/material_ui/tool/gen_defaults/test/test_fixtures/button_token_data.dart @@ -0,0 +1,8 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +class TokenButton { + static const double height = 40.0; + static const double borderRadius = 8.0; +} diff --git a/packages/material_ui/tool/gen_defaults/test/test_fixtures/test_templates.dart b/packages/material_ui/tool/gen_defaults/test/test_fixtures/test_templates.dart new file mode 100644 index 000000000000..e56d0c8ef85d --- /dev/null +++ b/packages/material_ui/tool/gen_defaults/test/test_fixtures/test_templates.dart @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../templates/template.dart'; +import 'button_token_data.dart'; + +class ButtonTemplate extends M3ETokenTemplate { + ButtonTemplate(this.customMaterialLib); + + final String customMaterialLib; + + @override + String get name => 'button'; + + @override + String get materialLib => customMaterialLib; + + @override + String generateContents() { + return ''' +class _ButtonDefaults { + static const double height = ${TokenButton.height}; + static const double borderRadius = ${TokenButton.borderRadius}; +} +'''; + } +} + +class UnformattedTemplate extends M3TokenTemplate { + UnformattedTemplate(this.customMaterialLib); + + final String customMaterialLib; + + @override + String get name => 'unformatted'; + + @override + String get materialLib => customMaterialLib; + + @override + String generateContents() { + return ''' +class UnformattedClass { +final int x = 1 ; + final String y = 'hello' ; +} +'''; + } +} + +class TestM3Template extends M3TokenTemplate { + @override + String get name => 'm3'; + + @override + String generateContents() => ''; +} + +class TestM3ExpressiveTemplate extends M3ETokenTemplate { + @override + String get name => 'm3e'; + + @override + String generateContents() => ''; +}