1515
1616/**
1717 * @fileoverview Generates SVG React components for each icon.
18- * N.B. we expect ../src/generated/ to contain SVG definitions of all the icons already
19- * generated by fantasticon (in other words, run ./generate-icon-fonts.js first).
18+ *
19+ * Paths are taken from the same resource SVGs as {@link generate-icon-paths.mjs} (used by {@code <Icon />}).
20+ * We intentionally do not use glyph paths from the generated icon font: those live in an upscaled coordinate
21+ * system and require fragile scale/translate transforms (see {@link ICON_RASTER_SCALING_FACTOR} in common.mjs).
2022 */
2123
2224// @ts -check
@@ -25,24 +27,12 @@ import { pascalCase } from "change-case";
2527import Handlebars from "handlebars" ;
2628import { mkdirSync , readFileSync , rmSync , writeFileSync } from "node:fs" ;
2729import { join , resolve } from "node:path" ;
28- import { parse } from "svg-parser" ;
2930
30- import { generatedComponentsDir , generatedSrcDir , ICON_RASTER_SCALING_FACTOR , ICON_SIZES } from "./common.mjs" ;
31+ import { generatedComponentsDir , generatedSrcDir , iconsMetadata } from "./common.mjs" ;
32+ import { extractPathsFromResourceSvg } from "./extractPathsFromResourceSvg.mjs" ;
3133
3234Handlebars . registerHelper ( "pascalCase" , iconName => pascalCase ( iconName ) ) ;
3335
34- /**
35- * Notes on icon component template implementation:
36- *
37- * The components rendered by this template (`<AddClip>`, `<Calendar>`, etc.) rely on a centered scale `transform` to
38- * display their SVG paths correctly.
39- *
40- * In this template, the `<path>` element applies `transform-origin` using the `style` attribute rather than
41- * `transformOrigin`. Although `trasformOrigin` was added as a supported SVG attribute to React in 2023,
42- * it is still difficult to use without compile-time and/or runtime errors, see:
43- * - https://github.com/facebook/react/pull/26130
44- * - https://github.com/palantir/blueprint/issues/6591
45- */
4636const iconComponentTemplate = Handlebars . compile (
4737 readFileSync ( resolve ( import . meta. dirname , "iconComponent.tsx.hbs" ) , "utf8" ) ,
4838) ;
@@ -51,50 +41,21 @@ const componentsIndexTemplate = Handlebars.compile(
5141) ;
5242const indexTemplate = Handlebars . compile ( readFileSync ( resolve ( import . meta. dirname , "index.ts.hbs" ) , "utf8" ) ) ;
5343
54- /** @type { { 16: {[iconName: string]: string}; 20: {[iconName: string]: string} } } */
55- const iconPaths = {
56- [ ICON_SIZES [ 0 ] ] : { } ,
57- [ ICON_SIZES [ 1 ] ] : { } ,
58- } ;
59-
60- // parse icon paths from the generated SVG font
61- for ( const iconSize of ICON_SIZES ) {
62- const iconFontSvgDocument = readFileSync (
63- join ( generatedSrcDir , `${ iconSize } px/blueprint-icons-${ iconSize } .svg` ) ,
64- "utf8" ,
65- ) ;
66-
67- console . info ( `Parsing SVG glyphs from generated ${ iconSize } px SVG icon font...` ) ;
68- parseIconGlyphs ( iconFontSvgDocument , ( iconName , iconPath ) => {
69- iconPaths [ iconSize ] [ iconName ] = iconPath ;
70- } ) ;
71- console . info ( `Parsed ${ Object . keys ( iconPaths [ iconSize ] ) . length } ${ iconSize } px icons.` ) ;
72- }
73-
74- // clear existing icon components
7544console . info ( "Clearing existing icon modules..." ) ;
7645rmSync ( generatedComponentsDir , { recursive : true , force : true } ) ;
7746
78- // generate an ES module for each icon
7947console . info ( "Generating ES modules for each icon..." ) ;
8048mkdirSync ( generatedComponentsDir , { recursive : true } ) ;
8149
82- for ( const [ iconName , icon16pxPath ] of Object . entries ( iconPaths [ 16 ] ) ) {
83- const icon20pxPath = iconPaths [ 20 ] [ iconName ] ;
84- if ( icon20pxPath === undefined ) {
85- console . error ( `Could not find corresponding 20px icon path for ${ iconName } , skipping!` ) ;
86- continue ;
87- }
50+ for ( const { iconName } of iconsMetadata ) {
51+ const paths16 = await extractPathsFromResourceSvg ( 16 , iconName ) ;
52+ const paths20 = await extractPathsFromResourceSvg ( 20 , iconName ) ;
8853 writeFileSync (
8954 join ( generatedComponentsDir , `${ iconName } .tsx` ) ,
90- // Notes on icon component template implementation:
91- // - path "translation" transform must use "viewbox" dimensions, not "size", in order to avoid issues
92- // like https://github.com/palantir/blueprint/issues/6220
9355 iconComponentTemplate ( {
9456 iconName,
95- icon16pxPath,
96- icon20pxPath,
97- pathScaleFactor : 1 / ICON_RASTER_SCALING_FACTOR ,
57+ paths16Json : JSON . stringify ( paths16 ) ,
58+ paths20Json : JSON . stringify ( paths20 ) ,
9859 } ) ,
9960 ) ;
10061}
@@ -103,43 +64,16 @@ console.info(`Writing index file for all icon modules...`);
10364writeFileSync (
10465 join ( generatedComponentsDir , "index.ts" ) ,
10566 componentsIndexTemplate ( {
106- iconNames : Object . keys ( iconPaths [ 16 ] ) ,
67+ iconNames : iconsMetadata . map ( i => i . iconName ) ,
10768 } ) ,
10869) ;
10970
11071console . info ( `Writing index file for package...` ) ;
11172writeFileSync (
11273 join ( generatedSrcDir , "index.ts" ) ,
11374 indexTemplate ( {
114- iconNames : Object . keys ( iconPaths [ 16 ] ) ,
75+ iconNames : iconsMetadata . map ( i => i . iconName ) ,
11576 } ) ,
11677) ;
11778
11879console . info ( "Done." ) ;
119-
120- /**
121- * Parse all icons of a given size from the SVG font generated by fantasticon.
122- * At this point we've already optimized the icon SVGs through svgo (via fantasticon), so
123- * we avoid duplicating that work by reading the generated glyphs here.
124- *
125- * @param {string } iconFontSvgDocument
126- * @param {(iconName: string, iconPath: string) => void } cb iterator for each icon path
127- */
128- function parseIconGlyphs ( iconFontSvgDocument , cb ) {
129- const rootNode = parse ( iconFontSvgDocument ) ;
130- // @ts -ignore
131- const defs = rootNode . children [ 0 ] . children [ 0 ] ;
132- const glyphs = defs . children [ 0 ] . children . filter ( node => node . tagName === "glyph" ) ;
133-
134- for ( const glyph of glyphs ) {
135- const name = glyph . properties [ "glyph-name" ] ;
136-
137- // HACKHACK: for some reason, there are duplicates with the suffix "-1", so we ignore those
138- if ( name . endsWith ( "-1" ) ) {
139- continue ;
140- }
141-
142- const path = glyph . properties [ "d" ] ;
143- cb ( name , path ) ;
144- }
145- }
0 commit comments