Files
stack/infra/marimo/theme/scripts/gen-lucide-shim.py
kert 16f3b43974
Some checks failed
CI / skinny-install (aco) (push) Successful in 1m12s
CI / skinny-install (api) (push) Successful in 30s
CI / skinny-install (bcda) (push) Successful in 36s
CI / skinny-install (bib) (push) Successful in 35s
CI / skinny-install (bls) (push) Successful in 27s
CI / skinny-install (ccw) (push) Successful in 32s
CI / skinny-install (cli) (push) Successful in 41s
CI / skinny-install (cms) (push) Successful in 37s
CI / skinny-install (conf) (push) Successful in 38s
CI / skinny-install (opps) (push) Successful in 33s
CI / skinny-install (perf) (push) Successful in 38s
CI / skinny-install (pfs) (push) Successful in 38s
CI / skinny-install (rex) (push) Successful in 34s
Deploy / build-scan-report (push) Failing after 46s
Infra CI / notebooks (push) Failing after 25s
Infra CI / zotero (push) Successful in 12s
Infra CI / docs (push) Failing after 16s
CI / lint-test (push) Failing after 11m2s
Infra CI / mc (push) Successful in 21s
Infra CI / api (push) Successful in 29s
Package Supply Chain / pkg-supply-chain (push) Failing after 41s
feat: full session — mail servers, comment pipeline, PRISMA fetch, email ingest
Mail: Maddy on DO (corwins.media+Resend, fhirworx.io+Postmark),
touchless/stateless/idempotent. Gitea SMTP via env_file. CMS inbox
at cmsupdates@mail.fhirworx.io with IMAP→bib poller.

Bib: regulations.gov v4 client, Federal Register discovery, 164K
comment backfill (running), IMAP email ingest, Zotero sync routing.

PRISMA: altcha PoW solver, CrossRef DOI resolution, 83/129 PDFs.
Zotero: schema parity, ops module, CLI, fail-fast guard.
CI: docs.Dockerfile COPY glob fix (tracks #341).
Infra: Gitea+marimo fhirworx themes, IOM/OIG modules.
2026-04-16 09:04:38 -04:00

332 lines
14 KiB
Python

#!/usr/bin/env python3
"""Regenerate `theme/frontend/src/lucide-shim.tsx`.
Scans a marimo source tree for every `import {…} from 'lucide-react'`
and emits a TSX shim that re-exports Tabler icons under each Lucide
name. Runs outside the Docker build (the shim is committed), so you
only rerun this when bumping the pinned marimo version or when an
upstream change introduces new lucide icons.
Usage:
python3 gen-lucide-shim.py [MARIMO_SRC_ROOT]
Default source root is `infra/marimo/src` relative to the stack repo root.
"""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
# Lucide-name → Tabler export name. Populated with the divergent cases;
# straight-rename names are handled by `transform()` below.
SPECIAL: dict[str, str] = {
"Play": "IconPlayerPlay",
"PlayIcon": "IconPlayerPlay",
"PlayCircleIcon": "IconPlayerPlay",
"PlaySquareIcon": "IconPlayerPlay",
"CirclePlayIcon": "IconPlayerPlay",
"Pause": "IconPlayerPause",
"StopCircleIcon": "IconPlayerStop",
"SkipForwardIcon": "IconPlayerSkipForward",
"FastForwardIcon": "IconPlayerTrackNext",
"Loader2": "IconLoader2",
"Loader2Icon": "IconLoader2",
"LoaderCircle": "IconLoader",
"TriangleAlert": "IconAlertTriangle",
"ChartColumn": "IconChartBar",
"ChartColumnIcon": "IconChartBar",
"ChartColumnStacked": "IconChartBarOff",
"ChartNoAxesColumn": "IconChartBar",
"HatGlasses": "IconMoodHappy",
"RulerDimensionLine": "IconRuler",
"FunnelPlusIcon": "IconFilterPlus",
"FilterX": "IconFilterOff",
"AreaChartIcon": "IconChartArea",
"BarChartIcon": "IconChartBar",
"BarChart2Icon": "IconChartBar",
"BarChartBigIcon": "IconChartBar",
"LineChartIcon": "IconChartLine",
"PieChartIcon": "IconChartPie",
"ChartPieIcon": "IconChartPie",
"ChartScatterIcon": "IconChartDots",
"ChartSplineIcon": "IconChartLine",
"Grid3x3Icon": "IconGrid3x3",
"Columns2Icon": "IconColumns2",
"Edit3Icon": "IconEdit",
"Code2Icon": "IconCode",
"FileAudio2Icon": "IconFileMusic",
"FileAudioIcon": "IconFileMusic",
"FileVideoCameraIcon": "IconFileTypography",
"SquareMIcon": "IconSquareLetterM",
"SquareCodeIcon": "IconCode",
"SquareDashedBottomCodeIcon": "IconSquareDashed",
"CircleHelpIcon": "IconHelpCircle",
"HelpCircleIcon": "IconHelpCircle",
"MessageCircleQuestionIcon": "IconMessageCircleQuestion",
"AlertOctagonIcon": "IconAlertOctagon",
"CheckCircle2Icon": "IconCircleCheck",
"CheckCircleIcon": "IconCircleCheck",
"CheckSquareIcon": "IconSquareCheck",
"XCircle": "IconCircleX",
"XCircleIcon": "IconCircleX",
"DatabaseZap": "IconDatabase",
"DatabaseZapIcon": "IconDatabase",
"ArrowDownWideNarrowIcon": "IconArrowsSort",
"ArrowUpWideNarrowIcon": "IconArrowsSort",
"ArrowUpNarrowWideIcon": "IconArrowsSort",
"ChevronsUpDown": "IconChevronUpDown",
"ChevronsUpDownIcon": "IconChevronUpDown",
"ChevronsDownUpIcon": "IconChevronDownUp",
"CircleEllipsis": "IconDotsCircleHorizontal",
"MousePointerSquareDashedIcon": "IconPointerShare",
"SquareMousePointerIcon": "IconPointer",
"SquareArrowOutUpRightIcon": "IconExternalLink",
"SquareFunction": "IconMathFunction",
"SquareFunctionIcon": "IconMathFunction",
"FunctionSquareIcon": "IconMathFunction",
"SquareEqualIcon": "IconEqual",
"Share2Icon": "IconShare",
"Undo2Icon": "IconArrowBackUp",
"Trash2Icon": "IconTrash",
"BetweenHorizontalStartIcon": "IconLayoutRows",
"BugPlayIcon": "IconBug",
"FolderArchiveIcon": "IconFolder",
"FolderCog2": "IconFolderCog",
"TerminalSquareIcon": "IconTerminal2",
"HardDriveDownloadIcon":"IconDeviceSdCard",
"DownloadCloudIcon": "IconCloudDownload",
"HardDrive": "IconDeviceSdCard",
"HardDriveIcon": "IconDeviceSdCard",
"MemoryStickIcon": "IconDeviceSdCard",
"MicrochipIcon": "IconCpu",
"BrickWallIcon": "IconWall",
"ViewIcon": "IconEye",
"BotMessageSquareIcon": "IconRobot",
"BotIcon": "IconRobot",
"TextCursorInputIcon": "IconCursorText",
"TextSelectionIcon": "IconTextCaption",
"TextSearchIcon": "IconZoomCode",
"TextIcon": "IconTextCaption",
"TypeIcon": "IconTypography",
"CaseSensitiveIcon": "IconLetterCase",
"WholeWordIcon": "IconAlphabetLatin",
"WrapTextIcon": "IconTextWrap",
"SendHorizontalIcon": "IconSend",
"NotebookText": "IconNotebook",
"NotebookPenIcon": "IconNotebook",
"BookMarkedIcon": "IconBookmark",
"BookTextIcon": "IconBook",
"KeyRoundIcon": "IconKey",
"PartyPopperIcon": "IconConfetti",
"PaintRollerIcon": "IconBrush",
"PackageCheckIcon": "IconPackage",
"PackageXIcon": "IconPackageOff",
"YoutubeIcon": "IconBrandYoutube",
"GithubIcon": "IconBrandGithub",
"Info": "IconInfoCircle",
"InfoIcon": "IconInfoCircle",
"CopyMinusIcon": "IconCopy",
"CopySlashIcon": "IconCopyOff",
"SearchCheck": "IconZoomCheck",
"ChevronDownCircleIcon": "IconCircleChevronDown",
"ChevronRightCircleIcon": "IconCircleChevronRight",
"MoreHorizontal": "IconDots",
"MoreHorizontalIcon": "IconDots",
"MoreVerticalIcon": "IconDotsVertical",
"RefreshCcw": "IconRefresh",
"RefreshCcwIcon": "IconRefresh",
"RefreshCwIcon": "IconRefresh",
"RotateCcwIcon": "IconRotate",
"RotateCwIcon": "IconRotateClockwise",
"AtSignIcon": "IconAt",
"SigmaIcon": "IconSum",
"BaselineIcon": "IconBaselineDensitySmall",
"AlignCenterVerticalIcon": "IconAlignCenter",
"AlignStartVerticalIcon": "IconAlignLeft",
"AlignEndVerticalIcon": "IconAlignRight",
"AlignHorizontalSpaceAroundIcon": "IconAlignJustified",
"AlignJustifyIcon": "IconAlignJustified",
"ZapIcon": "IconBolt",
"ZapOffIcon": "IconBoltOff",
"ListFilterIcon": "IconFilter",
"ListFilterPlusIcon": "IconFilterPlus",
"CircleHelp": "IconHelpCircle",
"CalendarClockIcon": "IconCalendarTime",
"ClipboardPasteIcon": "IconClipboardText",
"GraduationCapIcon": "IconSchool",
"ExpandIcon": "IconArrowsMaximize",
"LayersIcon": "IconStack",
"LayoutTemplateIcon": "IconLayoutBoard",
"ListOrderedIcon": "IconListNumbers",
"GroupIcon": "IconUsersGroup",
"GlobeIcon": "IconWorld",
"WorkflowIcon": "IconSitemap",
"Table2Icon": "IconTable",
"MessagesSquareIcon": "IconMessages",
"MenuIcon": "IconMenu2",
"MonitorIcon": "IconDeviceDesktop",
"PlusCircleIcon": "IconCirclePlus",
"PlusSquareIcon": "IconSquarePlus",
"FilePenIcon": "IconFilePencil",
"FilePlus2Icon": "IconFilePlus",
"FileImageIcon": "IconPhoto",
"FileJsonIcon": "IconJson",
"FileVideoIcon": "IconVideo",
"ImageIcon": "IconPhoto",
"PanelLeftIcon": "IconLayoutSidebar",
"PanelRightIcon": "IconLayoutSidebarRight",
"PowerOffIcon": "IconPower",
"PowerSquareIcon": "IconPower",
"SaveIcon": "IconDeviceFloppy",
"ShuffleIcon": "IconArrowsShuffle",
"GridIcon": "IconGrid3x3",
"CombineIcon": "IconStackPush",
"ArrowDownToLineIcon": "IconArrowBarToDown",
"ArrowRightFromLineIcon": "IconArrowBarRight",
"ArrowRightToLineIcon": "IconArrowBarToRight",
"ArrowRightSquareIcon": "IconSquareArrowRight",
"ArrowUpDownIcon": "IconArrowsUpDown",
"ArrowUpToLineIcon": "IconArrowBarToUp",
"SquareStack": "IconStack",
"WrenchIcon": "IconTool",
"SquareIcon": "IconSquare",
"CornerLeftUp": "IconCornerLeftUp",
"GripHorizontal": "IconGripHorizontal",
"GripHorizontalIcon": "IconGripHorizontal",
"GripVerticalIcon": "IconGripVertical",
"CrosshairIcon": "IconCrosshair",
"CommandIcon": "IconCommand",
"PinIcon": "IconPin",
"PinOffIcon": "IconPinnedOff",
"DiamondPlusIcon": "IconDiamondPlus",
"BinaryIcon": "IconBinary",
"BoxIcon": "IconBox",
"BracesIcon": "IconBraces",
"BracketsIcon": "IconBrackets",
"BrainIcon": "IconBrain",
"ActivityIcon": "IconActivity",
"Cog": "IconSettings",
"CogIcon": "IconSettings",
# Names that don't exist as-is in @tabler/icons-react v3.41.
"BookOpenIcon": "IconBook",
"BookPlusIcon": "IconBook",
"ChevronsUpDown": "IconSelector",
"ChevronsUpDownIcon": "IconSelector",
"ChevronsDownUpIcon": "IconSelector",
"CurlyBracesIcon": "IconBraces",
"DiamondPlusIcon": "IconDiamond",
"EllipsisIcon": "IconDots",
"FlaskConicalIcon": "IconFlask",
"FolderTreeIcon": "IconFolders",
"OrbitIcon": "IconCircleDashed",
"ScrollIcon": "IconArticle",
"ScrollTextIcon": "IconArticle",
}
_FALLBACK = "IconQuestionMark"
def load_tabler_exports() -> set[str] | None:
"""Return the set of Tabler icon names if the authoritative list is on
disk (generated by probing @tabler/icons-react in a container). If it's
missing, return None — generator emits without validation and build-time
errors surface the unknowns."""
cand = Path(__file__).resolve().parent / "tabler-icons.txt"
if not cand.is_file():
cand = Path("/tmp/tabler-icons.txt")
if not cand.is_file():
return None
return {line.strip() for line in cand.read_text().splitlines() if line.strip()}
_TABLER: set[str] | None = load_tabler_exports()
def transform(name: str) -> str:
"""Map a Lucide export name to its Tabler equivalent, falling back to
IconQuestionMark when the intended target doesn't actually exist in the
installed @tabler/icons-react version."""
if name in SPECIAL:
target = SPECIAL[name]
else:
base = name[:-4] if name.endswith("Icon") else name
target = "Icon" + base
if _TABLER is not None and target not in _TABLER:
return _FALLBACK
return target
def extract(src_root: Path) -> set[str]:
"""Collect every name marimo imports or re-exports from lucide-react."""
names: set[str] = set()
# Covers both `import { X, Y as Z } from "lucide-react"` and
# `export { X as A } from "lucide-react"` (0.23.1 added re-exports).
pattern = re.compile(
r'(?:import|export)\s*(?:type\s*)?\{([^}]+)\}\s*from\s*["\']lucide-react["\']'
)
for root, _, files in os.walk(src_root):
for fn in files:
if not fn.endswith((".ts", ".tsx")):
continue
text = (Path(root) / fn).read_text()
for match in pattern.finditer(text):
for token in match.group(1).split(","):
token = token.strip().removeprefix("type ").strip()
token = token.split(" as ")[0].strip()
if token and re.match(r"^[A-Z]", token):
names.add(token)
return names
def render(names: set[str]) -> str:
TYPE_ONLY = {"LucideIcon", "LucideProps"}
sorted_names = sorted(n for n in names if n not in TYPE_ONLY)
# De-dup tabler names (many lucide aliases resolve to the same icon).
tabler_set = sorted({transform(n) for n in sorted_names})
header = [
"// Auto-generated: Tabler Icons as a drop-in for lucide-react.",
"// Build-time replacement; do not edit by hand — see gen-lucide-shim.py",
"import type { ComponentType, SVGProps } from 'react';",
"import {",
]
header.extend(f" {name}," for name in tabler_set)
header.extend([
" type IconProps,",
"} from '@tabler/icons-react';",
"",
"// Prop shapes mirror lucide's public types so Omit<LucideProps, 'ref'>",
"// and similar patterns in upstream marimo code keep typechecking.",
"export type LucideProps = SVGProps<SVGSVGElement> & {",
" size?: number | string;",
" absoluteStrokeWidth?: boolean;",
"};",
"export type LucideIcon = ComponentType<LucideProps & IconProps>;",
"",
])
body = [
f"export const {n}: LucideIcon = {transform(n)} as LucideIcon;"
for n in sorted_names
]
return "\n".join(header + body) + "\n"
def main() -> None:
default = Path(__file__).resolve().parents[2] / "src"
src = Path(sys.argv[1]) if len(sys.argv) > 1 else default
if not src.is_dir():
raise SystemExit(f"marimo source not found: {src}")
names = extract(src / "frontend" / "src")
out = Path(__file__).resolve().parents[1] / "frontend" / "src" / "lucide-shim.tsx"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(render(names))
print(f"wrote {out} ({len(names)} names)")
if __name__ == "__main__":
main()