From 53b5c2f808f5f6ec1e2aaebb1e9749cd21016d68 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:49:34 -0700 Subject: [PATCH 1/6] perf(react): reduce SVG path precision --- apps/docs/app/api/og/route.tsx | 6 +- apps/docs/components/icons.tsx | 236 +++++++++--------- apps/sim/components/icons.tsx | 236 +++++++++--------- packages/emcn/src/icons/bubble-chat-delay.tsx | 2 +- packages/emcn/src/icons/download.tsx | 2 +- packages/emcn/src/icons/duplicate.tsx | 2 +- packages/emcn/src/icons/mic.tsx | 2 +- packages/emcn/src/icons/upload.tsx | 2 +- 8 files changed, 238 insertions(+), 250 deletions(-) diff --git a/apps/docs/app/api/og/route.tsx b/apps/docs/app/api/og/route.tsx index ee99eec89e9..c513897d136 100644 --- a/apps/docs/app/api/og/route.tsx +++ b/apps/docs/app/api/og/route.tsx @@ -159,15 +159,15 @@ function SimWordmark() { return ( diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 8580bb9ea85..b98fdede7cb 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -50,39 +50,39 @@ export function AgentPhoneIcon(props: SVGProps) { @@ -969,7 +969,7 @@ export function PersonaIcon(props: SVGProps) { {...props} > @@ -1562,23 +1562,23 @@ export function ProspeoIcon(props: SVGProps) { @@ -1753,32 +1753,29 @@ export function RB2BIcon(props: SVGProps) { return ( - - - - + + + + + + - + + + + - - - - - - + @@ -1896,11 +1893,11 @@ export function GoogleAppsheetIcon(props: SVGProps) { - - + + ) @@ -2267,11 +2264,11 @@ export function AtlassianIcon(props: SVGProps) { ) @@ -2355,15 +2352,15 @@ export function ConvexIcon(props: SVGProps) { xmlns='http://www.w3.org/2000/svg' > @@ -2386,11 +2383,11 @@ export function SendblueIcon(props: SVGProps) { ) @@ -2529,15 +2526,15 @@ export function MintlifyIcon(props: SVGProps) { return ( @@ -2799,13 +2796,13 @@ export function FindymailIcon(props: SVGProps) { @@ -2842,19 +2839,19 @@ export function ZeroBounceIcon(props: SVGProps) { @@ -3183,7 +3180,7 @@ export function ThriveIcon(props: SVGProps) { return ( @@ -4513,20 +4510,17 @@ export function QuartrIcon(props: SVGProps) { return ( - - + + - + ) @@ -4793,47 +4787,47 @@ export function MicrosoftOneDriveIcon(props: SVGProps) { ) @@ -6065,17 +6059,17 @@ export function ZoomInfoIcon(props: SVGProps) { > - + @@ -6290,27 +6284,27 @@ export function DynatraceIcon(props: SVGProps) { ) @@ -6565,7 +6559,7 @@ export function CodePipelineIcon(props: SVGProps) { transform='translate(40, 40) scale(1.25) translate(-40, -40)' > @@ -6885,25 +6879,25 @@ export function DaytonaIcon(props: SVGProps) { ) { y='12.9094' width='20.6556' height='8.54718' - transform='rotate(90 22.1582 12.9094)' + transform='rotate(90 22.16 12.91)' fill='currentColor' /> ) { y='42.825' width='25.6415' height='8.54718' - transform='rotate(90 52.0732 42.825)' + transform='rotate(90 52.07 42.83)' fill='currentColor' /> @@ -7196,15 +7190,15 @@ export function DowndetectorIcon(props: SVGProps) { @@ -7302,7 +7296,7 @@ export function GranolaIcon(props: SVGProps) { > ) @@ -7355,19 +7349,19 @@ export function GreptileIcon(props: SVGProps) { return ( @@ -8550,7 +8544,7 @@ export function VantaIcon(props: SVGProps) { ) { @@ -8921,31 +8915,31 @@ export function LeadMagicIcon(props: SVGProps) { @@ -8975,11 +8969,11 @@ export function IcypeasIcon(props: SVGProps) { @@ -9123,7 +9117,7 @@ export function RetoolIcon(props: SVGProps) { ) @@ -9188,51 +9182,51 @@ export function JupyterIcon(props: SVGProps) { @@ -9258,13 +9252,13 @@ export function RocketlaneIcon(props: SVGProps) { @@ -9285,7 +9279,7 @@ export function LogfireIcon(props: SVGProps) { role='img' xmlns='http://www.w3.org/2000/svg' > - + ) } @@ -9318,21 +9312,21 @@ export function SmartleadIcon(props: SVGProps) { > diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 8580bb9ea85..b98fdede7cb 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -50,39 +50,39 @@ export function AgentPhoneIcon(props: SVGProps) { @@ -969,7 +969,7 @@ export function PersonaIcon(props: SVGProps) { {...props} > @@ -1562,23 +1562,23 @@ export function ProspeoIcon(props: SVGProps) { @@ -1753,32 +1753,29 @@ export function RB2BIcon(props: SVGProps) { return ( - - - - + + + + + + - + + + + - - - - - - + @@ -1896,11 +1893,11 @@ export function GoogleAppsheetIcon(props: SVGProps) { - - + + ) @@ -2267,11 +2264,11 @@ export function AtlassianIcon(props: SVGProps) { ) @@ -2355,15 +2352,15 @@ export function ConvexIcon(props: SVGProps) { xmlns='http://www.w3.org/2000/svg' > @@ -2386,11 +2383,11 @@ export function SendblueIcon(props: SVGProps) { ) @@ -2529,15 +2526,15 @@ export function MintlifyIcon(props: SVGProps) { return ( @@ -2799,13 +2796,13 @@ export function FindymailIcon(props: SVGProps) { @@ -2842,19 +2839,19 @@ export function ZeroBounceIcon(props: SVGProps) { @@ -3183,7 +3180,7 @@ export function ThriveIcon(props: SVGProps) { return ( @@ -4513,20 +4510,17 @@ export function QuartrIcon(props: SVGProps) { return ( - - + + - + ) @@ -4793,47 +4787,47 @@ export function MicrosoftOneDriveIcon(props: SVGProps) { ) @@ -6065,17 +6059,17 @@ export function ZoomInfoIcon(props: SVGProps) { > - + @@ -6290,27 +6284,27 @@ export function DynatraceIcon(props: SVGProps) { ) @@ -6565,7 +6559,7 @@ export function CodePipelineIcon(props: SVGProps) { transform='translate(40, 40) scale(1.25) translate(-40, -40)' > @@ -6885,25 +6879,25 @@ export function DaytonaIcon(props: SVGProps) { ) { y='12.9094' width='20.6556' height='8.54718' - transform='rotate(90 22.1582 12.9094)' + transform='rotate(90 22.16 12.91)' fill='currentColor' /> ) { y='42.825' width='25.6415' height='8.54718' - transform='rotate(90 52.0732 42.825)' + transform='rotate(90 52.07 42.83)' fill='currentColor' /> @@ -7196,15 +7190,15 @@ export function DowndetectorIcon(props: SVGProps) { @@ -7302,7 +7296,7 @@ export function GranolaIcon(props: SVGProps) { > ) @@ -7355,19 +7349,19 @@ export function GreptileIcon(props: SVGProps) { return ( @@ -8550,7 +8544,7 @@ export function VantaIcon(props: SVGProps) { ) { @@ -8921,31 +8915,31 @@ export function LeadMagicIcon(props: SVGProps) { @@ -8975,11 +8969,11 @@ export function IcypeasIcon(props: SVGProps) { @@ -9123,7 +9117,7 @@ export function RetoolIcon(props: SVGProps) { ) @@ -9188,51 +9182,51 @@ export function JupyterIcon(props: SVGProps) { @@ -9258,13 +9252,13 @@ export function RocketlaneIcon(props: SVGProps) { @@ -9285,7 +9279,7 @@ export function LogfireIcon(props: SVGProps) { role='img' xmlns='http://www.w3.org/2000/svg' > - + ) } @@ -9318,21 +9312,21 @@ export function SmartleadIcon(props: SVGProps) { > diff --git a/packages/emcn/src/icons/bubble-chat-delay.tsx b/packages/emcn/src/icons/bubble-chat-delay.tsx index 4ac022f6be7..5b8c96f31bf 100644 --- a/packages/emcn/src/icons/bubble-chat-delay.tsx +++ b/packages/emcn/src/icons/bubble-chat-delay.tsx @@ -21,7 +21,7 @@ export function BubbleChatDelay({ size = 24, width, height, ...props }: IconProp {...props} > {/* tray — same as Upload */} - + {/* stem — top to tray */} {/* arrowhead pointing down */} diff --git a/packages/emcn/src/icons/duplicate.tsx b/packages/emcn/src/icons/duplicate.tsx index 36e235f3fde..5a756a3bdfe 100644 --- a/packages/emcn/src/icons/duplicate.tsx +++ b/packages/emcn/src/icons/duplicate.tsx @@ -19,7 +19,7 @@ export function Duplicate(props: SVGProps) { aria-hidden='true' {...props} > - + ) diff --git a/packages/emcn/src/icons/mic.tsx b/packages/emcn/src/icons/mic.tsx index 4a59eabd042..735f069566e 100644 --- a/packages/emcn/src/icons/mic.tsx +++ b/packages/emcn/src/icons/mic.tsx @@ -20,7 +20,7 @@ export function Mic(props: SVGProps) { {...props} > - + ) diff --git a/packages/emcn/src/icons/upload.tsx b/packages/emcn/src/icons/upload.tsx index d1ac11b4a05..157b25128d8 100644 --- a/packages/emcn/src/icons/upload.tsx +++ b/packages/emcn/src/icons/upload.tsx @@ -19,7 +19,7 @@ export function Upload(props: SVGProps) { aria-hidden='true' {...props} > - + From 86c3590b440cf0af0fcae85285768607c6a43050 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:29:03 -0700 Subject: [PATCH 2/6] fix(react): preserve Sim wordmark precision --- apps/docs/app/api/og/route.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs/app/api/og/route.tsx b/apps/docs/app/api/og/route.tsx index c513897d136..ee99eec89e9 100644 --- a/apps/docs/app/api/og/route.tsx +++ b/apps/docs/app/api/og/route.tsx @@ -159,15 +159,15 @@ function SimWordmark() { return ( From 0fa7c77c169ddad15255468da1c4fd657da123a8 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:18:51 -0700 Subject: [PATCH 3/6] fix(icons): preserve Quartr scale --- apps/docs/components/icons.tsx | 2 +- apps/sim/components/icons.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index b98fdede7cb..423055b27ac 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4510,7 +4510,7 @@ export function QuartrIcon(props: SVGProps) { return ( - + ) { return ( - + Date: Wed, 19 Aug 2026 17:15:41 -0700 Subject: [PATCH 4/6] test(icons): ratchet SVG path precision --- package.json | 4 +- .../check-icon-path-precision.baseline.json | 160 +++++++++ scripts/check-icon-path-precision.test.ts | 116 +++++++ scripts/check-icon-path-precision.ts | 315 ++++++++++++++++++ 4 files changed, 594 insertions(+), 1 deletion(-) create mode 100644 scripts/check-icon-path-precision.baseline.json create mode 100644 scripts/check-icon-path-precision.test.ts create mode 100644 scripts/check-icon-path-precision.ts diff --git a/package.json b/package.json index 587ef6854bc..217404b4cee 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,9 @@ "dev:sockets": "cd apps/realtime && bun run dev", "dev:full": "bunx concurrently -n \"App,Realtime\" -c \"cyan,magenta\" \"cd apps/sim && bun run dev\" \"cd apps/realtime && bun run dev\"", "dev:full:capped": "bunx concurrently -n \"App,Realtime\" -c \"cyan,magenta\" \"cd apps/sim && bun run dev:capped\" \"cd apps/realtime && bun run dev\"", - "test": "bun run test:setup && turbo run test", + "test": "bun run test:setup && bun run test:icon-path-precision && turbo run test", "test:setup": "bun run --cwd packages/sim-setup test", + "test:icon-path-precision": "bunx vitest run scripts/check-icon-path-precision.test.ts", "format": "turbo run format", "format:check": "turbo run format:check", "lint": "turbo run lint", @@ -46,6 +47,7 @@ "check:canvas-sentences": "bun run apps/sim/scripts/check-canvas-sentences.ts --require-coverage", "check:bare-icons": "bun run scripts/check-bare-icons.ts", "check:icon-paths": "bun run scripts/check-icon-paths.ts", + "check:icon-path-precision": "bun run scripts/check-icon-path-precision.ts", "check:migrations": "bun run scripts/check-migrations-safety.ts", "check:native-typecheck": "bun run scripts/check-native-typecheck.ts", "check:source-text": "bun run scripts/check-source-text.ts", diff --git a/scripts/check-icon-path-precision.baseline.json b/scripts/check-icon-path-precision.baseline.json new file mode 100644 index 00000000000..4ccb5e1c72b --- /dev/null +++ b/scripts/check-icon-path-precision.baseline.json @@ -0,0 +1,160 @@ +{ + "generatedFrom": "shared app/docs icon catalogs and packages/emcn/src/icons", + "maxFractionDigits": 2, + "entries": { + "01d9575ed69da153ef3b9e8e493584f212a979740a1cf824106889f31856a61b": 2, + "0391deca941d91d0f2abb90759450c5e608fa52747d0adeda05c90bb000ad4f2": 2, + "04866b8e0cc7027bc039520f77601469b0180d547fe1e0eb85200b4b5ca9bb1b": 2, + "051c689d7ae3af5a09c7512f3f2adac5f45d34c742dc2eb3615e5f5f6e017b94": 2, + "058b19b1afecf1e0a0982fd3f1d81a9260726cd17ccc2a04334f5867dd34661c": 2, + "0762e29006cf29fe941495d66d0a868cb699d269f054809774cd9ddd82342f67": 2, + "08228705b5f098a11779a97b460d7879606acc22b728fd52d0d6ad94416be328": 2, + "08ad1b8fc90daa6c7b1a623fd817682cdfea5ee81a634065e8a00c1c107756ea": 2, + "0992becea53293ef87801c3eb1f3d74ee09aa66bd42bab2c4ce39d95832f78fb": 2, + "0b54c1bbff031268071ae4d92e9c30b2fd5b6615d8513e4e096a26e982b8304e": 1, + "0b62d7cffbf245f5c2c087e97f0f7bc95bb74eae02cc397e1698464cfa227bab": 1, + "0b748a7626643a5125c304b155a706ad52efdba330799fa501c3288f04bcc148": 2, + "0fdefc9585999fa87abc33859b23bccfd33ccf02362a57e9fcbdcfc170e98c54": 2, + "1050dcf5409d0f27455b2d006c493ed16b861ff3f4271c8e312baf163ae9e527": 2, + "10c9a483f4506b4fe074140fd5ae571555bc6b25442e7b69d35de786405d2718": 2, + "12406594099aea2c0bb9cc1f50dbc14239472fea5a1143bed550996434d18866": 2, + "13978d9334f4cd7090059a6964e24ce77a7193ac873b1dc60da7d2d16d9f0041": 2, + "156593b11281fcdc753b9cf1f4b04509d62d610b5137b87e24660f9f14f4b4df": 2, + "161908d4f93158a504352f776a3223bf43518dec47c9c2a6873f7819b42212f7": 2, + "16c12bc105726fe3b95c7176917e4e50b9a7ba63fa163510d7fd1476d6bd33b0": 2, + "1703a9927a850ffa88223a2ee8bdb0b542df45e3903a8a908810bfbb1c067e2d": 2, + "170b4dc880d1df6fdfec9871829d0d946521a23579f46f8970fcae5a76039b48": 2, + "17d35aaa785946d9f53ae815454bbb7033460e7f516b4e388172d2e9eb7a2cff": 2, + "1986302cf1c0dcb04ef09db1ba885dc77d22c7fc8c3126c81971a332535b143b": 2, + "1e53d9a0461de7caa3b2055ad5745e2e43b17799f3dea8fd20b70e66abbae023": 2, + "2308b9289669ee335d7977974badc77bfff02e408578bb2bda1ed082ebcb5899": 2, + "2321445585ea741c2e54fc580e6070aae1893f9dd9a9a4f176c6e674fd907ce5": 2, + "23f8657ec2b1150021b203f0a2b7f16e78b271f5ebabd9157a388d8693b77c46": 2, + "256c723fde8eb3f27cb4113c99aae65c736e9810725d77ee3ba90ed6ec6a6e49": 2, + "265fa83b33a379d26a997332ee0c13cd4e0ee3a5bef16e8fe34f8666d68533d4": 1, + "2728b72e6a24a3691793f4003abd25d5e094cc435b4d83c5df3a27839aa5535f": 2, + "2828687e1b62525ec4159764e56c7e1aac0e73a736ae345478a3f3ccb4a274e1": 2, + "285eb34f6cb9b427a69ef2edf813dab6c3d7365d3c7476d23985c7d2e3cb8920": 2, + "299fe54dc3778075eb094a17ddb4af20f098f11f1e8f06ed1abc3718e0cd7b6b": 2, + "2c3e40e35481bca5599d9517439c7f4fc650097ce3472717e9431451c3c8b7df": 2, + "2c5903dafed9a5c65dc0fe52f3613c72adbbc9fd11712ac522dd3ccea611c5f6": 2, + "2d61a5d29ccc202f5926a69646a4b7e108f8652eba04516f0edd4253be8841df": 2, + "2f3183368096550260b2a924c1ef417b605616ea3fa28570f934547852ca0aff": 2, + "2f40fa80d125f04a18ee392f814eb102222ca7e8cca7a98c3e46fe45f668f854": 5, + "2fc86e9d449f01ddf257d775f66971be794a38ce0fd81686d06a23c1d13e1dc4": 5, + "31e2291235da90343f676808966d0cb92f31869a72351aad98b9d51da6436df8": 1, + "35fcbc8c5ccd86b23cf7c4a14b15f3912c7673501cc720969e4e040aecf3ab55": 2, + "38108a57d102f4b7e07604ebb2abc0556fe530603c4c3e5ec59197ecb5a930c9": 2, + "384b26470fef9359ad37bdb830f24292bb834d698c44f6d82e7660af5ac097dd": 2, + "3b9b34b8bee4168fc8c8aeea990af45caaaf8f87f40a850d3fba08a371056c56": 2, + "3d5f9e2cb34b3aaf9a8ca47540b1fa7604b5803015f04baf1714d77fee9faaea": 2, + "41c6a208a51089bfc29358d19526c345b50eebb00e31cb74357a1942dc0037e8": 2, + "4334e41205d6a9689d15a6380803381d25d1410152da90148575981edb83e74e": 2, + "4347487bcbe22dc124eae077ae60910fbc0e8c301394a0af5b2c109644f03c04": 2, + "4663ce2388ed7829474ada5cfda75ca08bfa656958350cd14a71c2fa2f382f3a": 2, + "4726934a486f7dc1c7b9562afbd126d072f014aef6ac4c8abb4faf5853c0eb91": 1, + "4744a77db4c9525952d7ab92088d67578f720e383736dfb4bbc343e7edcfcf5b": 2, + "47fba4cf26e8a26016cb126a9119a26314d0668d3673bbb84e956bb719d1f30c": 2, + "48a129d0240009e789671a62961a174d4e3e1429ed09f4eb68df2065e7e74cf8": 2, + "4a38ee7b095e2f9e32d792a7fedd319373b89f670eed12810b9b2cbdc467edf6": 2, + "4cfc13e9da711c4e1e010e8ce599bce72f5833396b83a68a4fa2725a5550cf71": 2, + "50959c6c78e6ff1637a35a1ef0a4ad968cdc01476c72249292d1a0518ab6f28e": 2, + "53871d2ee3b74f0646d4a87f375d3eaf7602c60167088bb8f1f807e4618215fe": 2, + "553fb8e93261d4a87c5715744b458a00c2345e95171000f8fcd007ae251643e1": 2, + "5601dbfd63e34662821d940b2112f3970768b02a0ddbb6fbb8eaa2883c677feb": 1, + "59439382d5ec30d0e14ea8e18e484ba93075165553c5ff2c2163a80e8bd483ef": 2, + "5b18c6189a09c671103fbcaadc833f429dcac90d8c4907799e0f09273b9eb2e9": 2, + "5be2f43d18b4ea2211539b458c887fb895ade8716e8165d80efc0675371eb0b7": 2, + "5c54592d14bf549e3f9e673b1d483a7dfc94f83b4d4b6b0e12ee048e75e09972": 2, + "62b593209fcbaa6995a476fe61f4a0dfae4ba20e40cdcb4814a0ca373ace26c3": 2, + "62e17b4225377d9e406d2445c9dfe4a828323e2b21119859bb3507dc77d2d836": 4, + "63ad4baae82b569b433668878c673dfad82d284aec7e19bc1c882382ebe2cc40": 2, + "6714f277d4320f9e55bf657fe38d6ef7cad78bc944313ae02e39ac5aee404932": 2, + "680bd1c4082fcc10a284b25faa4816b413dd555bc0c6890beebf1f7725b7ff0a": 2, + "688c9959732d8130b75cbef5db449e0a320b7c7bb7fce03ba7f807a316b319d1": 1, + "69b46ad693a007d0caccd4da8604c96cc53836e0b800e8d918a502713f11ceff": 1, + "6bfc05945005f15e301fc6f58eb100d1a070311872fa1139ca16d2d9fc5d5098": 2, + "6dcc773fb8f90e6febae9cbbb087b13d1fa826f76428ddd63ba96e4123450547": 2, + "715662ee0cbe9569a6035f2fdbe9616791b4aa48abbb1f141cd63851661d8ea8": 2, + "7259be419b212eac994aa869bf1393eb8566bab84b1508e6156346c6ac05b707": 2, + "755b0e8af176d05057da86f26a783dd3fa154d972524246b9488664ed0d1c8f3": 2, + "75e2bb662711cfe1508edbf31ccfb9314a65447aabf95564b5a07bfeb3af4922": 2, + "78232fea54e667db55d9c4c8384d35dc11816c311d2f20914777d8e442e1cac9": 2, + "78a22730f53e4f6b9e51c5ba82de4162252c2d8ee33f0aaf8207494945f2ea28": 2, + "79b57a918d5c53a40bdc888dcf03c9b4d98efc75215985b25506fa53a4589b59": 1, + "7d804c8cd37e7a77bfdf61c097fc7dd12baa292bbb7fc844a4ff98572229be0e": 2, + "7e3ddbed87c6dfd541e48dfe158bb0575da4319c00b2267434930ee57071c281": 2, + "7fe84977f695bc874882db321128342a5d312534ef3e80ec95431232f017267a": 4, + "8145c1cb5cca306a0738f3569c2ef267609b63be09957b129ff2249b333f00d6": 2, + "81588855089ceef5a52f0d6ab85e346102844c5d7ddf362130003e4d3fe0dd95": 2, + "81c6b4aa94e728a1ec0d215d6c2898471d84703c7a5833570da2680fd0d2b925": 2, + "85ce0287c2d96e49944bd4ffe80aa8857fa5defcce6582b74141b55bff9a34cd": 1, + "86d419b214718a5442624b3b129ea0d70458053c9579e82877b338867720dfa3": 2, + "88aedab7e8b06c5649bff7b705d43a94b8bf0334ad5d5be315aabe3525eab360": 2, + "89dae75e002619ff80177040ad08ab723b92558feea422ce65fdb2c3df5e92e2": 2, + "8bc94050cfc9b5117702b8dceb90a36e07af3d4f549dcd41b98f3f7cea948daa": 2, + "8d2846b9e43061180295d6d26c36a1d6a38b4c8d0a42a4a4e107b48823194c81": 2, + "8e954f76b21be8081f1afe22e51e17ae54a8e072929cde9599ed4e29109040e4": 2, + "9344263a944a0e23c7779eeb4bb1b0be59e3f900525dc9944c96451a737bb5a4": 2, + "93a61a026a283fba1cff62864ae364c8b910bdfdeaff3593acabc20023b89047": 2, + "94d142592cf6906ebbac4adbefd6ca9977a792b1a72f27836f7ed2a5fa5a2cc5": 2, + "968645dd3ebee60719d7533cabb2c7bf96409cf81ac1c8184a752fcf3186165f": 2, + "9b0da8bb7c263e671fef5e0f92e8056003598d9cbcfcc4528529b57da44d64db": 2, + "9bf135b3450e99e12188e9160078c82b0498872287ac45342c217aa29d77f3aa": 2, + "9d019d209cba6fbad466cfd9cffcb48741663aaba070e44f633c5b60c727d120": 2, + "9ded463715a46a309aa6d2a28ec1e6d0eb2804218c4acee14e4181a34092a690": 2, + "9f2f4418c070f988c709c8a6a84d467ab406fedaca49662d68b6d18f2439391f": 1, + "a01e9af9db63703daad6d984e1e0bd0c26f2ea99216e4a18328eb81d2f600b39": 2, + "a083f8666bfb203394d8438fe1f3e240bde5f2e8f4e90a8ffd81ea2296c7825c": 2, + "a5608094cf6025984b381f3c14fbe19b9ce249d2ec5b60865355f3134d263b3c": 2, + "a84afb74a7d6b0431983cd5e378591804aa64fb4466541600b16db2614b4eeeb": 2, + "ab26c38af67a3be223bd11afd0c12ad8a34217085472477f69fe0c4797e9cb37": 2, + "ab4adc35cea5722813b8511cb8f20011af6a4b99a1e5b7a16549ac40d7ccb063": 1, + "ac63739a302f7c4bd69fe1658326808eb87039582e58ed217b977f107eeb524e": 2, + "ad13f37e58db2969b8a958128c7f75e743b38212defa126eeffbb4253cb62a03": 2, + "ae91216f8cf4b7772a25193ea513302b137eee0bccdb34489ed82c9e2c67cde3": 1, + "b2a6611ac2a429e2768f5d84fdd8a6b34fec74f8b4444291f678c3b0416652e3": 2, + "b2b368ac05ef7d7d07479d190436fd3b6bd73cdb46feb5c24276e6463de281bb": 2, + "b4e85958e26ed93a57c0c0fae02eb124a66ec08b5bd0431d7c2c847004d5cc45": 2, + "b5dfd59a2d635def4201ea4aab6c5194c304534fd48c4f0f3fd5b02be762525e": 2, + "bab4b25441cd5775f60d640b8ecdc99f9aae537ad0ce315e69fe8eae0344e055": 1, + "bb807c79dfb99e922103775d065f7e93b20c023bade2b1d2d793592051bdba68": 2, + "bc3958f8d96171a79fe4ecfcc2b3b106d910142618ff20554c93eef33cb23fbd": 2, + "bc623aea8f48978b0079b10b8b198e8d4982eb12b2024172a66dfd3156814295": 2, + "bca7a7620fb50c38e15af20ad6603e2edfc46d597399f43c6e39630ad36042e8": 2, + "be8799f18f30223681510a72b4aee512fd71f8552761690e0dd4291e263d2082": 2, + "beed28dd40bcc2e67590093284fc11b458978dd9a7ba4ddbc564b5534d58782a": 2, + "c32985dd6d11b17134c72b0e34b6a80a63441c30826dc8160bbce53122a86927": 2, + "c727304e64fb98358d3caf259b7bf417439816ada1a90ea187a56c0c33b1a817": 2, + "c77e3d1c0109263c8accb02189c87effef64e10d7162348f4507f08e37995a7c": 2, + "ca9839646e7d1b13a65eadb86cf98db30c58548cdfa2f649687bda826994cdfb": 2, + "cc0b096d528d81c4e0074e984d9dc7b397ca1dfa6f4908df0b1f7c3d20ca5095": 2, + "cfd15b9df70668ba89f83310627b29a6c729e29ed285123bc3813c3b6636364b": 2, + "d160aa8da5157b9dc5a6ddb5987f4dfdbeff467ce758e523357c841775e439d8": 2, + "d32bef1dfd1bf7a1080d7b291a37879531ece9a8594071a269d71f196d6b1575": 2, + "d377d8ec12d1de38b2cf26d8a5f376dd05aac0daf65a56825b96ec01cdf21de8": 2, + "d5993edf895e3dd77b66b903a01bcbfa7ad62f57b3abd72714cc971f73e4534d": 2, + "d749f90459f8dc41a07ec35ae439b5df7a136a899951e716ddb5d4d164d09032": 2, + "da8d39ca18347bf316273009ae29479dc83d3a9f4fef8b0cc59d0f78746a80e4": 2, + "daab1943fd8420ab62e84f14cef45a5b25ade97723aa54ff80f87868e4086561": 2, + "db9a8a71677ca4db4b9ed1eb32c1060720b1c44ae1f5efd78522626ad30eb9ca": 2, + "e04ef970700db4dfcff4fbaa92e7147c6b5888faabad4c71438550a3d3fb8440": 2, + "e086e1a495cd74fa801da54e53000cce7c4a2ff70aba847740ff6e5cf95f5f01": 2, + "e27ae1a7f4c9c5876f6bcaec1e436f9f26ae2b5c06aac1790c8c41b7f642ebcf": 2, + "e4bf316502ed8109ce8efcf9c2c6aef3df7c5d1f40d3202406ee347ea8a9ee3c": 2, + "f25399e299ccaa7dcc26b9781d009c3876042f944126a1e785d59592182ff3bc": 2, + "f496e123fe004d0989caa87a43455f130cb0c7e49061de62ab47a59eb4afb4a4": 2, + "f66e8ff5795e9781d7c9ed00e311d7abfc26ff057a4b110b71163a95032b652c": 1, + "f6926eee6b0518eeee94dad939329d10f72a13b11c3b9a865012b1131ed58252": 2, + "fa6a09554a0acbcaebde50f4b786301738ec003a95e545fabb0f2fd85da790af": 1, + "fb128f122d4d1e5ac47e2d8c767323c77193a5008849f0c10dcde9de8dadb65d": 2, + "fb710e3017efe173d3787042a758186411cbf8bf3b51588716d6feb933aa3954": 2, + "fc0ee960e2f656a87d452901353c450abb29441121f50c6510b7e03f0542c156": 2, + "fc2dcb282c4f72726ca354be91103c1402e60349d48675fa67270665c5518367": 2, + "fd0d018dddea84bfa37142cc23f7017f6321e3862311c3f596b5f0e4638596b1": 2, + "fdce724e52112136555296585217a37e54d00884c35645c3d80015ceb447b2d8": 2, + "fdd68679068b4c8e48700440b9b07d89029d55b8e7ef1d96e49ca7b9279436be": 2, + "fe847dff4bb6ae25ebec9a7def819ec2583023552b2e88a572c481aad2d32433": 2, + "ffd05dd9b081e566e65fa965c6fdb18c5d7c1b35839090b3ae643fd8014c5295": 1 + } +} diff --git a/scripts/check-icon-path-precision.test.ts b/scripts/check-icon-path-precision.test.ts new file mode 100644 index 00000000000..d5e3d9b8cc8 --- /dev/null +++ b/scripts/check-icon-path-precision.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it } from 'vitest' +import { + comparePrecisionBaseline, + createPrecisionBaseline, + effectiveFractionDigits, + findPrecisionCandidates, +} from './check-icon-path-precision' + +const FIXTURE_PATH = '/repo/packages/emcn/src/icons/fixture.tsx' + +describe('icon path precision audit', () => { + it('accepts the two-decimal boundary and ignores geometry outside literal paths', () => { + const source = ` + const dynamicPath = 'M0.1234 1' + const unrelated = "d='M0.1234 1'" + export function SafeIcon() { + return ( + + + + + ) + } + ` + + expect(findPrecisionCandidates(source, FIXTURE_PATH)).toEqual([]) + }) + + it('finds ordinary decimals and exponents finer than a hundredth', () => { + const source = ` + export function PreciseIcon() { + return + } + ` + + const candidates = findPrecisionCandidates(source, FIXTURE_PATH) + expect(candidates).toHaveLength(1) + expect(candidates[0]).toMatchObject({ + icon: 'PreciseIcon', + maxFractionDigits: 3, + offendingNumbers: ['0.123', '1e-3'], + }) + expect(effectiveFractionDigits('2.13949e-05')).toBe(10) + }) + + it('checks literal JSX expressions and static template literals', () => { + const source = ` + export function ExpressionIcon() { + return ( + + + + + ) + } + ` + + expect(findPrecisionCandidates(source, FIXTURE_PATH)).toHaveLength(2) + }) + + it('ratchets exact legacy paths and rejects new duplicates', () => { + const original = ` + export function LegacyIcon() { + return + } + ` + const originalCandidates = findPrecisionCandidates(original, FIXTURE_PATH) + const baseline = createPrecisionBaseline(originalCandidates) + + expect(comparePrecisionBaseline(originalCandidates, baseline)).toEqual({ + unbaselined: [], + staleBaseline: [], + }) + + const duplicate = ` + export function LegacyIcon() { + return + } + ` + const comparison = comparePrecisionBaseline( + findPrecisionCandidates(duplicate, FIXTURE_PATH), + baseline + ) + expect(comparison.unbaselined).toHaveLength(1) + expect(comparison.staleBaseline).toEqual([]) + }) + + it('makes the baseline stale when legacy debt is changed or removed', () => { + const original = ` + export function LegacyIcon() { + return + } + ` + const baseline = createPrecisionBaseline(findPrecisionCandidates(original, FIXTURE_PATH)) + const changed = ` + export function LegacyIcon() { + return + } + ` + + const changedComparison = comparePrecisionBaseline( + findPrecisionCandidates(changed, FIXTURE_PATH), + baseline + ) + expect(changedComparison.unbaselined).toHaveLength(1) + expect(changedComparison.staleBaseline).toHaveLength(1) + + const cleaned = original.replace('0.123', '0.12') + const cleanedComparison = comparePrecisionBaseline( + findPrecisionCandidates(cleaned, FIXTURE_PATH), + baseline + ) + expect(cleanedComparison.unbaselined).toEqual([]) + expect(cleanedComparison.staleBaseline).toHaveLength(1) + }) +}) diff --git a/scripts/check-icon-path-precision.ts b/scripts/check-icon-path-precision.ts new file mode 100644 index 00000000000..682623db53d --- /dev/null +++ b/scripts/check-icon-path-precision.ts @@ -0,0 +1,315 @@ +#!/usr/bin/env bun +/** + * Prevents new overly precise numeric values in literal SVG icon `d` attributes. + * + * Two decimal places are enough for the reusable icons covered here: additional + * digits increase shipped source without a visible benefit. Existing legacy + * paths are recorded by exact content hash, so they may remain unchanged while + * new paths and edits to old paths must satisfy the limit. + * + * Scope is intentionally limited to the shared app/docs icon catalogs and EMCN + * icon components. SVG transforms, view boxes, dynamic path expressions, and + * page-specific artwork are not inspected because their safe precision depends + * on context. + * + * Run: `bun run check:icon-path-precision` + * Update reviewed legacy debt: `bun run scripts/check-icon-path-precision.ts --update-baseline` + */ +import { createHash } from 'node:crypto' +import { readdir, readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { parse } from '@babel/parser' + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const BASELINE_PATH = path.join(ROOT, 'scripts/check-icon-path-precision.baseline.json') +const EMCN_ICONS_DIRECTORY = path.join(ROOT, 'packages/emcn/src/icons') +const STATIC_ICON_FILES = [ + path.join(ROOT, 'apps/docs/components/icons.tsx'), + path.join(ROOT, 'apps/sim/components/icons.tsx'), +] +const SVG_NUMBER_PATTERN = /[+-]?(?:(?:\d+\.\d*)|(?:\.\d+)|(?:\d+))(?:[eE][+-]?\d+)?/g + +export const MAX_ICON_PATH_FRACTION_DIGITS = 2 + +interface LiteralPath { + icon: string + line: number + value: string +} + +export interface PrecisionCandidate { + file: string + icon: string + line: number + pathHash: string + maxFractionDigits: number + offendingNumbers: string[] +} + +export interface PrecisionBaseline { + generatedFrom: string + maxFractionDigits: number + entries: Record +} + +export interface PrecisionComparison { + unbaselined: PrecisionCandidate[] + staleBaseline: string[] +} + +function asRecord(value: unknown): Record | null { + return value !== null && typeof value === 'object' ? (value as Record) : null +} + +function jsxStringValue(attribute: Record): string | null { + const value = asRecord(attribute.value) + if (!value) return null + if (value.type === 'StringLiteral' && typeof value.value === 'string') return value.value + if (value.type !== 'JSXExpressionContainer') return null + + const expression = asRecord(value.expression) + if (!expression) return null + if (expression.type === 'StringLiteral' && typeof expression.value === 'string') { + return expression.value + } + if (expression.type !== 'TemplateLiteral') return null + + const expressions = expression.expressions + const quasis = expression.quasis + if (!Array.isArray(expressions) || expressions.length > 0 || !Array.isArray(quasis)) return null + const quasi = asRecord(quasis[0]) + const quasiValue = asRecord(quasi?.value) + if (!quasiValue) return null + if (typeof quasiValue.cooked === 'string') return quasiValue.cooked + return typeof quasiValue.raw === 'string' ? quasiValue.raw : null +} + +function iconNameAt(source: string, offset: number): string { + const before = source.slice(0, offset) + const matches = [...before.matchAll(/export (?:function|const) (\w+)\s*[=(]/g)] + return matches.length > 0 ? matches[matches.length - 1][1] : '' +} + +export function extractLiteralPaths(source: string, file: string): LiteralPath[] { + const syntaxTree = parse(source, { + sourceFilename: file, + sourceType: 'module', + plugins: ['typescript', 'jsx'], + }) + const paths: LiteralPath[] = [] + + function visit(value: unknown): void { + if (Array.isArray(value)) { + for (const entry of value) visit(entry) + return + } + const node = asRecord(value) + if (!node) return + + if (node.type === 'JSXAttribute') { + const name = asRecord(node.name) + if (name?.type === 'JSXIdentifier' && name.name === 'd') { + const pathValue = jsxStringValue(node) + if (pathValue !== null) { + const start = typeof node.start === 'number' ? node.start : 0 + const location = asRecord(node.loc) + const locationStart = asRecord(location?.start) + paths.push({ + icon: iconNameAt(source, start), + line: typeof locationStart?.line === 'number' ? locationStart.line : 1, + value: pathValue, + }) + } + } + } + + for (const [key, child] of Object.entries(node)) { + if (key === 'loc' || key === 'start' || key === 'end' || key === 'extra') continue + visit(child) + } + } + + visit(syntaxTree) + return paths +} + +/** + * Counts both digits written after the decimal point and precision introduced + * by a negative exponent. This catches values such as `1.234` and `1e-3`. + */ +export function effectiveFractionDigits(numberLiteral: string): number { + const [mantissa, exponentText] = numberLiteral.toLowerCase().split('e') + const decimalIndex = mantissa.indexOf('.') + const writtenFractionDigits = decimalIndex < 0 ? 0 : mantissa.length - decimalIndex - 1 + const exponent = exponentText === undefined ? 0 : Number.parseInt(exponentText, 10) + const exponentFractionDigits = Math.max(0, writtenFractionDigits - exponent) + return Math.max(writtenFractionDigits, exponentFractionDigits) +} + +function hashPath(pathValue: string): string { + return createHash('sha256').update(pathValue).digest('hex') +} + +function normalizedRelativePath(file: string): string { + return path.relative(ROOT, file).split(path.sep).join('/') +} + +export function findPrecisionCandidates(source: string, file: string): PrecisionCandidate[] { + const candidates: PrecisionCandidate[] = [] + for (const literalPath of extractLiteralPaths(source, file)) { + const preciseNumbers = [...literalPath.value.matchAll(SVG_NUMBER_PATTERN)] + .map((match) => match[0]) + .filter( + (numberLiteral) => effectiveFractionDigits(numberLiteral) > MAX_ICON_PATH_FRACTION_DIGITS + ) + if (preciseNumbers.length === 0) continue + + candidates.push({ + file: normalizedRelativePath(file), + icon: literalPath.icon, + line: literalPath.line, + pathHash: hashPath(literalPath.value), + maxFractionDigits: Math.max(...preciseNumbers.map(effectiveFractionDigits)), + offendingNumbers: [...new Set(preciseNumbers)].slice(0, 4), + }) + } + return candidates +} + +function baselineKey(entry: Pick): string { + return entry.pathHash +} + +export function createPrecisionBaseline(candidates: PrecisionCandidate[]): PrecisionBaseline { + const grouped = new Map() + for (const candidate of candidates) { + const key = baselineKey(candidate) + grouped.set(key, (grouped.get(key) ?? 0) + 1) + } + + return { + generatedFrom: 'shared app/docs icon catalogs and packages/emcn/src/icons', + maxFractionDigits: MAX_ICON_PATH_FRACTION_DIGITS, + entries: Object.fromEntries( + [...grouped.entries()].sort(([left], [right]) => left.localeCompare(right)) + ), + } +} + +export function comparePrecisionBaseline( + candidates: PrecisionCandidate[], + baseline: PrecisionBaseline +): PrecisionComparison { + const allowedCounts = new Map(Object.entries(baseline.entries)) + const seenCounts = new Map() + const unbaselined: PrecisionCandidate[] = [] + + for (const candidate of candidates) { + const key = baselineKey(candidate) + const seen = (seenCounts.get(key) ?? 0) + 1 + seenCounts.set(key, seen) + if (seen > (allowedCounts.get(key) ?? 0)) unbaselined.push(candidate) + } + + const staleBaseline = Object.entries(baseline.entries) + .filter(([key, occurrences]) => (seenCounts.get(key) ?? 0) < occurrences) + .map(([key]) => key) + return { unbaselined, staleBaseline } +} + +async function defaultIconFiles(): Promise { + const emcnIcons = (await readdir(EMCN_ICONS_DIRECTORY)) + .filter((file) => file.endsWith('.tsx')) + .sort() + .map((file) => path.join(EMCN_ICONS_DIRECTORY, file)) + return [...STATIC_ICON_FILES, ...emcnIcons] +} + +async function scanFiles(files: string[]): Promise { + const candidates: PrecisionCandidate[] = [] + for (const file of files) { + candidates.push(...findPrecisionCandidates(await readFile(file, 'utf8'), file)) + } + return candidates +} + +async function loadBaseline(): Promise { + return JSON.parse(await readFile(BASELINE_PATH, 'utf8')) as PrecisionBaseline +} + +function printCandidate(candidate: PrecisionCandidate): void { + console.error( + ` ${candidate.file}:${candidate.line} (${candidate.icon}) — ${candidate.maxFractionDigits} fractional digits` + ) + console.error(` values: ${candidate.offendingNumbers.join(', ')}`) +} + +async function main(): Promise { + const updateBaseline = process.argv.includes('--update-baseline') + const unknownArguments = process.argv + .slice(2) + .filter((argument) => argument !== '--update-baseline') + if (unknownArguments.length > 0) { + console.error(`Unknown argument(s): ${unknownArguments.join(', ')}`) + process.exit(1) + } + + const files = await defaultIconFiles() + const candidates = await scanFiles(files) + if (updateBaseline) { + const baseline = createPrecisionBaseline(candidates) + await writeFile(BASELINE_PATH, `${JSON.stringify(baseline, null, 2)}\n`) + console.log( + `✓ Wrote ${Object.keys(baseline.entries).length} exact legacy icon path fingerprint(s) to ${path.relative(ROOT, BASELINE_PATH)}.` + ) + return + } + + const baseline = await loadBaseline() + if (baseline.maxFractionDigits !== MAX_ICON_PATH_FRACTION_DIGITS) { + console.error( + `Icon path precision baseline uses ${baseline.maxFractionDigits} digits; checker expects ${MAX_ICON_PATH_FRACTION_DIGITS}.` + ) + process.exit(1) + } + + const comparison = comparePrecisionBaseline(candidates, baseline) + if (comparison.unbaselined.length > 0) { + console.error( + `\nFound ${comparison.unbaselined.length} new or changed icon path(s) with more than ${MAX_ICON_PATH_FRACTION_DIGITS} fractional digits:\n` + ) + for (const candidate of comparison.unbaselined) printCandidate(candidate) + console.error( + '\nRound only numeric values inside the literal d attribute to at most two decimal places.' + ) + console.error( + 'Do not round transform or viewBox values automatically; verify those geometry changes separately.' + ) + } + + if (comparison.staleBaseline.length > 0) { + console.error( + `\nThe icon path precision baseline has ${comparison.staleBaseline.length} stale entr${comparison.staleBaseline.length === 1 ? 'y' : 'ies'}.` + ) + console.error( + 'After confirming the legacy path was removed, rounded, or intentionally changed, regenerate the baseline with:' + ) + console.error(' bun run scripts/check-icon-path-precision.ts --update-baseline\n') + } + + if (comparison.unbaselined.length > 0 || comparison.staleBaseline.length > 0) { + process.exit(1) + } + + console.log( + `✓ No new overly precise icon paths (${files.length} files; ${Object.keys(baseline.entries).length} exact legacy path fingerprints ratcheted).` + ) +} + +if (import.meta.main) { + main().catch((error: unknown) => { + console.error(error) + process.exit(1) + }) +} From 9ac5bbeab29c307045b07e24aa17229b40428b3d Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:13:56 -0700 Subject: [PATCH 5/6] fix(icons): make precision exceptions local --- .github/workflows/test-build.yml | 10 + .../check-icon-path-precision.baseline.json | 160 -------- scripts/check-icon-path-precision.test.ts | 110 ++++-- scripts/check-icon-path-precision.ts | 360 ++++++++++++------ scripts/run-audits.ts | 1 + 5 files changed, 337 insertions(+), 304 deletions(-) delete mode 100644 scripts/check-icon-path-precision.baseline.json diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 80a0ec62352..772d2031bf5 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -123,6 +123,16 @@ jobs: - name: Repo audits run: bun run check:audits + - name: Check SVG icon path precision + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE_REF="origin/${{ github.base_ref }}" + git fetch --depth=1 origin "${{ github.base_ref }}" 2>/dev/null || true + else + BASE_REF="HEAD~1" + fi + bun run check:icon-path-precision "$BASE_REF" + - name: Migration safety (zero-downtime) audit run: | if [ "${{ github.event_name }}" = "pull_request" ]; then diff --git a/scripts/check-icon-path-precision.baseline.json b/scripts/check-icon-path-precision.baseline.json deleted file mode 100644 index 4ccb5e1c72b..00000000000 --- a/scripts/check-icon-path-precision.baseline.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "generatedFrom": "shared app/docs icon catalogs and packages/emcn/src/icons", - "maxFractionDigits": 2, - "entries": { - "01d9575ed69da153ef3b9e8e493584f212a979740a1cf824106889f31856a61b": 2, - "0391deca941d91d0f2abb90759450c5e608fa52747d0adeda05c90bb000ad4f2": 2, - "04866b8e0cc7027bc039520f77601469b0180d547fe1e0eb85200b4b5ca9bb1b": 2, - "051c689d7ae3af5a09c7512f3f2adac5f45d34c742dc2eb3615e5f5f6e017b94": 2, - "058b19b1afecf1e0a0982fd3f1d81a9260726cd17ccc2a04334f5867dd34661c": 2, - "0762e29006cf29fe941495d66d0a868cb699d269f054809774cd9ddd82342f67": 2, - "08228705b5f098a11779a97b460d7879606acc22b728fd52d0d6ad94416be328": 2, - "08ad1b8fc90daa6c7b1a623fd817682cdfea5ee81a634065e8a00c1c107756ea": 2, - "0992becea53293ef87801c3eb1f3d74ee09aa66bd42bab2c4ce39d95832f78fb": 2, - "0b54c1bbff031268071ae4d92e9c30b2fd5b6615d8513e4e096a26e982b8304e": 1, - "0b62d7cffbf245f5c2c087e97f0f7bc95bb74eae02cc397e1698464cfa227bab": 1, - "0b748a7626643a5125c304b155a706ad52efdba330799fa501c3288f04bcc148": 2, - "0fdefc9585999fa87abc33859b23bccfd33ccf02362a57e9fcbdcfc170e98c54": 2, - "1050dcf5409d0f27455b2d006c493ed16b861ff3f4271c8e312baf163ae9e527": 2, - "10c9a483f4506b4fe074140fd5ae571555bc6b25442e7b69d35de786405d2718": 2, - "12406594099aea2c0bb9cc1f50dbc14239472fea5a1143bed550996434d18866": 2, - "13978d9334f4cd7090059a6964e24ce77a7193ac873b1dc60da7d2d16d9f0041": 2, - "156593b11281fcdc753b9cf1f4b04509d62d610b5137b87e24660f9f14f4b4df": 2, - "161908d4f93158a504352f776a3223bf43518dec47c9c2a6873f7819b42212f7": 2, - "16c12bc105726fe3b95c7176917e4e50b9a7ba63fa163510d7fd1476d6bd33b0": 2, - "1703a9927a850ffa88223a2ee8bdb0b542df45e3903a8a908810bfbb1c067e2d": 2, - "170b4dc880d1df6fdfec9871829d0d946521a23579f46f8970fcae5a76039b48": 2, - "17d35aaa785946d9f53ae815454bbb7033460e7f516b4e388172d2e9eb7a2cff": 2, - "1986302cf1c0dcb04ef09db1ba885dc77d22c7fc8c3126c81971a332535b143b": 2, - "1e53d9a0461de7caa3b2055ad5745e2e43b17799f3dea8fd20b70e66abbae023": 2, - "2308b9289669ee335d7977974badc77bfff02e408578bb2bda1ed082ebcb5899": 2, - "2321445585ea741c2e54fc580e6070aae1893f9dd9a9a4f176c6e674fd907ce5": 2, - "23f8657ec2b1150021b203f0a2b7f16e78b271f5ebabd9157a388d8693b77c46": 2, - "256c723fde8eb3f27cb4113c99aae65c736e9810725d77ee3ba90ed6ec6a6e49": 2, - "265fa83b33a379d26a997332ee0c13cd4e0ee3a5bef16e8fe34f8666d68533d4": 1, - "2728b72e6a24a3691793f4003abd25d5e094cc435b4d83c5df3a27839aa5535f": 2, - "2828687e1b62525ec4159764e56c7e1aac0e73a736ae345478a3f3ccb4a274e1": 2, - "285eb34f6cb9b427a69ef2edf813dab6c3d7365d3c7476d23985c7d2e3cb8920": 2, - "299fe54dc3778075eb094a17ddb4af20f098f11f1e8f06ed1abc3718e0cd7b6b": 2, - "2c3e40e35481bca5599d9517439c7f4fc650097ce3472717e9431451c3c8b7df": 2, - "2c5903dafed9a5c65dc0fe52f3613c72adbbc9fd11712ac522dd3ccea611c5f6": 2, - "2d61a5d29ccc202f5926a69646a4b7e108f8652eba04516f0edd4253be8841df": 2, - "2f3183368096550260b2a924c1ef417b605616ea3fa28570f934547852ca0aff": 2, - "2f40fa80d125f04a18ee392f814eb102222ca7e8cca7a98c3e46fe45f668f854": 5, - "2fc86e9d449f01ddf257d775f66971be794a38ce0fd81686d06a23c1d13e1dc4": 5, - "31e2291235da90343f676808966d0cb92f31869a72351aad98b9d51da6436df8": 1, - "35fcbc8c5ccd86b23cf7c4a14b15f3912c7673501cc720969e4e040aecf3ab55": 2, - "38108a57d102f4b7e07604ebb2abc0556fe530603c4c3e5ec59197ecb5a930c9": 2, - "384b26470fef9359ad37bdb830f24292bb834d698c44f6d82e7660af5ac097dd": 2, - "3b9b34b8bee4168fc8c8aeea990af45caaaf8f87f40a850d3fba08a371056c56": 2, - "3d5f9e2cb34b3aaf9a8ca47540b1fa7604b5803015f04baf1714d77fee9faaea": 2, - "41c6a208a51089bfc29358d19526c345b50eebb00e31cb74357a1942dc0037e8": 2, - "4334e41205d6a9689d15a6380803381d25d1410152da90148575981edb83e74e": 2, - "4347487bcbe22dc124eae077ae60910fbc0e8c301394a0af5b2c109644f03c04": 2, - "4663ce2388ed7829474ada5cfda75ca08bfa656958350cd14a71c2fa2f382f3a": 2, - "4726934a486f7dc1c7b9562afbd126d072f014aef6ac4c8abb4faf5853c0eb91": 1, - "4744a77db4c9525952d7ab92088d67578f720e383736dfb4bbc343e7edcfcf5b": 2, - "47fba4cf26e8a26016cb126a9119a26314d0668d3673bbb84e956bb719d1f30c": 2, - "48a129d0240009e789671a62961a174d4e3e1429ed09f4eb68df2065e7e74cf8": 2, - "4a38ee7b095e2f9e32d792a7fedd319373b89f670eed12810b9b2cbdc467edf6": 2, - "4cfc13e9da711c4e1e010e8ce599bce72f5833396b83a68a4fa2725a5550cf71": 2, - "50959c6c78e6ff1637a35a1ef0a4ad968cdc01476c72249292d1a0518ab6f28e": 2, - "53871d2ee3b74f0646d4a87f375d3eaf7602c60167088bb8f1f807e4618215fe": 2, - "553fb8e93261d4a87c5715744b458a00c2345e95171000f8fcd007ae251643e1": 2, - "5601dbfd63e34662821d940b2112f3970768b02a0ddbb6fbb8eaa2883c677feb": 1, - "59439382d5ec30d0e14ea8e18e484ba93075165553c5ff2c2163a80e8bd483ef": 2, - "5b18c6189a09c671103fbcaadc833f429dcac90d8c4907799e0f09273b9eb2e9": 2, - "5be2f43d18b4ea2211539b458c887fb895ade8716e8165d80efc0675371eb0b7": 2, - "5c54592d14bf549e3f9e673b1d483a7dfc94f83b4d4b6b0e12ee048e75e09972": 2, - "62b593209fcbaa6995a476fe61f4a0dfae4ba20e40cdcb4814a0ca373ace26c3": 2, - "62e17b4225377d9e406d2445c9dfe4a828323e2b21119859bb3507dc77d2d836": 4, - "63ad4baae82b569b433668878c673dfad82d284aec7e19bc1c882382ebe2cc40": 2, - "6714f277d4320f9e55bf657fe38d6ef7cad78bc944313ae02e39ac5aee404932": 2, - "680bd1c4082fcc10a284b25faa4816b413dd555bc0c6890beebf1f7725b7ff0a": 2, - "688c9959732d8130b75cbef5db449e0a320b7c7bb7fce03ba7f807a316b319d1": 1, - "69b46ad693a007d0caccd4da8604c96cc53836e0b800e8d918a502713f11ceff": 1, - "6bfc05945005f15e301fc6f58eb100d1a070311872fa1139ca16d2d9fc5d5098": 2, - "6dcc773fb8f90e6febae9cbbb087b13d1fa826f76428ddd63ba96e4123450547": 2, - "715662ee0cbe9569a6035f2fdbe9616791b4aa48abbb1f141cd63851661d8ea8": 2, - "7259be419b212eac994aa869bf1393eb8566bab84b1508e6156346c6ac05b707": 2, - "755b0e8af176d05057da86f26a783dd3fa154d972524246b9488664ed0d1c8f3": 2, - "75e2bb662711cfe1508edbf31ccfb9314a65447aabf95564b5a07bfeb3af4922": 2, - "78232fea54e667db55d9c4c8384d35dc11816c311d2f20914777d8e442e1cac9": 2, - "78a22730f53e4f6b9e51c5ba82de4162252c2d8ee33f0aaf8207494945f2ea28": 2, - "79b57a918d5c53a40bdc888dcf03c9b4d98efc75215985b25506fa53a4589b59": 1, - "7d804c8cd37e7a77bfdf61c097fc7dd12baa292bbb7fc844a4ff98572229be0e": 2, - "7e3ddbed87c6dfd541e48dfe158bb0575da4319c00b2267434930ee57071c281": 2, - "7fe84977f695bc874882db321128342a5d312534ef3e80ec95431232f017267a": 4, - "8145c1cb5cca306a0738f3569c2ef267609b63be09957b129ff2249b333f00d6": 2, - "81588855089ceef5a52f0d6ab85e346102844c5d7ddf362130003e4d3fe0dd95": 2, - "81c6b4aa94e728a1ec0d215d6c2898471d84703c7a5833570da2680fd0d2b925": 2, - "85ce0287c2d96e49944bd4ffe80aa8857fa5defcce6582b74141b55bff9a34cd": 1, - "86d419b214718a5442624b3b129ea0d70458053c9579e82877b338867720dfa3": 2, - "88aedab7e8b06c5649bff7b705d43a94b8bf0334ad5d5be315aabe3525eab360": 2, - "89dae75e002619ff80177040ad08ab723b92558feea422ce65fdb2c3df5e92e2": 2, - "8bc94050cfc9b5117702b8dceb90a36e07af3d4f549dcd41b98f3f7cea948daa": 2, - "8d2846b9e43061180295d6d26c36a1d6a38b4c8d0a42a4a4e107b48823194c81": 2, - "8e954f76b21be8081f1afe22e51e17ae54a8e072929cde9599ed4e29109040e4": 2, - "9344263a944a0e23c7779eeb4bb1b0be59e3f900525dc9944c96451a737bb5a4": 2, - "93a61a026a283fba1cff62864ae364c8b910bdfdeaff3593acabc20023b89047": 2, - "94d142592cf6906ebbac4adbefd6ca9977a792b1a72f27836f7ed2a5fa5a2cc5": 2, - "968645dd3ebee60719d7533cabb2c7bf96409cf81ac1c8184a752fcf3186165f": 2, - "9b0da8bb7c263e671fef5e0f92e8056003598d9cbcfcc4528529b57da44d64db": 2, - "9bf135b3450e99e12188e9160078c82b0498872287ac45342c217aa29d77f3aa": 2, - "9d019d209cba6fbad466cfd9cffcb48741663aaba070e44f633c5b60c727d120": 2, - "9ded463715a46a309aa6d2a28ec1e6d0eb2804218c4acee14e4181a34092a690": 2, - "9f2f4418c070f988c709c8a6a84d467ab406fedaca49662d68b6d18f2439391f": 1, - "a01e9af9db63703daad6d984e1e0bd0c26f2ea99216e4a18328eb81d2f600b39": 2, - "a083f8666bfb203394d8438fe1f3e240bde5f2e8f4e90a8ffd81ea2296c7825c": 2, - "a5608094cf6025984b381f3c14fbe19b9ce249d2ec5b60865355f3134d263b3c": 2, - "a84afb74a7d6b0431983cd5e378591804aa64fb4466541600b16db2614b4eeeb": 2, - "ab26c38af67a3be223bd11afd0c12ad8a34217085472477f69fe0c4797e9cb37": 2, - "ab4adc35cea5722813b8511cb8f20011af6a4b99a1e5b7a16549ac40d7ccb063": 1, - "ac63739a302f7c4bd69fe1658326808eb87039582e58ed217b977f107eeb524e": 2, - "ad13f37e58db2969b8a958128c7f75e743b38212defa126eeffbb4253cb62a03": 2, - "ae91216f8cf4b7772a25193ea513302b137eee0bccdb34489ed82c9e2c67cde3": 1, - "b2a6611ac2a429e2768f5d84fdd8a6b34fec74f8b4444291f678c3b0416652e3": 2, - "b2b368ac05ef7d7d07479d190436fd3b6bd73cdb46feb5c24276e6463de281bb": 2, - "b4e85958e26ed93a57c0c0fae02eb124a66ec08b5bd0431d7c2c847004d5cc45": 2, - "b5dfd59a2d635def4201ea4aab6c5194c304534fd48c4f0f3fd5b02be762525e": 2, - "bab4b25441cd5775f60d640b8ecdc99f9aae537ad0ce315e69fe8eae0344e055": 1, - "bb807c79dfb99e922103775d065f7e93b20c023bade2b1d2d793592051bdba68": 2, - "bc3958f8d96171a79fe4ecfcc2b3b106d910142618ff20554c93eef33cb23fbd": 2, - "bc623aea8f48978b0079b10b8b198e8d4982eb12b2024172a66dfd3156814295": 2, - "bca7a7620fb50c38e15af20ad6603e2edfc46d597399f43c6e39630ad36042e8": 2, - "be8799f18f30223681510a72b4aee512fd71f8552761690e0dd4291e263d2082": 2, - "beed28dd40bcc2e67590093284fc11b458978dd9a7ba4ddbc564b5534d58782a": 2, - "c32985dd6d11b17134c72b0e34b6a80a63441c30826dc8160bbce53122a86927": 2, - "c727304e64fb98358d3caf259b7bf417439816ada1a90ea187a56c0c33b1a817": 2, - "c77e3d1c0109263c8accb02189c87effef64e10d7162348f4507f08e37995a7c": 2, - "ca9839646e7d1b13a65eadb86cf98db30c58548cdfa2f649687bda826994cdfb": 2, - "cc0b096d528d81c4e0074e984d9dc7b397ca1dfa6f4908df0b1f7c3d20ca5095": 2, - "cfd15b9df70668ba89f83310627b29a6c729e29ed285123bc3813c3b6636364b": 2, - "d160aa8da5157b9dc5a6ddb5987f4dfdbeff467ce758e523357c841775e439d8": 2, - "d32bef1dfd1bf7a1080d7b291a37879531ece9a8594071a269d71f196d6b1575": 2, - "d377d8ec12d1de38b2cf26d8a5f376dd05aac0daf65a56825b96ec01cdf21de8": 2, - "d5993edf895e3dd77b66b903a01bcbfa7ad62f57b3abd72714cc971f73e4534d": 2, - "d749f90459f8dc41a07ec35ae439b5df7a136a899951e716ddb5d4d164d09032": 2, - "da8d39ca18347bf316273009ae29479dc83d3a9f4fef8b0cc59d0f78746a80e4": 2, - "daab1943fd8420ab62e84f14cef45a5b25ade97723aa54ff80f87868e4086561": 2, - "db9a8a71677ca4db4b9ed1eb32c1060720b1c44ae1f5efd78522626ad30eb9ca": 2, - "e04ef970700db4dfcff4fbaa92e7147c6b5888faabad4c71438550a3d3fb8440": 2, - "e086e1a495cd74fa801da54e53000cce7c4a2ff70aba847740ff6e5cf95f5f01": 2, - "e27ae1a7f4c9c5876f6bcaec1e436f9f26ae2b5c06aac1790c8c41b7f642ebcf": 2, - "e4bf316502ed8109ce8efcf9c2c6aef3df7c5d1f40d3202406ee347ea8a9ee3c": 2, - "f25399e299ccaa7dcc26b9781d009c3876042f944126a1e785d59592182ff3bc": 2, - "f496e123fe004d0989caa87a43455f130cb0c7e49061de62ab47a59eb4afb4a4": 2, - "f66e8ff5795e9781d7c9ed00e311d7abfc26ff057a4b110b71163a95032b652c": 1, - "f6926eee6b0518eeee94dad939329d10f72a13b11c3b9a865012b1131ed58252": 2, - "fa6a09554a0acbcaebde50f4b786301738ec003a95e545fabb0f2fd85da790af": 1, - "fb128f122d4d1e5ac47e2d8c767323c77193a5008849f0c10dcde9de8dadb65d": 2, - "fb710e3017efe173d3787042a758186411cbf8bf3b51588716d6feb933aa3954": 2, - "fc0ee960e2f656a87d452901353c450abb29441121f50c6510b7e03f0542c156": 2, - "fc2dcb282c4f72726ca354be91103c1402e60349d48675fa67270665c5518367": 2, - "fd0d018dddea84bfa37142cc23f7017f6321e3862311c3f596b5f0e4638596b1": 2, - "fdce724e52112136555296585217a37e54d00884c35645c3d80015ceb447b2d8": 2, - "fdd68679068b4c8e48700440b9b07d89029d55b8e7ef1d96e49ca7b9279436be": 2, - "fe847dff4bb6ae25ebec9a7def819ec2583023552b2e88a572c481aad2d32433": 2, - "ffd05dd9b081e566e65fa965c6fdb18c5d7c1b35839090b3ae643fd8014c5295": 1 - } -} diff --git a/scripts/check-icon-path-precision.test.ts b/scripts/check-icon-path-precision.test.ts index d5e3d9b8cc8..63d0a0e64c2 100644 --- a/scripts/check-icon-path-precision.test.ts +++ b/scripts/check-icon-path-precision.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest' import { - comparePrecisionBaseline, - createPrecisionBaseline, + analyzeIconSource, effectiveFractionDigits, + findNewPrecisionCandidates, findPrecisionCandidates, } from './check-icon-path-precision' @@ -23,7 +23,10 @@ describe('icon path precision audit', () => { } ` - expect(findPrecisionCandidates(source, FIXTURE_PATH)).toEqual([]) + expect(analyzeIconSource(source, FIXTURE_PATH)).toEqual({ + candidates: [], + invalidExceptions: [], + }) }) it('finds ordinary decimals and exponents finer than a hundredth', () => { @@ -58,59 +61,90 @@ describe('icon path precision audit', () => { expect(findPrecisionCandidates(source, FIXTURE_PATH)).toHaveLength(2) }) - it('ratchets exact legacy paths and rejects new duplicates', () => { - const original = ` - export function LegacyIcon() { - return + it('accepts a reasoned TSDoc exception on the immediately following path', () => { + const source = ` + export function PreciseBrandIcon() { + return ( + + {/** + * svg-path-precision-exception: Rounding visibly distorts the provider-authored mark. + */} + + + ) } ` - const originalCandidates = findPrecisionCandidates(original, FIXTURE_PATH) - const baseline = createPrecisionBaseline(originalCandidates) - expect(comparePrecisionBaseline(originalCandidates, baseline)).toEqual({ - unbaselined: [], - staleBaseline: [], + expect(analyzeIconSource(source, FIXTURE_PATH)).toEqual({ + candidates: [], + invalidExceptions: [], }) + }) - const duplicate = ` - export function LegacyIcon() { - return + it('rejects exceptions without a reason and exceptions on already-clean paths', () => { + const missingReason = ` + export function PreciseIcon() { + return {/** svg-path-precision-exception: */} } ` - const comparison = comparePrecisionBaseline( - findPrecisionCandidates(duplicate, FIXTURE_PATH), - baseline - ) - expect(comparison.unbaselined).toHaveLength(1) - expect(comparison.staleBaseline).toEqual([]) + const unnecessary = ` + export function CleanIcon() { + return {/** svg-path-precision-exception: Keep detail. */} + } + ` + + const missingReasonAnalysis = analyzeIconSource(missingReason, FIXTURE_PATH) + expect(missingReasonAnalysis.candidates).toHaveLength(1) + expect(missingReasonAnalysis.invalidExceptions[0]?.message).toContain('specific reason') + + const unnecessaryAnalysis = analyzeIconSource(unnecessary, FIXTURE_PATH) + expect(unnecessaryAnalysis.candidates).toEqual([]) + expect(unnecessaryAnalysis.invalidExceptions[0]?.message).toContain('unnecessary') }) - it('makes the baseline stale when legacy debt is changed or removed', () => { - const original = ` + it('grandfathers exact paths from the target branch but rejects additions and edits', () => { + const base = ` export function LegacyIcon() { return } ` - const baseline = createPrecisionBaseline(findPrecisionCandidates(original, FIXTURE_PATH)) - const changed = ` + const unchanged = findPrecisionCandidates(base, FIXTURE_PATH) + expect(findNewPrecisionCandidates(unchanged, unchanged)).toEqual([]) + + const duplicate = ` export function LegacyIcon() { - return + return } ` + expect( + findNewPrecisionCandidates(findPrecisionCandidates(duplicate, FIXTURE_PATH), unchanged) + ).toHaveLength(1) - const changedComparison = comparePrecisionBaseline( - findPrecisionCandidates(changed, FIXTURE_PATH), - baseline - ) - expect(changedComparison.unbaselined).toHaveLength(1) - expect(changedComparison.staleBaseline).toHaveLength(1) + const changed = base.replace('0.123', '0.1234') + expect( + findNewPrecisionCandidates(findPrecisionCandidates(changed, FIXTURE_PATH), unchanged) + ).toHaveLength(1) + }) - const cleaned = original.replace('0.123', '0.12') - const cleanedComparison = comparePrecisionBaseline( - findPrecisionCandidates(cleaned, FIXTURE_PATH), - baseline + it('requires a committed exception to remain while its precise path remains', () => { + const excepted = ` + export function PreciseBrandIcon() { + return ( + + {/** svg-path-precision-exception: Rounding visibly distorts the brand mark. */} + + + ) + } + ` + const exceptionRemoved = excepted.replace( + '{/** svg-path-precision-exception: Rounding visibly distorts the brand mark. */}', + '' ) - expect(cleanedComparison.unbaselined).toEqual([]) - expect(cleanedComparison.staleBaseline).toHaveLength(1) + + expect(findPrecisionCandidates(excepted, FIXTURE_PATH)).toEqual([]) + expect( + findNewPrecisionCandidates(findPrecisionCandidates(exceptionRemoved, FIXTURE_PATH), []) + ).toHaveLength(1) }) }) diff --git a/scripts/check-icon-path-precision.ts b/scripts/check-icon-path-precision.ts index 682623db53d..a49236603e4 100644 --- a/scripts/check-icon-path-precision.ts +++ b/scripts/check-icon-path-precision.ts @@ -1,38 +1,45 @@ #!/usr/bin/env bun /** - * Prevents new overly precise numeric values in literal SVG icon `d` attributes. + * Prevents newly introduced overly precise numeric values in literal SVG icon + * `d` attributes. * * Two decimal places are enough for the reusable icons covered here: additional - * digits increase shipped source without a visible benefit. Existing legacy - * paths are recorded by exact content hash, so they may remain unchanged while - * new paths and edits to old paths must satisfy the limit. + * digits increase shipped source without a visible benefit. Paths already in the + * target branch are grandfathered by exact content, while new paths and edits to + * old paths must satisfy the limit or carry a reasoned local exception. * * Scope is intentionally limited to the shared app/docs icon catalogs and EMCN * icon components. SVG transforms, view boxes, dynamic path expressions, and * page-specific artwork are not inspected because their safe precision depends * on context. * - * Run: `bun run check:icon-path-precision` - * Update reviewed legacy debt: `bun run scripts/check-icon-path-precision.ts --update-baseline` + * Run against the intended merge target: `bun run check:icon-path-precision staging` */ import { createHash } from 'node:crypto' -import { readdir, readFile, writeFile } from 'node:fs/promises' +import { readdir, readFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' import { parse } from '@babel/parser' const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') -const BASELINE_PATH = path.join(ROOT, 'scripts/check-icon-path-precision.baseline.json') const EMCN_ICONS_DIRECTORY = path.join(ROOT, 'packages/emcn/src/icons') const STATIC_ICON_FILES = [ path.join(ROOT, 'apps/docs/components/icons.tsx'), path.join(ROOT, 'apps/sim/components/icons.tsx'), ] +const STATIC_ICON_PATHS = STATIC_ICON_FILES.map((file) => normalizedRelativePath(file)) const SVG_NUMBER_PATTERN = /[+-]?(?:(?:\d+\.\d*)|(?:\.\d+)|(?:\d+))(?:[eE][+-]?\d+)?/g +const PRECISION_EXCEPTION_DIRECTIVE = 'svg-path-precision-exception:' export const MAX_ICON_PATH_FRACTION_DIGITS = 2 +interface ParsedPrecisionException { + line: number + reason: string | null +} + interface LiteralPath { + exception: ParsedPrecisionException | null icon: string line: number value: string @@ -47,15 +54,20 @@ export interface PrecisionCandidate { offendingNumbers: string[] } -export interface PrecisionBaseline { - generatedFrom: string - maxFractionDigits: number - entries: Record +export interface InvalidPrecisionException { + file: string + line: number + message: string +} + +export interface IconPrecisionAnalysis { + candidates: PrecisionCandidate[] + invalidExceptions: InvalidPrecisionException[] } -export interface PrecisionComparison { - unbaselined: PrecisionCandidate[] - staleBaseline: string[] +interface ExtractedPaths { + paths: LiteralPath[] + invalidExceptions: Omit[] } function asRecord(value: unknown): Record | null { @@ -91,15 +103,97 @@ function iconNameAt(source: string, offset: number): string { return matches.length > 0 ? matches[matches.length - 1][1] : '' } -export function extractLiteralPaths(source: string, file: string): LiteralPath[] { +function nodeLine(node: Record): number { + const location = asRecord(node.loc) + const start = asRecord(location?.start) + return typeof start?.line === 'number' ? start.line : 1 +} + +function jsxElementName(node: Record): string | null { + const openingElement = asRecord(node.openingElement) + const name = asRecord(openingElement?.name) + return name?.type === 'JSXIdentifier' && typeof name.name === 'string' ? name.name : null +} + +function normalizedComment(comment: Record): string { + if (typeof comment.value !== 'string') return '' + return comment.value + .split('\n') + .map((line) => line.replace(/^\s*\*?\s?/, '').trim()) + .filter(Boolean) + .join(' ') +} + +function precisionExceptionFromChild( + child: Record, + source: string +): ParsedPrecisionException | null { + if (child.type !== 'JSXExpressionContainer') return null + const expression = asRecord(child.expression) + if (expression?.type !== 'JSXEmptyExpression') return null + const comments = expression.innerComments + if (!Array.isArray(comments)) return null + + for (const value of comments) { + const comment = asRecord(value) + if (!comment) continue + const start = typeof comment.start === 'number' ? comment.start : -1 + const end = typeof comment.end === 'number' ? comment.end : -1 + if (start < 0 || end < 0 || !source.slice(start, end).startsWith('/**')) continue + const text = normalizedComment(comment) + if (!text.startsWith(PRECISION_EXCEPTION_DIRECTIVE)) continue + const reason = text.slice(PRECISION_EXCEPTION_DIRECTIVE.length).trim() + return { line: nodeLine(comment), reason: reason || null } + } + return null +} + +function extractLiteralPaths(source: string, file: string): ExtractedPaths { const syntaxTree = parse(source, { sourceFilename: file, sourceType: 'module', plugins: ['typescript', 'jsx'], }) const paths: LiteralPath[] = [] + const invalidExceptions: Omit[] = [] + + function invalidate(exception: ParsedPrecisionException, message: string): void { + invalidExceptions.push({ line: exception.line, message }) + } - function visit(value: unknown): void { + function visitChildren(children: unknown): void { + if (!Array.isArray(children)) return + let pendingException: ParsedPrecisionException | null = null + + for (const value of children) { + const child = asRecord(value) + if (!child) continue + if (child.type === 'JSXText' && typeof child.value === 'string' && !child.value.trim()) + continue + + const exception = precisionExceptionFromChild(child, source) + if (exception) { + if (pendingException) { + invalidate(pendingException, 'Exception must immediately precede one literal .') + } + pendingException = exception + continue + } + + if (pendingException && (child.type !== 'JSXElement' || jsxElementName(child) !== 'path')) { + invalidate(pendingException, 'Exception must immediately precede one literal .') + pendingException = null + } + visit(child, pendingException) + pendingException = null + } + + if (pendingException) { + invalidate(pendingException, 'Exception must immediately precede one literal .') + } + } + + function visit(value: unknown, exception: ParsedPrecisionException | null = null): void { if (Array.isArray(value)) { for (const entry of value) visit(entry) return @@ -107,21 +201,41 @@ export function extractLiteralPaths(source: string, file: string): LiteralPath[] const node = asRecord(value) if (!node) return - if (node.type === 'JSXAttribute') { - const name = asRecord(node.name) - if (name?.type === 'JSXIdentifier' && name.name === 'd') { - const pathValue = jsxStringValue(node) + if (node.type === 'JSXElement') { + const openingElement = asRecord(node.openingElement) + if (jsxElementName(node) === 'path' && openingElement) { + const attributes = openingElement.attributes + const dAttribute = Array.isArray(attributes) + ? attributes.map(asRecord).find((attribute) => { + const name = asRecord(attribute?.name) + return name?.type === 'JSXIdentifier' && name.name === 'd' + }) + : null + const pathValue = dAttribute ? jsxStringValue(dAttribute) : null if (pathValue !== null) { - const start = typeof node.start === 'number' ? node.start : 0 - const location = asRecord(node.loc) - const locationStart = asRecord(location?.start) + const start = typeof openingElement.start === 'number' ? openingElement.start : 0 paths.push({ + exception, icon: iconNameAt(source, start), - line: typeof locationStart?.line === 'number' ? locationStart.line : 1, + line: nodeLine(dAttribute ?? openingElement), value: pathValue, }) + } else if (exception) { + invalidate(exception, 'Exception applies only to a literal value.') } + } else if (exception) { + invalidate(exception, 'Exception must immediately precede one literal .') } + visitChildren(node.children) + return + } + + if (node.type === 'JSXFragment') { + if (exception) { + invalidate(exception, 'Exception must immediately precede one literal .') + } + visitChildren(node.children) + return } for (const [key, child] of Object.entries(node)) { @@ -131,7 +245,7 @@ export function extractLiteralPaths(source: string, file: string): LiteralPath[] } visit(syntaxTree) - return paths + return { paths, invalidExceptions } } /** @@ -155,18 +269,43 @@ function normalizedRelativePath(file: string): string { return path.relative(ROOT, file).split(path.sep).join('/') } -export function findPrecisionCandidates(source: string, file: string): PrecisionCandidate[] { +export function analyzeIconSource(source: string, file: string): IconPrecisionAnalysis { + const extracted = extractLiteralPaths(source, file) const candidates: PrecisionCandidate[] = [] - for (const literalPath of extractLiteralPaths(source, file)) { + const normalizedFile = normalizedRelativePath(file) + const invalidExceptions = extracted.invalidExceptions.map((exception) => ({ + ...exception, + file: normalizedFile, + })) + + for (const literalPath of extracted.paths) { const preciseNumbers = [...literalPath.value.matchAll(SVG_NUMBER_PATTERN)] .map((match) => match[0]) .filter( (numberLiteral) => effectiveFractionDigits(numberLiteral) > MAX_ICON_PATH_FRACTION_DIGITS ) - if (preciseNumbers.length === 0) continue + if (literalPath.exception) { + if (!literalPath.exception.reason) { + invalidExceptions.push({ + file: normalizedFile, + line: literalPath.exception.line, + message: 'Exception must include a specific reason after the colon.', + }) + } else if (preciseNumbers.length === 0) { + invalidExceptions.push({ + file: normalizedFile, + line: literalPath.exception.line, + message: 'Exception is unnecessary because this path uses at most two decimal places.', + }) + } else { + continue + } + } + + if (preciseNumbers.length === 0) continue candidates.push({ - file: normalizedRelativePath(file), + file: normalizedFile, icon: literalPath.icon, line: literalPath.line, pathHash: hashPath(literalPath.value), @@ -174,51 +313,32 @@ export function findPrecisionCandidates(source: string, file: string): Precision offendingNumbers: [...new Set(preciseNumbers)].slice(0, 4), }) } - return candidates -} -function baselineKey(entry: Pick): string { - return entry.pathHash + return { candidates, invalidExceptions } } -export function createPrecisionBaseline(candidates: PrecisionCandidate[]): PrecisionBaseline { - const grouped = new Map() - for (const candidate of candidates) { - const key = baselineKey(candidate) - grouped.set(key, (grouped.get(key) ?? 0) + 1) - } - - return { - generatedFrom: 'shared app/docs icon catalogs and packages/emcn/src/icons', - maxFractionDigits: MAX_ICON_PATH_FRACTION_DIGITS, - entries: Object.fromEntries( - [...grouped.entries()].sort(([left], [right]) => left.localeCompare(right)) - ), - } +export function findPrecisionCandidates(source: string, file: string): PrecisionCandidate[] { + return analyzeIconSource(source, file).candidates } -export function comparePrecisionBaseline( - candidates: PrecisionCandidate[], - baseline: PrecisionBaseline -): PrecisionComparison { - const allowedCounts = new Map(Object.entries(baseline.entries)) - const seenCounts = new Map() - const unbaselined: PrecisionCandidate[] = [] - - for (const candidate of candidates) { - const key = baselineKey(candidate) - const seen = (seenCounts.get(key) ?? 0) + 1 - seenCounts.set(key, seen) - if (seen > (allowedCounts.get(key) ?? 0)) unbaselined.push(candidate) +export function findNewPrecisionCandidates( + current: PrecisionCandidate[], + base: PrecisionCandidate[] +): PrecisionCandidate[] { + const allowedCounts = new Map() + for (const candidate of base) { + allowedCounts.set(candidate.pathHash, (allowedCounts.get(candidate.pathHash) ?? 0) + 1) } - const staleBaseline = Object.entries(baseline.entries) - .filter(([key, occurrences]) => (seenCounts.get(key) ?? 0) < occurrences) - .map(([key]) => key) - return { unbaselined, staleBaseline } + const seenCounts = new Map() + return current.filter((candidate) => { + const seen = (seenCounts.get(candidate.pathHash) ?? 0) + 1 + seenCounts.set(candidate.pathHash, seen) + return seen > (allowedCounts.get(candidate.pathHash) ?? 0) + }) } -async function defaultIconFiles(): Promise { +async function currentIconFiles(): Promise { const emcnIcons = (await readdir(EMCN_ICONS_DIRECTORY)) .filter((file) => file.endsWith('.tsx')) .sort() @@ -226,16 +346,55 @@ async function defaultIconFiles(): Promise { return [...STATIC_ICON_FILES, ...emcnIcons] } -async function scanFiles(files: string[]): Promise { +function gitOutput(arguments_: string[]): string { + const result = Bun.spawnSync(['git', ...arguments_], { + cwd: ROOT, + stdout: 'pipe', + stderr: 'pipe', + }) + if (result.exitCode !== 0) { + const error = new TextDecoder().decode(result.stderr).trim() + throw new Error(`git ${arguments_.join(' ')} failed: ${error}`) + } + return new TextDecoder().decode(result.stdout) +} + +function baseIconPaths(baseCommit: string): string[] { + const output = gitOutput([ + 'ls-tree', + '-r', + '--name-only', + baseCommit, + '--', + ...STATIC_ICON_PATHS, + 'packages/emcn/src/icons', + ]) + return output + .split('\n') + .filter( + (file) => + STATIC_ICON_PATHS.includes(file) || /^packages\/emcn\/src\/icons\/.*\.tsx$/.test(file) + ) +} + +async function scanCurrentFiles(files: string[]): Promise { const candidates: PrecisionCandidate[] = [] + const invalidExceptions: InvalidPrecisionException[] = [] for (const file of files) { - candidates.push(...findPrecisionCandidates(await readFile(file, 'utf8'), file)) + const analysis = analyzeIconSource(await readFile(file, 'utf8'), file) + candidates.push(...analysis.candidates) + invalidExceptions.push(...analysis.invalidExceptions) } - return candidates + return { candidates, invalidExceptions } } -async function loadBaseline(): Promise { - return JSON.parse(await readFile(BASELINE_PATH, 'utf8')) as PrecisionBaseline +function scanBaseFiles(baseCommit: string, files: string[]): PrecisionCandidate[] { + const candidates: PrecisionCandidate[] = [] + for (const file of files) { + const source = gitOutput(['show', `${baseCommit}:${file}`]) + candidates.push(...findPrecisionCandidates(source, path.join(ROOT, file))) + } + return candidates } function printCandidate(candidate: PrecisionCandidate): void { @@ -246,64 +405,53 @@ function printCandidate(candidate: PrecisionCandidate): void { } async function main(): Promise { - const updateBaseline = process.argv.includes('--update-baseline') - const unknownArguments = process.argv - .slice(2) - .filter((argument) => argument !== '--update-baseline') - if (unknownArguments.length > 0) { - console.error(`Unknown argument(s): ${unknownArguments.join(', ')}`) + const [baseRef, ...unknownArguments] = process.argv.slice(2) + if (!baseRef || unknownArguments.length > 0 || baseRef.startsWith('-')) { + console.error('Usage: bun run check:icon-path-precision ') + console.error('Example: bun run check:icon-path-precision staging') process.exit(1) } - const files = await defaultIconFiles() - const candidates = await scanFiles(files) - if (updateBaseline) { - const baseline = createPrecisionBaseline(candidates) - await writeFile(BASELINE_PATH, `${JSON.stringify(baseline, null, 2)}\n`) - console.log( - `✓ Wrote ${Object.keys(baseline.entries).length} exact legacy icon path fingerprint(s) to ${path.relative(ROOT, BASELINE_PATH)}.` - ) - return - } + const baseCommit = gitOutput(['rev-parse', '--verify', `${baseRef}^{commit}`]).trim() + const files = await currentIconFiles() + const current = await scanCurrentFiles(files) + const baseCandidates = scanBaseFiles(baseCommit, baseIconPaths(baseCommit)) + const newCandidates = findNewPrecisionCandidates(current.candidates, baseCandidates) - const baseline = await loadBaseline() - if (baseline.maxFractionDigits !== MAX_ICON_PATH_FRACTION_DIGITS) { + if (current.invalidExceptions.length > 0) { console.error( - `Icon path precision baseline uses ${baseline.maxFractionDigits} digits; checker expects ${MAX_ICON_PATH_FRACTION_DIGITS}.` + `\nFound ${current.invalidExceptions.length} invalid SVG precision exception(s):\n` ) - process.exit(1) + for (const exception of current.invalidExceptions) { + console.error(` ${exception.file}:${exception.line} — ${exception.message}`) + } } - const comparison = comparePrecisionBaseline(candidates, baseline) - if (comparison.unbaselined.length > 0) { + if (newCandidates.length > 0) { console.error( - `\nFound ${comparison.unbaselined.length} new or changed icon path(s) with more than ${MAX_ICON_PATH_FRACTION_DIGITS} fractional digits:\n` + `\nFound ${newCandidates.length} new or changed icon path(s) with more than ${MAX_ICON_PATH_FRACTION_DIGITS} fractional digits compared with ${baseRef}:\n` ) - for (const candidate of comparison.unbaselined) printCandidate(candidate) + for (const candidate of newCandidates) printCandidate(candidate) console.error( '\nRound only numeric values inside the literal d attribute to at most two decimal places.' ) console.error( - 'Do not round transform or viewBox values automatically; verify those geometry changes separately.' - ) - } - - if (comparison.staleBaseline.length > 0) { - console.error( - `\nThe icon path precision baseline has ${comparison.staleBaseline.length} stale entr${comparison.staleBaseline.length === 1 ? 'y' : 'ies'}.` + 'If extra precision is visibly necessary, place this reasoned exception immediately before that path:' ) + console.error(`{/** + * ${PRECISION_EXCEPTION_DIRECTIVE} Explain why rounding changes this geometry. + */}`) console.error( - 'After confirming the legacy path was removed, rounded, or intentionally changed, regenerate the baseline with:' + 'Do not round transform or viewBox values automatically; verify those geometry changes separately.' ) - console.error(' bun run scripts/check-icon-path-precision.ts --update-baseline\n') } - if (comparison.unbaselined.length > 0 || comparison.staleBaseline.length > 0) { + if (current.invalidExceptions.length > 0 || newCandidates.length > 0) { process.exit(1) } console.log( - `✓ No new overly precise icon paths (${files.length} files; ${Object.keys(baseline.entries).length} exact legacy path fingerprints ratcheted).` + `✓ No new overly precise icon paths compared with ${baseRef} (${files.length} current icon files checked).` ) } diff --git a/scripts/run-audits.ts b/scripts/run-audits.ts index d671a0908fa..4e1b8ae5a17 100644 --- a/scripts/run-audits.ts +++ b/scripts/run-audits.ts @@ -17,6 +17,7 @@ import path from 'node:path' const EXCLUDED: Record = { 'check:audits': 'this runner', 'check:migrations': 'needs a git base ref argument', + 'check:icon-path-precision': 'needs a git base ref argument', 'check:api-validation': 'superseded by the :strict variant, which this runner does run', } From 61a843a49f78fa9b167c8f5bc369ff6dce3336fe Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:04:25 -0700 Subject: [PATCH 6/6] perf(icons): enforce three-decimal paths --- .github/workflows/test-build.yml | 10 -- apps/docs/components/icons.tsx | 38 +++---- apps/sim/components/icons.tsx | 38 +++---- .../emcn/src/icons/document-attachment.tsx | 2 +- scripts/check-icon-path-precision.test.ts | 49 ++------- scripts/check-icon-path-precision.ts | 102 +++--------------- scripts/run-audits.ts | 1 - 7 files changed, 65 insertions(+), 175 deletions(-) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 772d2031bf5..80a0ec62352 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -123,16 +123,6 @@ jobs: - name: Repo audits run: bun run check:audits - - name: Check SVG icon path precision - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - BASE_REF="origin/${{ github.base_ref }}" - git fetch --depth=1 origin "${{ github.base_ref }}" 2>/dev/null || true - else - BASE_REF="HEAD~1" - fi - bun run check:icon-path-precision "$BASE_REF" - - name: Migration safety (zero-downtime) audit run: | if [ "${{ github.event_name }}" = "pull_request" ]; then diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 423055b27ac..4628c9425c7 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -675,11 +675,11 @@ export function SlackIcon(props: SVGProps) { fill='#E01E5A' /> ) { - - + + @@ -915,7 +915,7 @@ export function ConnectIcon(props: SVGProps) { xmlns='http://www.w3.org/2000/svg' > @@ -1513,7 +1513,7 @@ export function InputIcon(props: SVGProps) { xmlns='http://www.w3.org/2000/svg' > @@ -1721,7 +1721,7 @@ export function OpenAIIcon(props: SVGProps) { xmlns='http://www.w3.org/2000/svg' > @@ -2754,7 +2754,7 @@ export function ExtendIcon(props: SVGProps) { return ( ) { /> @@ -3054,7 +3054,7 @@ export function LinearIcon(props: React.SVGProps) { > ) @@ -5853,23 +5853,23 @@ export function PosthogIcon(props: SVGProps) { xmlns='http://www.w3.org/2000/svg' > @@ -6029,7 +6029,7 @@ export function ZendeskIcon(props: SVGProps) { > @@ -6751,11 +6751,11 @@ export function GitlabIcon(props: SVGProps) { ) { /> ) diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 423055b27ac..4628c9425c7 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -675,11 +675,11 @@ export function SlackIcon(props: SVGProps) { fill='#E01E5A' /> ) { - - + + @@ -915,7 +915,7 @@ export function ConnectIcon(props: SVGProps) { xmlns='http://www.w3.org/2000/svg' > @@ -1513,7 +1513,7 @@ export function InputIcon(props: SVGProps) { xmlns='http://www.w3.org/2000/svg' > @@ -1721,7 +1721,7 @@ export function OpenAIIcon(props: SVGProps) { xmlns='http://www.w3.org/2000/svg' > @@ -2754,7 +2754,7 @@ export function ExtendIcon(props: SVGProps) { return ( ) { /> @@ -3054,7 +3054,7 @@ export function LinearIcon(props: React.SVGProps) { > ) @@ -5853,23 +5853,23 @@ export function PosthogIcon(props: SVGProps) { xmlns='http://www.w3.org/2000/svg' > @@ -6029,7 +6029,7 @@ export function ZendeskIcon(props: SVGProps) { > @@ -6751,11 +6751,11 @@ export function GitlabIcon(props: SVGProps) { ) { /> ) diff --git a/packages/emcn/src/icons/document-attachment.tsx b/packages/emcn/src/icons/document-attachment.tsx index 957678cdf23..57986b5e519 100644 --- a/packages/emcn/src/icons/document-attachment.tsx +++ b/packages/emcn/src/icons/document-attachment.tsx @@ -16,7 +16,7 @@ export function DocumentAttachment(props: SVGProps) { {...props} > { - it('accepts the two-decimal boundary and ignores geometry outside literal paths', () => { + it('accepts the three-decimal boundary and ignores geometry outside literal paths', () => { const source = ` const dynamicPath = 'M0.1234 1' const unrelated = "d='M0.1234 1'" export function SafeIcon() { return ( - + ) @@ -29,10 +28,10 @@ describe('icon path precision audit', () => { }) }) - it('finds ordinary decimals and exponents finer than a hundredth', () => { + it('finds ordinary decimals and exponents finer than a thousandth', () => { const source = ` export function PreciseIcon() { - return + return } ` @@ -40,8 +39,8 @@ describe('icon path precision audit', () => { expect(candidates).toHaveLength(1) expect(candidates[0]).toMatchObject({ icon: 'PreciseIcon', - maxFractionDigits: 3, - offendingNumbers: ['0.123', '1e-3'], + maxFractionDigits: 4, + offendingNumbers: ['0.1234', '1e-4'], }) expect(effectiveFractionDigits('2.13949e-05')).toBe(10) }) @@ -51,8 +50,8 @@ describe('icon path precision audit', () => { export function ExpressionIcon() { return ( - - + + ) } @@ -84,7 +83,7 @@ describe('icon path precision audit', () => { it('rejects exceptions without a reason and exceptions on already-clean paths', () => { const missingReason = ` export function PreciseIcon() { - return {/** svg-path-precision-exception: */} + return {/** svg-path-precision-exception: */} } ` const unnecessary = ` @@ -102,31 +101,7 @@ describe('icon path precision audit', () => { expect(unnecessaryAnalysis.invalidExceptions[0]?.message).toContain('unnecessary') }) - it('grandfathers exact paths from the target branch but rejects additions and edits', () => { - const base = ` - export function LegacyIcon() { - return - } - ` - const unchanged = findPrecisionCandidates(base, FIXTURE_PATH) - expect(findNewPrecisionCandidates(unchanged, unchanged)).toEqual([]) - - const duplicate = ` - export function LegacyIcon() { - return - } - ` - expect( - findNewPrecisionCandidates(findPrecisionCandidates(duplicate, FIXTURE_PATH), unchanged) - ).toHaveLength(1) - - const changed = base.replace('0.123', '0.1234') - expect( - findNewPrecisionCandidates(findPrecisionCandidates(changed, FIXTURE_PATH), unchanged) - ).toHaveLength(1) - }) - - it('requires a committed exception to remain while its precise path remains', () => { + it('requires a reasoned exception to remain while its precise path remains', () => { const excepted = ` export function PreciseBrandIcon() { return ( @@ -143,8 +118,6 @@ describe('icon path precision audit', () => { ) expect(findPrecisionCandidates(excepted, FIXTURE_PATH)).toEqual([]) - expect( - findNewPrecisionCandidates(findPrecisionCandidates(exceptionRemoved, FIXTURE_PATH), []) - ).toHaveLength(1) + expect(findPrecisionCandidates(exceptionRemoved, FIXTURE_PATH)).toHaveLength(1) }) }) diff --git a/scripts/check-icon-path-precision.ts b/scripts/check-icon-path-precision.ts index a49236603e4..3df32cb4fd9 100644 --- a/scripts/check-icon-path-precision.ts +++ b/scripts/check-icon-path-precision.ts @@ -1,21 +1,18 @@ #!/usr/bin/env bun /** - * Prevents newly introduced overly precise numeric values in literal SVG icon - * `d` attributes. + * Prevents overly precise numeric values in literal SVG icon `d` attributes. * - * Two decimal places are enough for the reusable icons covered here: additional - * digits increase shipped source without a visible benefit. Paths already in the - * target branch are grandfathered by exact content, while new paths and edits to - * old paths must satisfy the limit or carry a reasoned local exception. + * Three decimal places are enough for the reusable icons covered here: additional + * digits increase shipped source without a visible benefit. Every path must + * satisfy the limit or carry a reasoned local exception. * * Scope is intentionally limited to the shared app/docs icon catalogs and EMCN * icon components. SVG transforms, view boxes, dynamic path expressions, and * page-specific artwork are not inspected because their safe precision depends * on context. * - * Run against the intended merge target: `bun run check:icon-path-precision staging` + * Run with `bun run check:icon-path-precision`. */ -import { createHash } from 'node:crypto' import { readdir, readFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' @@ -27,11 +24,10 @@ const STATIC_ICON_FILES = [ path.join(ROOT, 'apps/docs/components/icons.tsx'), path.join(ROOT, 'apps/sim/components/icons.tsx'), ] -const STATIC_ICON_PATHS = STATIC_ICON_FILES.map((file) => normalizedRelativePath(file)) const SVG_NUMBER_PATTERN = /[+-]?(?:(?:\d+\.\d*)|(?:\.\d+)|(?:\d+))(?:[eE][+-]?\d+)?/g const PRECISION_EXCEPTION_DIRECTIVE = 'svg-path-precision-exception:' -export const MAX_ICON_PATH_FRACTION_DIGITS = 2 +export const MAX_ICON_PATH_FRACTION_DIGITS = 3 interface ParsedPrecisionException { line: number @@ -49,7 +45,6 @@ export interface PrecisionCandidate { file: string icon: string line: number - pathHash: string maxFractionDigits: number offendingNumbers: string[] } @@ -261,10 +256,6 @@ export function effectiveFractionDigits(numberLiteral: string): number { return Math.max(writtenFractionDigits, exponentFractionDigits) } -function hashPath(pathValue: string): string { - return createHash('sha256').update(pathValue).digest('hex') -} - function normalizedRelativePath(file: string): string { return path.relative(ROOT, file).split(path.sep).join('/') } @@ -296,7 +287,7 @@ export function analyzeIconSource(source: string, file: string): IconPrecisionAn invalidExceptions.push({ file: normalizedFile, line: literalPath.exception.line, - message: 'Exception is unnecessary because this path uses at most two decimal places.', + message: 'Exception is unnecessary because this path uses at most three decimal places.', }) } else { continue @@ -308,7 +299,6 @@ export function analyzeIconSource(source: string, file: string): IconPrecisionAn file: normalizedFile, icon: literalPath.icon, line: literalPath.line, - pathHash: hashPath(literalPath.value), maxFractionDigits: Math.max(...preciseNumbers.map(effectiveFractionDigits)), offendingNumbers: [...new Set(preciseNumbers)].slice(0, 4), }) @@ -321,23 +311,6 @@ export function findPrecisionCandidates(source: string, file: string): Precision return analyzeIconSource(source, file).candidates } -export function findNewPrecisionCandidates( - current: PrecisionCandidate[], - base: PrecisionCandidate[] -): PrecisionCandidate[] { - const allowedCounts = new Map() - for (const candidate of base) { - allowedCounts.set(candidate.pathHash, (allowedCounts.get(candidate.pathHash) ?? 0) + 1) - } - - const seenCounts = new Map() - return current.filter((candidate) => { - const seen = (seenCounts.get(candidate.pathHash) ?? 0) + 1 - seenCounts.set(candidate.pathHash, seen) - return seen > (allowedCounts.get(candidate.pathHash) ?? 0) - }) -} - async function currentIconFiles(): Promise { const emcnIcons = (await readdir(EMCN_ICONS_DIRECTORY)) .filter((file) => file.endsWith('.tsx')) @@ -346,37 +319,6 @@ async function currentIconFiles(): Promise { return [...STATIC_ICON_FILES, ...emcnIcons] } -function gitOutput(arguments_: string[]): string { - const result = Bun.spawnSync(['git', ...arguments_], { - cwd: ROOT, - stdout: 'pipe', - stderr: 'pipe', - }) - if (result.exitCode !== 0) { - const error = new TextDecoder().decode(result.stderr).trim() - throw new Error(`git ${arguments_.join(' ')} failed: ${error}`) - } - return new TextDecoder().decode(result.stdout) -} - -function baseIconPaths(baseCommit: string): string[] { - const output = gitOutput([ - 'ls-tree', - '-r', - '--name-only', - baseCommit, - '--', - ...STATIC_ICON_PATHS, - 'packages/emcn/src/icons', - ]) - return output - .split('\n') - .filter( - (file) => - STATIC_ICON_PATHS.includes(file) || /^packages\/emcn\/src\/icons\/.*\.tsx$/.test(file) - ) -} - async function scanCurrentFiles(files: string[]): Promise { const candidates: PrecisionCandidate[] = [] const invalidExceptions: InvalidPrecisionException[] = [] @@ -388,15 +330,6 @@ async function scanCurrentFiles(files: string[]): Promise return { candidates, invalidExceptions } } -function scanBaseFiles(baseCommit: string, files: string[]): PrecisionCandidate[] { - const candidates: PrecisionCandidate[] = [] - for (const file of files) { - const source = gitOutput(['show', `${baseCommit}:${file}`]) - candidates.push(...findPrecisionCandidates(source, path.join(ROOT, file))) - } - return candidates -} - function printCandidate(candidate: PrecisionCandidate): void { console.error( ` ${candidate.file}:${candidate.line} (${candidate.icon}) — ${candidate.maxFractionDigits} fractional digits` @@ -405,18 +338,13 @@ function printCandidate(candidate: PrecisionCandidate): void { } async function main(): Promise { - const [baseRef, ...unknownArguments] = process.argv.slice(2) - if (!baseRef || unknownArguments.length > 0 || baseRef.startsWith('-')) { - console.error('Usage: bun run check:icon-path-precision ') - console.error('Example: bun run check:icon-path-precision staging') + if (process.argv.length > 2) { + console.error('Usage: bun run check:icon-path-precision') process.exit(1) } - const baseCommit = gitOutput(['rev-parse', '--verify', `${baseRef}^{commit}`]).trim() const files = await currentIconFiles() const current = await scanCurrentFiles(files) - const baseCandidates = scanBaseFiles(baseCommit, baseIconPaths(baseCommit)) - const newCandidates = findNewPrecisionCandidates(current.candidates, baseCandidates) if (current.invalidExceptions.length > 0) { console.error( @@ -427,13 +355,13 @@ async function main(): Promise { } } - if (newCandidates.length > 0) { + if (current.candidates.length > 0) { console.error( - `\nFound ${newCandidates.length} new or changed icon path(s) with more than ${MAX_ICON_PATH_FRACTION_DIGITS} fractional digits compared with ${baseRef}:\n` + `\nFound ${current.candidates.length} icon path(s) with more than ${MAX_ICON_PATH_FRACTION_DIGITS} fractional digits:\n` ) - for (const candidate of newCandidates) printCandidate(candidate) + for (const candidate of current.candidates) printCandidate(candidate) console.error( - '\nRound only numeric values inside the literal d attribute to at most two decimal places.' + '\nRound only numeric values inside the literal d attribute to at most three decimal places.' ) console.error( 'If extra precision is visibly necessary, place this reasoned exception immediately before that path:' @@ -446,12 +374,12 @@ async function main(): Promise { ) } - if (current.invalidExceptions.length > 0 || newCandidates.length > 0) { + if (current.invalidExceptions.length > 0 || current.candidates.length > 0) { process.exit(1) } console.log( - `✓ No new overly precise icon paths compared with ${baseRef} (${files.length} current icon files checked).` + `✓ All literal icon paths use at most three decimal places (${files.length} files checked).` ) } diff --git a/scripts/run-audits.ts b/scripts/run-audits.ts index 4e1b8ae5a17..d671a0908fa 100644 --- a/scripts/run-audits.ts +++ b/scripts/run-audits.ts @@ -17,7 +17,6 @@ import path from 'node:path' const EXCLUDED: Record = { 'check:audits': 'this runner', 'check:migrations': 'needs a git base ref argument', - 'check:icon-path-precision': 'needs a git base ref argument', 'check:api-validation': 'superseded by the :strict variant, which this runner does run', }