Skip to content

v1: Tree-shaking of icon packagesย #5557

@FeodorFitsner

Description

@FeodorFitsner

This is continuation of work in #5468, #5556.

Tree-shaking of icon is needed for mega-sets like Material Symbols.
Without tree-shaking that set adds around 30 MB to an application bundle!

The idea:

  • During flet build scan Python program codebase to find all occurrences of icon values of enums inherited from flet.IconData, for example ft.Icons.ABC or ft.CupertinoIcons.ALARM.
  • In flet build template re-write getIconData method on Dart side which returns IconData from a hard-coded list of icons found on a previous step. For example:
switch(iconCode) {
    case 0x0001: return flet.Icons.ABC;
    case 0x0002: return flet.CupertinoIcons.ALARM;
    default: return null;
}
  • As no materialIcons or cupertinoIcons lists referenced Dart compiler will do icon-shaking and leave only icons used in a switch.
Vibe-coded prototype of Icon scanner
import ast
import importlib
from pathlib import Path
from types import ModuleType
from typing import Optional, Set, Tuple


def resolve_enum_reference(
    module: ModuleType, class_name: str, member_name: str
) -> Optional[object]:
    try:
        enum_class = getattr(module, class_name, None)
        if enum_class and hasattr(enum_class, member_name):
            return getattr(enum_class, member_name)
    except Exception:
        pass
    return None


def find_icon_usages(source_code: str) -> Set[Tuple[str, str]]:
    used_icons = set()

    class IconVisitor(ast.NodeVisitor):
        def visit_Attribute(self, node: ast.Attribute):
            full_chain = []
            current = node

            while isinstance(current, ast.Attribute):
                full_chain.append(current.attr)
                current = current.value
            if isinstance(current, ast.Name):
                full_chain.append(current.id)

            full_chain = list(reversed(full_chain))

            if len(full_chain) == 3 and full_chain[0] == "ft":
                enum_class = full_chain[1]
                icon_name = full_chain[2]
                used_icons.add((enum_class, icon_name))

            self.generic_visit(node)

    tree = ast.parse(source_code)
    IconVisitor().visit(tree)
    return used_icons


def scan_project_icons(root_dir: Path) -> Set[Tuple[str, str]]:
    all_used_icons = set()
    for file in root_dir.rglob("*.py"):
        try:
            source = file.read_text(encoding="utf-8")
            found = find_icon_usages(source)
            if found:
                print(f"๐Ÿ“„ {file.relative_to(root_dir)}: {found}")
                all_used_icons.update(found)
        except Exception as e:
            print(f"โš ๏ธ Error reading {file}: {e}")
    return all_used_icons


def print_resolved_icons(
    used: Set[Tuple[str, str]],
    enum_module: str,
    icon_base_class_path: str,
):
    base_mod_path, base_class_name = icon_base_class_path.rsplit(".", 1)
    base_mod = importlib.import_module(base_mod_path)
    IconDataBase = getattr(base_mod, base_class_name)

    enum_mod = importlib.import_module(enum_module)

    print("\nโœ… Resolved icon enum instances with metadata:")
    for class_name, member_name in sorted(used):
        try:
            enum_class = getattr(enum_mod, class_name)
            if not issubclass(enum_class, IconDataBase):
                continue  # Skip non-icon enums

            icon_member = getattr(enum_class, member_name, None)
            if icon_member is not None:
                value = hex(icon_member.value)
                package = getattr(enum_class, "_package_name", "unknown")
                klass = getattr(enum_class, "_class_name", "unknown")
                print(
                    f"{class_name}.{member_name} = {value} "
                    f"(package={package}, class={klass})"
                )
        except Exception as e:
            print(f"โš ๏ธ Error resolving {class_name}.{member_name}: {e}")


if __name__ == "__main__":
    import sys

    if len(sys.argv) != 4:
        print(
            "Usage: python scan_icon_usage.py <path_to_project> <enum_module> <base_class_path>"
        )
        print(
            "Example: python scan_icon_usage.py ./myapp flet.icons flet.icons.IconData"
        )
        sys.exit(1)

    project_path = Path(sys.argv[1])
    enum_module = sys.argv[2]  # e.g. "flet.icons"
    icon_base_class_path = sys.argv[3]  # e.g. "flet.icons.IconData"

    if not project_path.exists():
        print(f"โŒ Path does not exist: {project_path}")
        sys.exit(1)

    sys.path.insert(0, str(project_path.resolve()))

    used = scan_project_icons(project_path)
    print_resolved_icons(used, enum_module, icon_base_class_path)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions