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 (
)
@@ -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',
}