-
Notifications
You must be signed in to change notification settings - Fork 50
[Project Solar / Phase 1 / Cherry-pick] Flight icons tokens pipeline / Generation of symbol-js (Part 1: Code)
#3709
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
didoo
merged 2 commits into
main
from
project-solar/phase-1/HDS-6024/HDS-6144/flight-icons-symbol-js__code
Mar 18, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@hashicorp/flight-icons": minor | ||
| --- | ||
|
|
||
| Added additional build and sync scripts which generate loader modules and types to support the transition from Flight to Carbon icons systems. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
|
|
||
| import type { IconName } from './svg'; | ||
|
|
||
| export interface IconAsset { | ||
| iconName: IconName; | ||
| category: string; | ||
| size: string; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| export interface IconCatalog { | ||
| assets: IconAsset[]; | ||
| } | ||
|
|
||
| declare const catalog: IconCatalog; | ||
| export default catalog; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
packages/flight-icons/scripts/build-parts/generateBundleSymbolJS.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| /** | ||
| * Copyright IBM Corp. 2021, 2025 | ||
| * SPDX-License-Identifier: MPL-2.0 | ||
| */ | ||
|
|
||
| import fs from 'fs-extra'; | ||
| import prettier from 'prettier'; | ||
| import path from 'path'; | ||
| import replaceDynamicColor from './replaceDynamicColor'; | ||
|
|
||
| import { ConfigData } from '../@types/ConfigData'; | ||
| import { AssetsCatalog } from '../@types/AssetsCatalog'; | ||
|
|
||
| const prettierConfig = { | ||
| tabWidth: 4, | ||
| singleQuote: true, | ||
| trailingComma: 'none' | ||
| } as const; | ||
|
|
||
| // important: if you update this function, update the identical one in `packages/components/src/services/hds-icon-registry.ts` as well (and vice versa) | ||
| function makeDomSafeId(value: string): string { | ||
| return value.replace(/[^a-zA-Z0-9_-]/g, '-'); | ||
| } | ||
|
|
||
| // important: if you update this function, update the identical one in `packages/components/src/services/hds-icon-registry.ts` as well (and vice versa) | ||
| function makeSymbolIdFromKey(key: string): string { | ||
| return `hds-icon-${makeDomSafeId(key)}`; | ||
| } | ||
|
|
||
| const getSymbolModule = (sourceSvg: string, id: string): string => { | ||
| const viewBoxMatch = sourceSvg.match(/viewBox="([^"]+)"/); | ||
| const viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 16 16'; | ||
|
|
||
| let innerContent = sourceSvg | ||
| .replace(/<svg[^>]*>/, '') | ||
| .replace(/<\/svg>/, ''); | ||
|
|
||
| innerContent = replaceDynamicColor(innerContent); | ||
|
|
||
| return `export default \`<symbol id="${id}" viewBox="${viewBox}">${innerContent}</symbol>\`;`; | ||
| }; | ||
|
|
||
| export async function generateBundleSymbolJS({ config, catalog }: { config: ConfigData, catalog: AssetsCatalog }): Promise<void> { | ||
| const tempSVGFolderPath = config.tempFolder; | ||
| const mappingFilePath = path.resolve(__dirname, '../hds-carbon-icon-map.json'); | ||
| const carbonIconsPath = path.resolve(__dirname, '../../node_modules/@carbon/icons/svg'); | ||
|
|
||
| let mapping: Record<string, string> = {}; | ||
| try { | ||
| mapping = await fs.readJSON(mappingFilePath); | ||
| } catch { | ||
| console.warn('β οΈ Map file not found.'); | ||
| } | ||
|
|
||
| // Define folders | ||
| const outputFolder = `${config.mainFolder}/symbol-js`; | ||
| const flightFolder = `${outputFolder}/flight`; | ||
| const carbonFolder = `${outputFolder}/carbon`; | ||
alex-ju marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Ensure folders exist (but empty) | ||
| await fs.emptyDir(outputFolder); | ||
| await fs.ensureDir(flightFolder); | ||
| await fs.ensureDir(carbonFolder); | ||
|
|
||
| const registry: Record<string, { flight: Record<string, string>, carbon: string | null }> = {}; | ||
|
|
||
| for (const { fileName } of catalog.assets) { | ||
| const match = fileName.match(/^(.*)-(16|24)$/); | ||
|
|
||
| if (match) { | ||
| const [, baseName, size] = match; | ||
|
|
||
| if (!registry[baseName]) { | ||
| registry[baseName] = { flight: {}, carbon: null }; | ||
| } | ||
|
|
||
| // --- FLIGHT --- | ||
| try { | ||
| const key = `flight-${baseName}-${size}`; | ||
| const symbolId = makeSymbolIdFromKey(key); | ||
|
|
||
| const flightSource = await fs.readFile(`${tempSVGFolderPath}/${fileName}.svg`, 'utf8'); | ||
| const flightContent = await prettier.format( | ||
| getSymbolModule(flightSource, symbolId), | ||
| { ...prettierConfig, parser: 'typescript' } | ||
| ); | ||
|
|
||
| await fs.writeFile(`${flightFolder}/${fileName}.js`, flightContent); | ||
| } catch (err) { | ||
| console.error(`Error reading Flight icon: ${fileName}`, err); | ||
| } | ||
|
|
||
| registry[baseName].flight[size] = `() => import('./flight/${fileName}.js')`; | ||
|
|
||
| // --- CARBON --- | ||
| const carbonName = mapping[baseName]; | ||
|
|
||
| if (carbonName && !registry[baseName].carbon) { | ||
| const carbonPath = path.join(carbonIconsPath, '32', `${carbonName}.svg`); | ||
|
|
||
| if (fs.existsSync(carbonPath)) { | ||
| const key = `carbon-${baseName}`; | ||
| const symbolId = makeSymbolIdFromKey(key); | ||
|
|
||
| const carbonSource = await fs.readFile(carbonPath, 'utf8'); | ||
| const carbonContent = await prettier.format(getSymbolModule(carbonSource, symbolId), { ...prettierConfig, parser: 'typescript' }); | ||
|
|
||
| await fs.writeFile(`${carbonFolder}/${baseName}.js`, carbonContent); | ||
|
|
||
| registry[baseName].carbon = `() => import('./carbon/${baseName}.js')`; | ||
| } else { | ||
| console.warn(`β οΈ Carbon icon missing: ${carbonName} (size 32) - Mapped from Flight icon ${fileName}`); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Generate Registry (JS + Types) | ||
| const registryLines = Object.entries(registry).map(([baseName, data]) => { | ||
| const flightSizes = Object.entries(data.flight) | ||
| .map(([size, imp]) => `'${size}': ${imp}`) | ||
| .join(', '); | ||
|
|
||
| const carbonImp = data.carbon ? data.carbon : 'null'; | ||
|
|
||
| return `'${baseName}': { | ||
| flight: { ${flightSizes} }, | ||
| carbon: ${carbonImp} | ||
| },`; | ||
| }); | ||
|
|
||
| const registryJsContent = await prettier.format(` | ||
| export const IconRegistry = { | ||
| ${registryLines.join('\n')} | ||
| }; | ||
| `, { ...prettierConfig, parser: 'babel' }); | ||
|
|
||
| await fs.writeFile(`${outputFolder}/registry.js`, registryJsContent); | ||
|
|
||
| const registryDtsContent = await prettier.format(` | ||
| import type { IconName } from '../svg'; | ||
|
|
||
| export interface HdsIconModule { | ||
| default: string; | ||
| } | ||
|
|
||
| export interface HdsIconRegistryEntry { | ||
| flight: { | ||
| [size: string]: () => Promise<HdsIconModule>; | ||
| }; | ||
| carbon: (() => Promise<HdsIconModule>) | null; | ||
| } | ||
|
|
||
| export declare const IconRegistry: Record<IconName, HdsIconRegistryEntry>; | ||
| `, { ...prettierConfig, parser: 'typescript' }); | ||
|
|
||
| await fs.writeFile(`${outputFolder}/registry.d.ts`, registryDtsContent); | ||
| } | ||
12 changes: 12 additions & 0 deletions
12
packages/flight-icons/scripts/build-parts/replaceDynamicColor.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| /** | ||
| * Copyright IBM Corp. 2021, 2025 | ||
| * SPDX-License-Identifier: MPL-2.0 | ||
| */ | ||
|
|
||
| export default function replaceDynamicColor(source: string) { | ||
| // completely remove any "fill" attribute that has #000001 ("dynamic" color in Figma, equivalent of "currentColor") as value | ||
| // the reason for this is that the Ember addon uses the "fill" attribute with "currentColor" value (or a value passed as prop by the user) | ||
| // to set the color of the children <path> elements (see https://github.com/hashicorp/flight/issues/200) so the only way to have it work properly | ||
| // is not to use `fill="currentColor"` on the <path> elements, but to completely remove the fill attribute so it's inherited from the parent <SVG> element | ||
| return source.replace(/ fill="#000001"/gi, ''); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.