11#!/usr/bin/env bun
22/**
3- * Prevents newly introduced overly precise numeric values in literal SVG icon
4- * `d` attributes.
3+ * Prevents overly precise numeric values in literal SVG icon `d` attributes.
54 *
6- * Two decimal places are enough for the reusable icons covered here: additional
7- * digits increase shipped source without a visible benefit. Paths already in the
8- * target branch are grandfathered by exact content, while new paths and edits to
9- * old paths must satisfy the limit or carry a reasoned local exception.
5+ * Three decimal places are enough for the reusable icons covered here: additional
6+ * digits increase shipped source without a visible benefit. Every path must
7+ * satisfy the limit or carry a reasoned local exception.
108 *
119 * Scope is intentionally limited to the shared app/docs icon catalogs and EMCN
1210 * icon components. SVG transforms, view boxes, dynamic path expressions, and
1311 * page-specific artwork are not inspected because their safe precision depends
1412 * on context.
1513 *
16- * Run against the intended merge target: `bun run check:icon-path-precision staging`
14+ * Run with `bun run check:icon-path-precision`.
1715 */
18- import { createHash } from 'node:crypto'
1916import { readdir , readFile } from 'node:fs/promises'
2017import path from 'node:path'
2118import { fileURLToPath } from 'node:url'
@@ -27,11 +24,10 @@ const STATIC_ICON_FILES = [
2724 path . join ( ROOT , 'apps/docs/components/icons.tsx' ) ,
2825 path . join ( ROOT , 'apps/sim/components/icons.tsx' ) ,
2926]
30- const STATIC_ICON_PATHS = STATIC_ICON_FILES . map ( ( file ) => normalizedRelativePath ( file ) )
3127const SVG_NUMBER_PATTERN = / [ + - ] ? (?: (?: \d + \. \d * ) | (?: \. \d + ) | (?: \d + ) ) (?: [ e E ] [ + - ] ? \d + ) ? / g
3228const PRECISION_EXCEPTION_DIRECTIVE = 'svg-path-precision-exception:'
3329
34- export const MAX_ICON_PATH_FRACTION_DIGITS = 2
30+ export const MAX_ICON_PATH_FRACTION_DIGITS = 3
3531
3632interface ParsedPrecisionException {
3733 line : number
@@ -49,7 +45,6 @@ export interface PrecisionCandidate {
4945 file : string
5046 icon : string
5147 line : number
52- pathHash : string
5348 maxFractionDigits : number
5449 offendingNumbers : string [ ]
5550}
@@ -261,10 +256,6 @@ export function effectiveFractionDigits(numberLiteral: string): number {
261256 return Math . max ( writtenFractionDigits , exponentFractionDigits )
262257}
263258
264- function hashPath ( pathValue : string ) : string {
265- return createHash ( 'sha256' ) . update ( pathValue ) . digest ( 'hex' )
266- }
267-
268259function normalizedRelativePath ( file : string ) : string {
269260 return path . relative ( ROOT , file ) . split ( path . sep ) . join ( '/' )
270261}
@@ -296,7 +287,7 @@ export function analyzeIconSource(source: string, file: string): IconPrecisionAn
296287 invalidExceptions . push ( {
297288 file : normalizedFile ,
298289 line : literalPath . exception . line ,
299- message : 'Exception is unnecessary because this path uses at most two decimal places.' ,
290+ message : 'Exception is unnecessary because this path uses at most three decimal places.' ,
300291 } )
301292 } else {
302293 continue
@@ -308,7 +299,6 @@ export function analyzeIconSource(source: string, file: string): IconPrecisionAn
308299 file : normalizedFile ,
309300 icon : literalPath . icon ,
310301 line : literalPath . line ,
311- pathHash : hashPath ( literalPath . value ) ,
312302 maxFractionDigits : Math . max ( ...preciseNumbers . map ( effectiveFractionDigits ) ) ,
313303 offendingNumbers : [ ...new Set ( preciseNumbers ) ] . slice ( 0 , 4 ) ,
314304 } )
@@ -321,23 +311,6 @@ export function findPrecisionCandidates(source: string, file: string): Precision
321311 return analyzeIconSource ( source , file ) . candidates
322312}
323313
324- export function findNewPrecisionCandidates (
325- current : PrecisionCandidate [ ] ,
326- base : PrecisionCandidate [ ]
327- ) : PrecisionCandidate [ ] {
328- const allowedCounts = new Map < string , number > ( )
329- for ( const candidate of base ) {
330- allowedCounts . set ( candidate . pathHash , ( allowedCounts . get ( candidate . pathHash ) ?? 0 ) + 1 )
331- }
332-
333- const seenCounts = new Map < string , number > ( )
334- return current . filter ( ( candidate ) => {
335- const seen = ( seenCounts . get ( candidate . pathHash ) ?? 0 ) + 1
336- seenCounts . set ( candidate . pathHash , seen )
337- return seen > ( allowedCounts . get ( candidate . pathHash ) ?? 0 )
338- } )
339- }
340-
341314async function currentIconFiles ( ) : Promise < string [ ] > {
342315 const emcnIcons = ( await readdir ( EMCN_ICONS_DIRECTORY ) )
343316 . filter ( ( file ) => file . endsWith ( '.tsx' ) )
@@ -346,37 +319,6 @@ async function currentIconFiles(): Promise<string[]> {
346319 return [ ...STATIC_ICON_FILES , ...emcnIcons ]
347320}
348321
349- function gitOutput ( arguments_ : string [ ] ) : string {
350- const result = Bun . spawnSync ( [ 'git' , ...arguments_ ] , {
351- cwd : ROOT ,
352- stdout : 'pipe' ,
353- stderr : 'pipe' ,
354- } )
355- if ( result . exitCode !== 0 ) {
356- const error = new TextDecoder ( ) . decode ( result . stderr ) . trim ( )
357- throw new Error ( `git ${ arguments_ . join ( ' ' ) } failed: ${ error } ` )
358- }
359- return new TextDecoder ( ) . decode ( result . stdout )
360- }
361-
362- function baseIconPaths ( baseCommit : string ) : string [ ] {
363- const output = gitOutput ( [
364- 'ls-tree' ,
365- '-r' ,
366- '--name-only' ,
367- baseCommit ,
368- '--' ,
369- ...STATIC_ICON_PATHS ,
370- 'packages/emcn/src/icons' ,
371- ] )
372- return output
373- . split ( '\n' )
374- . filter (
375- ( file ) =>
376- STATIC_ICON_PATHS . includes ( file ) || / ^ p a c k a g e s \/ e m c n \/ s r c \/ i c o n s \/ .* \. t s x $ / . test ( file )
377- )
378- }
379-
380322async function scanCurrentFiles ( files : string [ ] ) : Promise < IconPrecisionAnalysis > {
381323 const candidates : PrecisionCandidate [ ] = [ ]
382324 const invalidExceptions : InvalidPrecisionException [ ] = [ ]
@@ -388,15 +330,6 @@ async function scanCurrentFiles(files: string[]): Promise<IconPrecisionAnalysis>
388330 return { candidates, invalidExceptions }
389331}
390332
391- function scanBaseFiles ( baseCommit : string , files : string [ ] ) : PrecisionCandidate [ ] {
392- const candidates : PrecisionCandidate [ ] = [ ]
393- for ( const file of files ) {
394- const source = gitOutput ( [ 'show' , `${ baseCommit } :${ file } ` ] )
395- candidates . push ( ...findPrecisionCandidates ( source , path . join ( ROOT , file ) ) )
396- }
397- return candidates
398- }
399-
400333function printCandidate ( candidate : PrecisionCandidate ) : void {
401334 console . error (
402335 ` ${ candidate . file } :${ candidate . line } (${ candidate . icon } ) — ${ candidate . maxFractionDigits } fractional digits`
@@ -405,18 +338,13 @@ function printCandidate(candidate: PrecisionCandidate): void {
405338}
406339
407340async function main ( ) : Promise < void > {
408- const [ baseRef , ...unknownArguments ] = process . argv . slice ( 2 )
409- if ( ! baseRef || unknownArguments . length > 0 || baseRef . startsWith ( '-' ) ) {
410- console . error ( 'Usage: bun run check:icon-path-precision <base-ref>' )
411- console . error ( 'Example: bun run check:icon-path-precision staging' )
341+ if ( process . argv . length > 2 ) {
342+ console . error ( 'Usage: bun run check:icon-path-precision' )
412343 process . exit ( 1 )
413344 }
414345
415- const baseCommit = gitOutput ( [ 'rev-parse' , '--verify' , `${ baseRef } ^{commit}` ] ) . trim ( )
416346 const files = await currentIconFiles ( )
417347 const current = await scanCurrentFiles ( files )
418- const baseCandidates = scanBaseFiles ( baseCommit , baseIconPaths ( baseCommit ) )
419- const newCandidates = findNewPrecisionCandidates ( current . candidates , baseCandidates )
420348
421349 if ( current . invalidExceptions . length > 0 ) {
422350 console . error (
@@ -427,13 +355,13 @@ async function main(): Promise<void> {
427355 }
428356 }
429357
430- if ( newCandidates . length > 0 ) {
358+ if ( current . candidates . length > 0 ) {
431359 console . error (
432- `\nFound ${ newCandidates . length } new or changed icon path(s) with more than ${ MAX_ICON_PATH_FRACTION_DIGITS } fractional digits compared with ${ baseRef } :\n`
360+ `\nFound ${ current . candidates . length } icon path(s) with more than ${ MAX_ICON_PATH_FRACTION_DIGITS } fractional digits:\n`
433361 )
434- for ( const candidate of newCandidates ) printCandidate ( candidate )
362+ for ( const candidate of current . candidates ) printCandidate ( candidate )
435363 console . error (
436- '\nRound only numeric values inside the literal d attribute to at most two decimal places.'
364+ '\nRound only numeric values inside the literal d attribute to at most three decimal places.'
437365 )
438366 console . error (
439367 'If extra precision is visibly necessary, place this reasoned exception immediately before that path:'
@@ -446,12 +374,12 @@ async function main(): Promise<void> {
446374 )
447375 }
448376
449- if ( current . invalidExceptions . length > 0 || newCandidates . length > 0 ) {
377+ if ( current . invalidExceptions . length > 0 || current . candidates . length > 0 ) {
450378 process . exit ( 1 )
451379 }
452380
453381 console . log (
454- `✓ No new overly precise icon paths compared with ${ baseRef } (${ files . length } current icon files checked).`
382+ `✓ All literal icon paths use at most three decimal places (${ files . length } files checked).`
455383 )
456384}
457385
0 commit comments