Skip to content

Commit

Permalink
feat: pull assets from Figma (#162)
Browse files Browse the repository at this point in the history
* feat: adding new config for assets

* feat: asset generators

* comments

* feat: download assets and adjust generators, adjust brick

smaller fixes

* cleanup

* feat: added package info to the AssetImage getters

* fix: line length

* refactor: address PR feedback

* fix: test

* linterlove

* linterlove?

---------

Co-authored-by: Jesper <[email protected]>
  • Loading branch information
JesperBllnbm and Jesper authored Jan 4, 2025
1 parent ece37d3 commit ea4a3dd
Show file tree
Hide file tree
Showing 26 changed files with 947 additions and 22 deletions.
101 changes: 101 additions & 0 deletions integration_test/asset_download_integration_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import 'dart:io';

import 'package:figmage/src/command_runner.dart';
import 'package:riverpod/riverpod.dart';
import 'package:test/test.dart';

void main() {
group('Asset download integration test', () {
test(
'downloads and generates assets from figma with config',
() async {
final dir = Directory('./asset_test_package')..createSync();
addTearDown(() => dir.deleteSync(recursive: true));

// Create figmage.yaml with asset configuration
File('${dir.path}/figmage.yaml').writeAsStringSync('''
fileId: AxYhq1VnMWeq09C4otvoQx
assets:
generate: true
nodes:
"1:5":
name: icon_one
scales: [0.5, 1, 2]
"1:48":
name: icon_two
"2001:89":
name: icon_three
scales: [100, 2, 3]
''');

// Create container and command runner
final container = ProviderContainer();
addTearDown(container.dispose);

// Using the command runner to enable break points
final commandRunner = FigmageCommandRunner(container);

// Run the forge command directly
final exitCode = await commandRunner.run([
'forge',
'-t',
Platform.environment['FIGMA_FREE_TOKEN']!,
'-p',
dir.path,
]);

expect(exitCode, 0);

// Verify assets directory was created
final assetsDir = Directory('${dir.path}/assets/');
expect(
assetsDir.existsSync(),
true,
reason: 'Assets directory was not created',
);

// Verify assets file was generated
final assetsFile = File('${dir.path}/lib/src/assets.dart');
expect(
assetsFile.existsSync(),
true,
reason: 'Assets file was not generated',
);

// Verify specific assets were downloaded with correct scales
final expectedFiles = [
'icon_one.png',
'[email protected]',
'[email protected]',
'icon_two.png',
];

final assetFiles =
assetsDir.listSync().map((f) => f.path.split('/').last);
for (final expectedFile in expectedFiles) {
expect(
assetFiles.contains(expectedFile),
true,
reason: 'Expected asset $expectedFile was not downloaded',
);
}

// Verify the generated assets.dart file has correct content
final assetsContent = assetsFile.readAsStringSync();
expect(
assetsContent,
contains('static const String iconOne05xPng = '),
reason: 'Assets class missing iconOne constant',
);
expect(
assetsContent,
contains('static const String iconTwoPng = '),
reason: 'Assets class missing iconTwo constant',
);
},
timeout: const Timeout(
Duration(minutes: 1),
),
);
});
}
112 changes: 112 additions & 0 deletions lib/src/data/generators/assets/asset_class_generator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import 'package:code_builder/code_builder.dart';
import 'package:figmage/src/data/generators/generator_util.dart';
import 'package:figmage/src/domain/generators/theme_class_generator.dart';

/// {@template asset_class_generator}
/// Generates a class for accessing Figma assets.
/// Creates constants for asset paths and convenience getters for AssetImage
/// instances. Supports assets with different scale factors through filename
/// suffixes.
/// {@endtemplate}
class AssetClassGenerator implements ThemeClassGenerator {
/// {@macro asset_class_generator}
const AssetClassGenerator({
required this.className,
required this.assets,
required this.packageName,
});

@override
final String className;

/// The successfully downloaded assets to generate code for.
/// where each entry: key: id, value: assetNames
final Map<String, List<String>> assets;

/// The name of the package these assets belong to.
final String packageName;

@override
bool get buildContextExtensionNullable => false;

@override
Class generateClass() {
return Class(
(b) {
b
..name = className
..constructors.add(
Constructor(
(b) => b..constant = true,
),
)
..annotations.add(
refer('immutable', 'package:meta/meta.dart'),
)
..modifier = ClassModifier.final$
..fields.add(
Field(
(b) => b
..name = '_basePath'
..static = true
..modifier = FieldModifier.constant
..type = refer('String')
..assignment = const Code("'assets/'"),
),
)
..fields.addAll([
for (final MapEntry(key: id, value: assetNames) in assets.entries)
...assetNames.map((asset) {
final assetName = convertToValidVariableName(asset);
return Field(
(b) => b
..name = assetName
..static = true
..modifier = FieldModifier.constant
..type = refer('String')
..assignment = Code("'\${_basePath}$asset'")
..docs.add('/// Rendered from frame $id'),
);
}),
])
..methods.addAll(
assets.values.expand((values) {
return values.map((asset) {
final assetName = convertToValidVariableName(asset);

return Method(
(b) => b
..name = '${assetName}Image'
..static = true
..type = MethodType.getter
..returns = refer('AssetImage')
..lambda = true
..body =
Code("AssetImage($assetName, package: '$packageName')"),
);
});
}),
);
},
);
}

@override
Extension generateExtension() {
return Extension(
(b) => b
..name = '${className}BuildContextX'
..on = refer('BuildContext', 'package:flutter/widgets.dart')
..methods.add(
Method(
(b) => b
..name = className.toLowerCase()
..type = MethodType.getter
..returns = refer(className)
..lambda = true
..body = Code('$className()'),
),
),
);
}
}
49 changes: 49 additions & 0 deletions lib/src/data/generators/file_generators/asset_file_generator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:code_builder/code_builder.dart';
import 'package:figmage/src/data/generators/assets/asset_class_generator.dart';
import 'package:figmage/src/domain/generators/file_generator.dart';
import 'package:figmage/src/domain/generators/theme_class_generator.dart';
import 'package:figmage_package_generator/figmage_package_generator.dart';

/// {@template asset_file_generator}
/// A [FileGenerator] that generates a file for accessing Figma assets.
/// {@endtemplate}
class AssetFileGenerator implements FileGenerator {
/// {@macro asset_file_generator}
AssetFileGenerator({
required this.assets,
required this.packageName,
});

/// Map of node IDs to their downloaded asset file names.
final Map<String, List<String>> assets;

/// The name of the package these assets belong to.
final String packageName;

@override
final TokenFileType type = TokenFileType.assets;

@override
late final Iterable<ThemeClassGenerator> generators = [
AssetClassGenerator(
className: 'Assets',
assets: assets,
packageName: packageName,
),
];

@override
Library generate() {
final lib = LibraryBuilder();
lib.comments.addAll(FileGenerator.generatedFilePrefix);
lib.body.addAll(
[
for (final g in generators) ...[
g.generateClass(),
g.generateExtension(),
],
],
);
return lib.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,6 @@ abstract class BaseFileGenerator<T> implements DesignTokenFileGenerator<T> {
required this.tokens,
});

/// The prefix for all generated files that are generated by this package.
static const _generatedFilePrefix = [
'coverage:ignore-file',
'GENERATED CODE - DO NOT MODIFY BY HAND',
'ignore_for_file: type=lint',
];

@override
final TokenFileType type;

Expand Down Expand Up @@ -54,7 +47,7 @@ abstract class BaseFileGenerator<T> implements DesignTokenFileGenerator<T> {
@override
Library generate() {
final lib = LibraryBuilder();
lib.comments.addAll(_generatedFilePrefix);
lib.comments.addAll(FileGenerator.generatedFilePrefix);
lib.body.addAll(
[
for (final g in generators) ...[
Expand Down
Loading

0 comments on commit ea4a3dd

Please sign in to comment.