Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions doc/style-dedup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Style Dedup Challenges

## Why duplicates happen
- Figma’s REST styles endpoints (`/v1/files/:file_key/styles`, `/v1/teams/:team_id/styles`, etc.) return `key`, `file_key`, `node_id`, `style_type`, `name`, `description`, timestamps, and `sort_position`. There is **no** `collectionId` concept for styles; the “Headline/Headline 2” grouping you see in the UI is simply encoded in the `name` string via slash naming or folders.
- Designers can create multiple published styles that share the exact same `name` (path) inside the same file or library. When Figmage groups styles by `name` to generate strongly typed classes, every later style would overwrite the earlier one unless we intervene.
- The API doesn’t tell us which duplicate is “authoritative.” We can only distinguish them by IDs or timestamps (e.g., `key`, `node_id`, `created_at`), so deduping requires extra configuration or heuristics that Figmage doesn’t currently have.

## Current Figmage strategy
- We keep **all** occurrences. While building `valuesByNameByMode`, duplicate style names receive deterministic suffixes (`Variant2`, `Variant3`, …) so map keys stay unique.
- The code generators also sanitize identifiers; if two already-suffixed names still collide after camel-casing, the generator appends another suffix. That’s how extreme cases produced names like `display1Variant2Variant2`—one suffix came from the token map, another from the generator’s collision guard.
- Users get a warning listing every duplicate style (with node IDs) so they can clean up the design file or expect suffixed members in the generated Dart code.

## Key takeaways
- Styles don’t have collections in the REST API; only variables have `variableCollectionId`. Figmage infers “collections” from the slash-separated `name`.
- Duplicate warnings mean “Figmage kept every copy and suffixed the Dart members,” not that we can tell which one is canonical.
- To truly pick one style out of a set of duplicates, we would need extra rules (designer config, allowlists, or heuristics using `key`, `created_at`, etc.). Until then, suffixing keeps generated code consistent without silently dropping tokens.*** End Patch*** End Patch
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,17 @@ abstract class ModeThemeExtensionGenerator<T>

@override
Class generateClass() {
final uniqueFieldNames =
_getUniqueVariableNames(valuesByNameByMode.values.first.keys);

final validValueMaps = valuesByNameByMode.map(
(key, value) => MapEntry(
switch (key) {
"" => "",
_ => convertToValidVariableName(key),
},
value.map(
(key, value) => MapEntry(convertToValidVariableName(key), value),
(key, value) => MapEntry(uniqueFieldNames[key]!, value),
),
),
);
Expand Down Expand Up @@ -313,6 +316,28 @@ abstract class ModeThemeExtensionGenerator<T>
}
}

Map<String, String> _getUniqueVariableNames(Iterable<String> names) {
final result = <String, String>{};
final usedNames = <String>{};
final counts = <String, int>{};

for (final name in names) {
final baseName = convertToValidVariableName(name);
var count = (counts[baseName] ?? 0) + 1;
counts[baseName] = count;
var candidate = count == 1 ? baseName : '${baseName}Variant$count';
while (usedNames.contains(candidate)) {
count += 1;
candidate = '${baseName}Variant$count';
}
counts[baseName] = count;
usedNames.add(candidate);
result[name] = candidate;
}

return result;
}

/// Extension on Reference
extension ReferenceX on Reference {
/// Returns the a nullable reference
Expand Down
15 changes: 7 additions & 8 deletions lib/src/data/repositories/figma_assets_repository.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:figma_variables_api/figma_variables_api.dart';
import 'package:figma/figma.dart';
import 'package:figmage/src/domain/models/config/config.dart';
import 'package:figmage/src/domain/repositories/assets_repository.dart';
import 'package:http/http.dart' as http;
Expand Down Expand Up @@ -44,16 +44,15 @@ class FigmaAssetsRepository implements AssetsRepository {

final result = await client.getImages(
fileId,
batch,
scale: scale,
GetImages(
ids: batch.join(','),
scale: scale,
),
);

if (result.err != null) {
throw UnknownAssetsException(result.err);
}

final images = await _downloadImages(
images: result.images,
images: result.images
.map((key, value) => MapEntry(key, value?.toString())),
nodeSettings: nodeSettings,
scale: scale.toDouble(),
outputDir: outputDir,
Expand Down
Loading
Loading